From 46548b957a37914c0416b9ca07bfd1569d168317 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Sat, 22 Aug 2026 06:46:56 -0400 Subject: [PATCH 1/3] refactor: cut agent over to catalog provider runtime --- Dockerfile | 4 - README.md | 138 +- compose.capacity-provider.yml | 98 +- docs/capacity-provider-runtime.md | 180 +- docs/src/content/objectives/core.md | 8 +- package-lock.json | 11064 ++-------------- package.json | 62 +- scripts/agents/agents.ts | 17 - scripts/agents/test-agent-contracts.ts | 5 - .../agents/test-agent-tools-live-copilot.ts | 216 - scripts/agents/test-agent-tools-live.ts | 240 - scripts/build/build-dist.ts | 26 +- .../build-capacity-provider-container.ts | 3 +- .../test-capacity-provider-container.ts | 165 - .../test-capacity-provider-live-tools.ts | 9 - scripts/packages/release-verify.ts | 361 +- scripts/support/verify-driver.ts | 48 +- scripts/testing/test-smoke.ts | 142 - src/agent-runtime.ts | 166 - src/agents/adapters/accounts/codex-auth.ts | 62 - src/agents/adapters/accounts/notification.ts | 41 - .../adapters/codex/codex-child-pipe-guard.ts | 32 - src/agents/adapters/codex/codex-readiness.ts | 214 - .../adapters/codex/execution-codex-core.ts | 491 - .../codex/execution-codex-deadline.ts | 16 - .../codex/execution-codex-receipts.ts | 61 - .../codex/execution-codex-redaction.ts | 20 - .../adapters/codex/execution-codex-result.ts | 313 - .../adapters/codex/execution-codex-stream.ts | 45 - src/agents/adapters/codex/execution-codex.ts | 29 - src/agents/adapters/execution/execution.ts | 209 - .../integrations/execution-discord.ts | 290 - .../adapters/integrations/execution-jira.ts | 330 - .../adapters/opencode/execution-opencode.ts | 63 - .../execution-platform-operation.ts | 68 - .../adapters/operations/execution-workflow.ts | 499 - src/agents/adapters/operations/operations.ts | 158 - .../reconciliation/execution-codex-adapter.ts | 470 - .../execution-completion-receipts.ts | 78 - .../execution-discord-adapter.ts | 305 - .../execution-github-issues-adapter.ts | 278 - .../reconciliation/execution-jira-adapter.ts | 382 - .../reconciliation/execution-timeout.ts | 92 - .../repositories/execution-github-issues.ts | 358 - .../adapters/repositories/repository.ts | 58 - .../adapters/runtime/codex-runtime-home.ts | 54 - src/agents/adapters/tools/mutations.ts | 67 - src/agents/adapters/tools/research.ts | 53 - src/agents/adapters/tools/verification.ts | 60 - src/agents/cli-tools.ts | 1 - src/agents/content/content-artifacts.ts | 34 - src/agents/context/code-context-packs.ts | 292 - src/agents/context/context-processor.ts | 216 - src/agents/contracts/knowledge.ts | 142 - src/agents/contracts/messages.ts | 384 - src/agents/contracts/research.ts | 72 - src/agents/contracts/run.ts | 30 - src/agents/handlers/actor.ts | 14 - .../context/shared-context-queries.ts | 99 - src/agents/handlers/estimate.ts | 223 - .../handlers/execution-content-artifacts.ts | 56 - .../handlers/execution-content-context.ts | 421 - .../handlers/execution-content-prompt.ts | 278 - src/agents/handlers/execution-content.ts | 489 - src/agents/handlers/releaser.ts | 20 - src/agents/handlers/reporter.ts | 7 - .../results/execution-content-completion.ts | 18 - src/agents/handlers/shared.ts | 94 - src/agents/handlers/writer.ts | 14 - src/agents/index.ts | 1 - src/agents/kernel/agents/agent-kernel.ts | 493 - .../kernel/artifacts/artifact-manifest.ts | 329 - .../kernel/artifacts/artifact-receipts.ts | 84 - .../assignments/assignment-preflight.ts | 47 - src/agents/kernel/execution/context-loader.ts | 111 - .../kernel/execution/execution-dispatcher.ts | 144 - .../kernel/execution/execution-result.ts | 77 - src/agents/kernel/execution/run-types.ts | 23 - src/agents/kernel/runtime/kernel-runtime.ts | 28 - src/agents/kernel/runtime/runtime-helpers.ts | 56 - .../telemetry/activity-profile-resolver.ts | 82 - src/agents/kernel/telemetry/telemetry.ts | 42 - .../kernel/validation/failure-classifier.ts | 18 - .../kernel/validation/fallback-controller.ts | 23 - .../kernel/validation/output-validator.ts | 40 - src/agents/runtime/runtime-types.ts | 264 - src/agents/support/cli.ts | 109 - src/agents/support/registry-helper.ts | 19 - src/agents/support/registry.ts | 237 - src/agents/support/spec-loader.ts | 77 - .../support/spec-normalizer-activities.ts | 200 - .../support/spec-normalizer-execution.ts | 135 - src/agents/support/spec-normalizer-policy.ts | 225 - .../support/spec-normalizer-primitives.ts | 144 - src/agents/support/spec-normalizer.ts | 198 - src/agents/support/spec-types.ts | 116 - .../testing/agent-authoring-diagnostics.ts | 176 - src/agents/testing/agent-contracts.ts | 278 - src/agents/testing/agent-test-catalog.ts | 120 - src/agents/testing/e2e-harness.ts | 3 - .../e2e-harness/create-agent-test-runtime.ts | 224 - .../testing/e2e-harness/exec-file-async.ts | 272 - .../e2e-harness/patch-fixture-agent-specs.ts | 183 - .../engineering-agent-test-definition.ts | 126 - src/agents/testing/message-chain.ts | 218 - .../testing/research-agent-test-definition.ts | 102 - src/agents/tools/agent-tool-completion.ts | 80 - src/agents/tools/agent-tool-copilot.ts | 44 - src/agents/tools/agent-tool-mcp-server.ts | 168 - src/agents/tools/agent-tool-runtime.ts | 443 - src/agents/tools/agent-tool-schema.ts | 161 - src/agents/tools/agent-tool-telemetry.ts | 286 - .../assignment/communication-response-tool.ts | 57 - .../tools/assignment/discussion-tool.ts | 38 - .../assignment/operational-content-tool.ts | 157 - src/agents/tools/content-tool-runtime.ts | 492 - .../tools/content/relation-validation.ts | 46 - src/agents/tools/governed-research-tools.ts | 175 - src/agents/tools/path-scope/path-matcher.ts | 15 - .../tools/runtime/agent-tool-runtime-types.ts | 54 - .../tools/status/assignment-activity-tool.ts | 25 - .../tools/status/assignment-status-tool.ts | 67 - src/agents/tools/treedx-proxy-client.ts | 175 - src/agents/tools/treedx-proxy-tool.ts | 87 - src/api/agents/agent-routes.ts | 143 - src/api/auth/d1-database.ts | 22 - src/api/auth/d1-provider.ts | 129 - src/api/auth/d1-store-core.ts | 498 - src/api/auth/d1-store.ts | 424 - src/api/auth/d1-user-store.ts | 157 - src/api/auth/memory-provider.ts | 317 - src/api/auth/rbac.ts | 180 - src/api/auth/tokens.ts | 74 - src/api/capacity/providers/providers.ts | 81 - src/api/configuration/config.ts | 144 - src/api/hosting/railway.ts | 91 - src/api/index.ts | 9 - src/api/operations/operations-routes.ts | 59 - src/api/operations/operations.ts | 30 - .../projects-core/project-route-helpers.ts | 51 - .../projects/projects-core/project-routes.ts | 370 - .../projects/projects-core/project-summary.ts | 210 - src/api/support/app.ts | 81 - src/api/support/capabilities.ts | 35 - src/api/support/http.ts | 59 - src/api/support/sdk-dispatch.ts | 20 - src/api/support/sdk-routes.ts | 50 - src/api/support/server.ts | 9 - src/api/support/templates.ts | 36 - src/api/types.ts | 253 - src/env.yaml | 2167 +-- src/index.ts | 77 +- src/provider-acceptance.ts | 350 - src/provider-governance.ts | 8 +- src/provider/accounts/identity.ts | 45 +- .../capacity/accounting/usage-reporter.ts | 44 - .../assignment-agent-spec-loader.ts | 86 - .../assignments/assignment-result-reporter.ts | 299 - .../assignments/assignment-tool-policy.ts | 25 - .../capacity/assignments/kernel-assignment.ts | 73 - .../assignments/performance-summary.ts | 79 - .../workday-assignment-authority.ts | 92 - .../providers/execution-provider-selection.ts | 130 - .../catalog/assignment-tool-catalog.ts | 121 - src/provider/configuration/capabilities.ts | 6 +- src/provider/configuration/config.ts | 283 +- src/provider/configuration/manifest.ts | 32 +- src/provider/configuration/value-utils.ts | 16 - src/provider/coordination/client.ts | 26 +- .../coordination/connection-scheduler.ts | 96 - src/provider/coordination/connection-state.ts | 6 +- src/provider/coordination/contracts.ts | 5 + src/provider/coordination/coordinator.ts | 105 +- src/provider/coordination/lease-client.ts | 43 - src/provider/coordination/lease-recovery.ts | 108 +- src/provider/execution/contracts.ts | 35 + src/provider/execution/execution-lifecycle.ts | 414 - src/provider/execution/execution-support.ts | 122 - src/provider/execution/executor-loader.ts | 27 + .../execution/kernel-bridge-support.ts | 18 - src/provider/execution/kernel-bridge.ts | 499 - src/provider/execution/promise-timeout.ts | 14 - src/provider/lifecycle/entrypoint.ts | 4 - src/provider/lifecycle/lifecycle.ts | 180 +- src/provider/operations/runner-contracts.ts | 24 - src/provider/operations/runner-lifecycle.ts | 336 - src/provider/operations/runner.ts | 241 +- .../projects-core/availability-projection.ts | 68 - .../projects-core/project-materialization.ts | 261 - src/provider/reporting/codex-event-message.ts | 40 - src/provider/reporting/error-diagnostics.ts | 52 - src/provider/reporting/message-recorder.ts | 53 - src/provider/reporting/mode-run-reporter.ts | 38 - src/provider/reporting/mode-run-telemetry.ts | 110 - .../reporting/runtime-event-reporter.ts | 26 - src/provider/teams/multi-team-runtime.ts | 440 +- .../treedx/graph/treedx-context-adapter.ts | 287 - .../treedx/repositories/treedx-handle.ts | 46 - src/services/agent-worktrees.ts | 81 - src/services/approval-policy.ts | 39 - src/services/report-paths.ts | 23 - src/services/runtime-paths.ts | 63 - .../deploy-capacity-provider.workflow.yml | 2 +- templates/railway/capacity-provider.yml | 18 - .../provider/provider-acceptance.test.ts | 156 - tests/contract/agents/agent-contracts.test.ts | 30 - .../agents/agent-test-catalog.test.ts | 120 - .../content-tool-validation-atomicity.test.ts | 150 - .../agents/execution-content-contract.test.ts | 499 - ...cution-content-validation-contract.test.ts | 111 - .../agents/knowledge-contract.test.ts | 158 - .../contract/agents/starter-contracts.test.ts | 21 - .../verified-context-query-input.test.ts | 25 - tests/contract/package/package-shape.test.ts | 407 - .../contract/package/release-version.test.ts | 15 - .../provider/provider-architecture.test.ts | 44 - .../provider/workday-contracts.test.ts | 54 - tests/fixtures/0007_site_web_sessions.sql | 196 - .../basic/expected-result.json | 3 - .../basic/trigger-message.json | 11 - .../docs-engineer/basic/expected-result.json | 3 - .../docs-engineer/basic/trigger-message.json | 6 - .../docs-planner/basic/expected-result.json | 3 - .../docs-planner/basic/trigger-message.json | 4 - .../docs-reviewer/basic/expected-result.json | 3 - .../docs-reviewer/basic/trigger-message.json | 4 - .../basic/expected-result.json | 3 - .../basic/trigger-message.json | 4 - .../governed-mutation/basic/chain.json | 5 - .../basic/expected-result.json | 3 - .../basic/trigger-message.json | 34 - .../basic/expected-result.json | 3 - .../basic/trigger-message.json | 4 - .../releaser/basic/expected-result.json | 3 - .../releaser/basic/trigger-message.json | 4 - .../basic/expected-result.json | 3 - .../basic/trigger-message.json | 4 - tests/fixtures/runtime-site/src/manifest.yaml | 7 - .../fixtures/runtime-site/treeseed.site.yaml | 29 - .../agents/agent-tool-copilot.test.ts | 70 - .../agents/agent-tool-mcp-server.test.ts | 303 - ...ounded-assignment-repository-paths.test.ts | 145 - ...-a-timed-out-turn-before-returning.test.ts | 84 - ...receipts-explicit-completion-gates.test.ts | 316 - ...hen-the-agent-sandbox-is-read-only.test.ts | 212 - ...pension-from-durable-tool-evidence.test.ts | 19 - ...obtain-missing-completion-receipts.test.ts | 373 - ...-fallback-when-auth-json-is-absent.test.ts | 303 - ...t-listed-in-the-assignment-catalog.test.ts | 327 - ...quired-response-content-checkpoint.test.ts | 218 - .../reconciliation/operations-adapter.test.ts | 135 - .../agents/testing/message-chain.test.ts | 24 - ...-through-the-treedx-proxy-envelope.test.ts | 364 - .../accounts/manager-session-recovery.test.ts | 37 - ...nt-lacks-canonical-project-context.test.ts | 369 - ...-after-a-durable-terminal-response.test.ts | 333 - ...-an-unexpected-preparation-failure.test.ts | 228 - .../local-capacity-store.test.ts | 120 - .../agent-tool-provider-runtime.test.ts | 215 - .../execution-provider-selection.test.ts | 81 - .../provider-market-client.test.ts | 39 - .../availability-projection.test.ts | 14 - .../teams/multi-team-coordinator.test.ts | 239 - .../provider/teams/multi-team-runtime.test.ts | 54 - .../provider/teams/multi-team-service.test.ts | 125 - .../testing/connection-scheduler.test.ts | 41 - .../provider/testing/lease-recovery.test.ts | 67 - .../testing/mode-run-telemetry.test.ts | 139 - .../services/agent-worktrees.test.ts | 59 - tests/modern/provider-boundary.test.ts | 36 + tests/modern/provider-runner.test.ts | 67 + tests/support/setup-runtime.ts | 26 - .../adapters/codex-child-pipe-guard.test.ts | 15 - .../unit/agents/adapters/codex-stream.test.ts | 91 - .../agents/adapters/content-receipts.test.ts | 89 - tests/unit/agents/adapters/opencode.test.ts | 27 - .../agent-authoring-diagnostics.test.ts | 65 - tests/unit/agents/agent-tool-schema.test.ts | 28 - tests/unit/agents/artifact-manifest.test.ts | 354 - tests/unit/agents/content-artifacts.test.ts | 35 - tests/unit/agents/context-processor.test.ts | 165 - .../non-agpl-contribution-policy.test.ts | 16 - .../agent-definition-compatibility.test.ts | 25 - .../execution-retry-propagation.test.ts | 39 - .../agents/execution-time-guidance.test.ts | 86 - tests/unit/agents/fallback-controller.test.ts | 19 - tests/unit/agents/registry.test.ts | 381 - .../timing/assignment-activity-tool.test.ts | 31 - .../assignment-operational-safety.test.ts | 114 - .../timing/assignment-process-timeout.test.ts | 11 - .../timing/assignment-status-tool.test.ts | 39 - .../operational-content-response.test.ts | 60 - .../validation/early-completion.test.ts | 14 - .../validation/failure-classifier.test.ts | 18 - .../workflow-execution-provider.test.ts | 34 - .../assignment-performance-summary.test.ts | 18 - .../assignment-result-reporter.test.ts | 48 - tests/unit/provider/budgets.test.ts | 64 - tests/unit/provider/capabilities.test.ts | 11 - .../workday-assignment-authority.test.ts | 106 - .../configuration/runtime-version.test.ts | 12 - tests/unit/provider/disk-capacity.test.ts | 29 - tests/unit/provider/error-diagnostics.test.ts | 26 - .../provider/runtime-event-reporter.test.ts | 26 - .../provider/treedx-context-adapter.test.ts | 100 - treeseed.package.yaml | 4 - vitest.config.ts | 32 +- 307 files changed, 1821 insertions(+), 47783 deletions(-) delete mode 100644 scripts/agents/agents.ts delete mode 100644 scripts/agents/test-agent-contracts.ts delete mode 100644 scripts/agents/test-agent-tools-live-copilot.ts delete mode 100644 scripts/agents/test-agent-tools-live.ts delete mode 100644 scripts/capacity/providers/test-capacity-provider-container.ts delete mode 100644 scripts/capacity/providers/test-capacity-provider-live-tools.ts delete mode 100644 scripts/testing/test-smoke.ts delete mode 100644 src/agent-runtime.ts delete mode 100644 src/agents/adapters/accounts/codex-auth.ts delete mode 100644 src/agents/adapters/accounts/notification.ts delete mode 100644 src/agents/adapters/codex/codex-child-pipe-guard.ts delete mode 100644 src/agents/adapters/codex/codex-readiness.ts delete mode 100644 src/agents/adapters/codex/execution-codex-core.ts delete mode 100644 src/agents/adapters/codex/execution-codex-deadline.ts delete mode 100644 src/agents/adapters/codex/execution-codex-receipts.ts delete mode 100644 src/agents/adapters/codex/execution-codex-redaction.ts delete mode 100644 src/agents/adapters/codex/execution-codex-result.ts delete mode 100644 src/agents/adapters/codex/execution-codex-stream.ts delete mode 100644 src/agents/adapters/codex/execution-codex.ts delete mode 100644 src/agents/adapters/execution/execution.ts delete mode 100644 src/agents/adapters/integrations/execution-discord.ts delete mode 100644 src/agents/adapters/integrations/execution-jira.ts delete mode 100644 src/agents/adapters/opencode/execution-opencode.ts delete mode 100644 src/agents/adapters/operations/execution-platform-operation.ts delete mode 100644 src/agents/adapters/operations/execution-workflow.ts delete mode 100644 src/agents/adapters/operations/operations.ts delete mode 100644 src/agents/adapters/reconciliation/execution-codex-adapter.ts delete mode 100644 src/agents/adapters/reconciliation/execution-completion-receipts.ts delete mode 100644 src/agents/adapters/reconciliation/execution-discord-adapter.ts delete mode 100644 src/agents/adapters/reconciliation/execution-github-issues-adapter.ts delete mode 100644 src/agents/adapters/reconciliation/execution-jira-adapter.ts delete mode 100644 src/agents/adapters/reconciliation/execution-timeout.ts delete mode 100644 src/agents/adapters/repositories/execution-github-issues.ts delete mode 100644 src/agents/adapters/repositories/repository.ts delete mode 100644 src/agents/adapters/runtime/codex-runtime-home.ts delete mode 100644 src/agents/adapters/tools/mutations.ts delete mode 100644 src/agents/adapters/tools/research.ts delete mode 100644 src/agents/adapters/tools/verification.ts delete mode 100644 src/agents/cli-tools.ts delete mode 100644 src/agents/content/content-artifacts.ts delete mode 100644 src/agents/context/code-context-packs.ts delete mode 100644 src/agents/context/context-processor.ts delete mode 100644 src/agents/contracts/knowledge.ts delete mode 100644 src/agents/contracts/messages.ts delete mode 100644 src/agents/contracts/research.ts delete mode 100644 src/agents/contracts/run.ts delete mode 100644 src/agents/handlers/actor.ts delete mode 100644 src/agents/handlers/context/shared-context-queries.ts delete mode 100644 src/agents/handlers/estimate.ts delete mode 100644 src/agents/handlers/execution-content-artifacts.ts delete mode 100644 src/agents/handlers/execution-content-context.ts delete mode 100644 src/agents/handlers/execution-content-prompt.ts delete mode 100644 src/agents/handlers/execution-content.ts delete mode 100644 src/agents/handlers/releaser.ts delete mode 100644 src/agents/handlers/reporter.ts delete mode 100644 src/agents/handlers/results/execution-content-completion.ts delete mode 100644 src/agents/handlers/shared.ts delete mode 100644 src/agents/handlers/writer.ts delete mode 100644 src/agents/index.ts delete mode 100644 src/agents/kernel/agents/agent-kernel.ts delete mode 100644 src/agents/kernel/artifacts/artifact-manifest.ts delete mode 100644 src/agents/kernel/artifacts/artifact-receipts.ts delete mode 100644 src/agents/kernel/capacity/assignments/assignment-preflight.ts delete mode 100644 src/agents/kernel/execution/context-loader.ts delete mode 100644 src/agents/kernel/execution/execution-dispatcher.ts delete mode 100644 src/agents/kernel/execution/execution-result.ts delete mode 100644 src/agents/kernel/execution/run-types.ts delete mode 100644 src/agents/kernel/runtime/kernel-runtime.ts delete mode 100644 src/agents/kernel/runtime/runtime-helpers.ts delete mode 100644 src/agents/kernel/telemetry/activity-profile-resolver.ts delete mode 100644 src/agents/kernel/telemetry/telemetry.ts delete mode 100644 src/agents/kernel/validation/failure-classifier.ts delete mode 100644 src/agents/kernel/validation/fallback-controller.ts delete mode 100644 src/agents/kernel/validation/output-validator.ts delete mode 100644 src/agents/runtime/runtime-types.ts delete mode 100644 src/agents/support/cli.ts delete mode 100644 src/agents/support/registry-helper.ts delete mode 100644 src/agents/support/registry.ts delete mode 100644 src/agents/support/spec-loader.ts delete mode 100644 src/agents/support/spec-normalizer-activities.ts delete mode 100644 src/agents/support/spec-normalizer-execution.ts delete mode 100644 src/agents/support/spec-normalizer-policy.ts delete mode 100644 src/agents/support/spec-normalizer-primitives.ts delete mode 100644 src/agents/support/spec-normalizer.ts delete mode 100644 src/agents/support/spec-types.ts delete mode 100644 src/agents/testing/agent-authoring-diagnostics.ts delete mode 100644 src/agents/testing/agent-contracts.ts delete mode 100644 src/agents/testing/agent-test-catalog.ts delete mode 100644 src/agents/testing/e2e-harness.ts delete mode 100644 src/agents/testing/e2e-harness/create-agent-test-runtime.ts delete mode 100644 src/agents/testing/e2e-harness/exec-file-async.ts delete mode 100644 src/agents/testing/e2e-harness/patch-fixture-agent-specs.ts delete mode 100644 src/agents/testing/engineering-agent-test-definition.ts delete mode 100644 src/agents/testing/message-chain.ts delete mode 100644 src/agents/testing/research-agent-test-definition.ts delete mode 100644 src/agents/tools/agent-tool-completion.ts delete mode 100644 src/agents/tools/agent-tool-copilot.ts delete mode 100644 src/agents/tools/agent-tool-mcp-server.ts delete mode 100644 src/agents/tools/agent-tool-runtime.ts delete mode 100644 src/agents/tools/agent-tool-schema.ts delete mode 100644 src/agents/tools/agent-tool-telemetry.ts delete mode 100644 src/agents/tools/assignment/communication-response-tool.ts delete mode 100644 src/agents/tools/assignment/discussion-tool.ts delete mode 100644 src/agents/tools/assignment/operational-content-tool.ts delete mode 100644 src/agents/tools/content-tool-runtime.ts delete mode 100644 src/agents/tools/content/relation-validation.ts delete mode 100644 src/agents/tools/governed-research-tools.ts delete mode 100644 src/agents/tools/path-scope/path-matcher.ts delete mode 100644 src/agents/tools/runtime/agent-tool-runtime-types.ts delete mode 100644 src/agents/tools/status/assignment-activity-tool.ts delete mode 100644 src/agents/tools/status/assignment-status-tool.ts delete mode 100644 src/agents/tools/treedx-proxy-client.ts delete mode 100644 src/agents/tools/treedx-proxy-tool.ts delete mode 100644 src/api/agents/agent-routes.ts delete mode 100644 src/api/auth/d1-database.ts delete mode 100644 src/api/auth/d1-provider.ts delete mode 100644 src/api/auth/d1-store-core.ts delete mode 100644 src/api/auth/d1-store.ts delete mode 100644 src/api/auth/d1-user-store.ts delete mode 100644 src/api/auth/memory-provider.ts delete mode 100644 src/api/auth/rbac.ts delete mode 100644 src/api/auth/tokens.ts delete mode 100644 src/api/capacity/providers/providers.ts delete mode 100644 src/api/configuration/config.ts delete mode 100644 src/api/hosting/railway.ts delete mode 100644 src/api/index.ts delete mode 100644 src/api/operations/operations-routes.ts delete mode 100644 src/api/operations/operations.ts delete mode 100644 src/api/projects/projects-core/project-route-helpers.ts delete mode 100644 src/api/projects/projects-core/project-routes.ts delete mode 100644 src/api/projects/projects-core/project-summary.ts delete mode 100644 src/api/support/app.ts delete mode 100644 src/api/support/capabilities.ts delete mode 100644 src/api/support/http.ts delete mode 100644 src/api/support/sdk-dispatch.ts delete mode 100644 src/api/support/sdk-routes.ts delete mode 100644 src/api/support/server.ts delete mode 100644 src/api/support/templates.ts delete mode 100644 src/api/types.ts delete mode 100644 src/provider-acceptance.ts delete mode 100644 src/provider/capacity/accounting/usage-reporter.ts delete mode 100644 src/provider/capacity/assignments/assignment-agent-spec-loader.ts delete mode 100644 src/provider/capacity/assignments/assignment-result-reporter.ts delete mode 100644 src/provider/capacity/assignments/assignment-tool-policy.ts delete mode 100644 src/provider/capacity/assignments/kernel-assignment.ts delete mode 100644 src/provider/capacity/assignments/performance-summary.ts delete mode 100644 src/provider/capacity/assignments/workday-assignment-authority.ts delete mode 100644 src/provider/capacity/providers/execution-provider-selection.ts delete mode 100644 src/provider/commerce/catalog/assignment-tool-catalog.ts delete mode 100644 src/provider/configuration/value-utils.ts delete mode 100644 src/provider/coordination/connection-scheduler.ts create mode 100644 src/provider/coordination/contracts.ts delete mode 100644 src/provider/coordination/lease-client.ts create mode 100644 src/provider/execution/contracts.ts delete mode 100644 src/provider/execution/execution-lifecycle.ts delete mode 100644 src/provider/execution/execution-support.ts create mode 100644 src/provider/execution/executor-loader.ts delete mode 100644 src/provider/execution/kernel-bridge-support.ts delete mode 100644 src/provider/execution/kernel-bridge.ts delete mode 100644 src/provider/execution/promise-timeout.ts delete mode 100644 src/provider/operations/runner-contracts.ts delete mode 100644 src/provider/operations/runner-lifecycle.ts delete mode 100644 src/provider/projects/projects-core/availability-projection.ts delete mode 100644 src/provider/projects/projects-core/project-materialization.ts delete mode 100644 src/provider/reporting/codex-event-message.ts delete mode 100644 src/provider/reporting/error-diagnostics.ts delete mode 100644 src/provider/reporting/message-recorder.ts delete mode 100644 src/provider/reporting/mode-run-reporter.ts delete mode 100644 src/provider/reporting/mode-run-telemetry.ts delete mode 100644 src/provider/reporting/runtime-event-reporter.ts delete mode 100644 src/provider/treedx/graph/treedx-context-adapter.ts delete mode 100644 src/provider/treedx/repositories/treedx-handle.ts delete mode 100644 src/services/agent-worktrees.ts delete mode 100644 src/services/approval-policy.ts delete mode 100644 src/services/report-paths.ts delete mode 100644 src/services/runtime-paths.ts delete mode 100644 templates/railway/capacity-provider.yml delete mode 100644 tests/acceptance/provider/provider-acceptance.test.ts delete mode 100644 tests/contract/agents/agent-contracts.test.ts delete mode 100644 tests/contract/agents/agent-test-catalog.test.ts delete mode 100644 tests/contract/agents/content-tool-validation-atomicity.test.ts delete mode 100644 tests/contract/agents/execution-content-contract.test.ts delete mode 100644 tests/contract/agents/execution-content-validation-contract.test.ts delete mode 100644 tests/contract/agents/knowledge-contract.test.ts delete mode 100644 tests/contract/agents/starter-contracts.test.ts delete mode 100644 tests/contract/agents/verified-context-query-input.test.ts delete mode 100644 tests/contract/package/package-shape.test.ts delete mode 100644 tests/contract/package/release-version.test.ts delete mode 100644 tests/contract/provider/provider-architecture.test.ts delete mode 100644 tests/contract/provider/workday-contracts.test.ts delete mode 100644 tests/fixtures/0007_site_web_sessions.sql delete mode 100644 tests/fixtures/agent-tests/codebase-cartographer/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/codebase-cartographer/basic/trigger-message.json delete mode 100644 tests/fixtures/agent-tests/docs-engineer/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/docs-engineer/basic/trigger-message.json delete mode 100644 tests/fixtures/agent-tests/docs-planner/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/docs-planner/basic/trigger-message.json delete mode 100644 tests/fixtures/agent-tests/docs-reviewer/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/docs-reviewer/basic/trigger-message.json delete mode 100644 tests/fixtures/agent-tests/governance-steward/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/governance-steward/basic/trigger-message.json delete mode 100644 tests/fixtures/agent-tests/governed-mutation/basic/chain.json delete mode 100644 tests/fixtures/agent-tests/knowledge-generator/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/knowledge-generator/basic/trigger-message.json delete mode 100644 tests/fixtures/agent-tests/knowledge-optimizer/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/knowledge-optimizer/basic/trigger-message.json delete mode 100644 tests/fixtures/agent-tests/releaser/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/releaser/basic/trigger-message.json delete mode 100644 tests/fixtures/agent-tests/workday-reporter/basic/expected-result.json delete mode 100644 tests/fixtures/agent-tests/workday-reporter/basic/trigger-message.json delete mode 100644 tests/fixtures/runtime-site/src/manifest.yaml delete mode 100644 tests/fixtures/runtime-site/treeseed.site.yaml delete mode 100644 tests/integration/agents/agent-tool-copilot.test.ts delete mode 100644 tests/integration/agents/agent-tool-mcp-server.test.ts delete mode 100644 tests/integration/agents/capacity/assignments/agent-tool-runtime.reads-and-searches-only-bounded-assignment-repository-paths.test.ts delete mode 100644 tests/integration/agents/capacity/providers/codex-provider.cancels-a-timed-out-turn-before-returning.test.ts delete mode 100644 tests/integration/agents/capacity/providers/codex-provider.makes-verify-and-checkpoint-receipts-explicit-completion-gates.test.ts delete mode 100644 tests/integration/agents/capacity/providers/codex-provider.prepares-an-exact-ref-worktree-even-when-the-agent-sandbox-is-read-only.test.ts delete mode 100644 tests/integration/agents/capacity/providers/codex-provider.recovers-required-response-suspension-from-durable-tool-evidence.test.ts delete mode 100644 tests/integration/agents/capacity/providers/codex-provider.resumes-the-same-thread-once-to-obtain-missing-completion-receipts.test.ts delete mode 100644 tests/integration/agents/capacity/providers/codex-provider.uses-codex-api-key-as-an-api-billed-fallback-when-auth-json-is-absent.test.ts delete mode 100644 tests/integration/agents/commerce/catalog/agent-tool-runtime.rejects-tools-not-listed-in-the-assignment-catalog.test.ts delete mode 100644 tests/integration/agents/communication/required-response-content-checkpoint.test.ts delete mode 100644 tests/integration/agents/reconciliation/operations-adapter.test.ts delete mode 100644 tests/integration/agents/testing/message-chain.test.ts delete mode 100644 tests/integration/agents/treedx/repositories/agent-tool-runtime.preserves-commit-provenance-returned-through-the-treedx-proxy-envelope.test.ts delete mode 100644 tests/integration/provider/accounts/manager-session-recovery.test.ts delete mode 100644 tests/integration/provider/capacity/assignments/assignment-runtime.fails-closed-when-a-leased-assignment-lacks-canonical-project-context.test.ts delete mode 100644 tests/integration/provider/capacity/assignments/assignment-runtime.releases-internal-execution-provider-resources-only-after-a-durable-terminal-response.test.ts delete mode 100644 tests/integration/provider/capacity/assignments/assignment-runtime.returns-a-leased-assignment-after-an-unexpected-preparation-failure.test.ts delete mode 100644 tests/integration/provider/capacity/capacity-core/local-capacity-store.test.ts delete mode 100644 tests/integration/provider/capacity/providers/agent-tool-provider-runtime.test.ts delete mode 100644 tests/integration/provider/capacity/providers/execution-provider-selection.test.ts delete mode 100644 tests/integration/provider/coordination/provider-market-client.test.ts delete mode 100644 tests/integration/provider/projects/projects-core/availability-projection.test.ts delete mode 100644 tests/integration/provider/teams/multi-team-coordinator.test.ts delete mode 100644 tests/integration/provider/teams/multi-team-runtime.test.ts delete mode 100644 tests/integration/provider/teams/multi-team-service.test.ts delete mode 100644 tests/integration/provider/testing/connection-scheduler.test.ts delete mode 100644 tests/integration/provider/testing/lease-recovery.test.ts delete mode 100644 tests/integration/provider/testing/mode-run-telemetry.test.ts delete mode 100644 tests/integration/services/agent-worktrees.test.ts create mode 100644 tests/modern/provider-boundary.test.ts create mode 100644 tests/modern/provider-runner.test.ts delete mode 100644 tests/support/setup-runtime.ts delete mode 100644 tests/unit/agents/adapters/codex-child-pipe-guard.test.ts delete mode 100644 tests/unit/agents/adapters/codex-stream.test.ts delete mode 100644 tests/unit/agents/adapters/content-receipts.test.ts delete mode 100644 tests/unit/agents/adapters/opencode.test.ts delete mode 100644 tests/unit/agents/agent-authoring-diagnostics.test.ts delete mode 100644 tests/unit/agents/agent-tool-schema.test.ts delete mode 100644 tests/unit/agents/artifact-manifest.test.ts delete mode 100644 tests/unit/agents/content-artifacts.test.ts delete mode 100644 tests/unit/agents/context-processor.test.ts delete mode 100644 tests/unit/agents/contributions/non-agpl-contribution-policy.test.ts delete mode 100644 tests/unit/agents/definitions/agent-definition-compatibility.test.ts delete mode 100644 tests/unit/agents/execution-retry-propagation.test.ts delete mode 100644 tests/unit/agents/execution-time-guidance.test.ts delete mode 100644 tests/unit/agents/fallback-controller.test.ts delete mode 100644 tests/unit/agents/registry.test.ts delete mode 100644 tests/unit/agents/timing/assignment-activity-tool.test.ts delete mode 100644 tests/unit/agents/timing/assignment-operational-safety.test.ts delete mode 100644 tests/unit/agents/timing/assignment-process-timeout.test.ts delete mode 100644 tests/unit/agents/timing/assignment-status-tool.test.ts delete mode 100644 tests/unit/agents/tools/operational-content-response.test.ts delete mode 100644 tests/unit/agents/validation/early-completion.test.ts delete mode 100644 tests/unit/agents/validation/failure-classifier.test.ts delete mode 100644 tests/unit/agents/workflow-execution-provider.test.ts delete mode 100644 tests/unit/provider/assignment-performance-summary.test.ts delete mode 100644 tests/unit/provider/assignment-result-reporter.test.ts delete mode 100644 tests/unit/provider/budgets.test.ts delete mode 100644 tests/unit/provider/capabilities.test.ts delete mode 100644 tests/unit/provider/capacity/assignments/workday-assignment-authority.test.ts delete mode 100644 tests/unit/provider/configuration/runtime-version.test.ts delete mode 100644 tests/unit/provider/disk-capacity.test.ts delete mode 100644 tests/unit/provider/error-diagnostics.test.ts delete mode 100644 tests/unit/provider/runtime-event-reporter.test.ts delete mode 100644 tests/unit/provider/treedx-context-adapter.test.ts diff --git a/Dockerfile b/Dockerfile index 104acc05..bda98acc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,6 @@ RUN apk add --no-cache ca-certificates tini util-linux git openssh-client \ && mkdir -p /data COPY --chown=65532:65532 dist ./dist -COPY --chown=65532:65532 templates ./templates -COPY --chown=65532:65532 docs ./docs COPY --chown=65532:65532 docker-entrypoint.sh ./docker-entrypoint.sh RUN chmod 0755 /app/docker-entrypoint.sh \ @@ -30,5 +28,3 @@ CMD ["manager"] FROM agent-runtime AS agent-runner ENV TREESEED_PROVIDER_ROLE=runner CMD ["runner"] - -FROM agent-runner AS railway-runtime diff --git a/README.md b/README.md index 1ecce914..85b33dd8 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,58 @@ # @treeseed/agent -`@treeseed/agent` runs Treeseed capacity providers. It provides the provider manager, provider runner, AgentKernel execution, mode scheduling, provider-local capacity enforcement, package-owned Docker/Compose assets, and deployment templates. +`@treeseed/agent` is the outbound-only capacity-provider and assignment-execution package for TreeSeed. -Use this package when your organization needs to run or maintain capacity that executes Treeseed work. Ordinary hosted projects can consume assigned capacity without owning a provider runtime. +The API owns scheduling, workdays, capacity plans, assignments, leases, authorization, and settlement. The SDK owns portable wire contracts and the catalog-driven remote client. Agent owns only provider identity/custody, availability publication, provider-local admission, trusted execution, recovery, usage, and terminal evidence. -## What You Can Run With Agent +## Runtime roles -- provider manager and provider runner roles -- AgentKernel planning/acting mode execution -- local provider diagnostics -- provider containers and compose workflows -- runtime tests for capacity scheduling and provider lifecycle +- provider manager reconciles repository-governed connections and publishes observed capacity; +- provider runner accepts an API-issued lease and invokes one trusted executor. -## Install +The package does not host a control-plane API, persist control-plane resources, synthesize project work, expose a public Agent CLI, or construct raw API paths. -```bash -npm install @treeseed/agent @treeseed/sdk -``` - -For package development: +## Development ```bash -npm install -npm run build -npm test -npm run verify:local +npm ci +npm run build:dist +npm run test:modern +npm run release:verify ``` -## Operate A Capacity Provider +Only focused tests for the current provider boundary are retained. Tests for the removed embedded API, local Agent SDK, content runtime, and legacy provider implementations were deleted with those implementations. -Use the CLI for provider lifecycle: +## Provider profile -```bash -trsd capacity build -trsd capacity up -trsd capacity status -trsd capacity logs -trsd capacity down -trsd capacity test-local -``` +Provider connections declare `serverProfile`, `controlPlaneUrl`, and `controlPlaneAudience`. Local Compose uses: -The package-owned role images are: - -```text -treeseed/agent-manager -treeseed/agent-runner -``` +- `TREESEED_SERVER_PROFILE_LOCAL_URL`; +- `TREESEED_SERVER_PROFILE_LOCAL_AUDIENCE`; +- `TREESEED_CAPACITY_PROVIDER_MANIFEST`; +- `TREESEED_PROVIDER_DATA_DIR`. -Production release images are published for `linux/amd64` and `linux/arm64`. Staging builds the provider services from source at an exact commit. The images start: - -```bash -node ./dist/provider/lifecycle/entrypoint.js manager -node ./dist/provider/lifecycle/entrypoint.js runner -node ./dist/provider/lifecycle/entrypoint.js doctor -``` - -Use the qualified role names provider manager and provider runner in architecture and implementation docs. The provider manager supervises one provider's local runtime; API-side assignment selection lives in `@treeseed/api`. - -Store provider credentials through `trsd config` or host secret managers. Do not create plaintext provider `.env` files. - -Advanced launch configuration can be declared in `treeseed.capacity-provider.yaml` and passed to `trsd capacity up --config treeseed.capacity-provider.yaml`. The manifest can select official role images or derived images built from `treeseed/agent-manager` and `treeseed/agent-runner`. Secrets belong in encrypted Treeseed config or host secret managers, not in the manifest. - -See [Capacity Provider Runtime](./docs/capacity-provider-runtime.md) for the provider check-in, assignment lease, TreeDX proxy, and mode-run protocol. - -## How Agent Fits With Other Packages - -- `@treeseed/admin` may display and manage capacity-provider configuration, status, and diagnostics. -- `@treeseed/ui` owns reusable capacity/status components. -- `@treeseed/api` owns backend control-plane routes, operation state, provider-authenticated API behavior, provider availability sessions, assignment leases, mode-run persistence, and capacity ledger settlement. -- `@treeseed/sdk` owns shared provider contracts, portable capacity/assignment types, config, and reconciliation primitives. -- `@treeseed/cli` owns the operator command surface for provider lifecycle. -- root market hosts the admin UI and future marketplace/business overlays. - -Agent runtime must stay external to the root web app and API process. - -## Common Commands - -Source entrypoints: - -```bash -node --import tsx ./src/provider/lifecycle/entrypoint.ts doctor --json -node --import tsx ./src/provider/lifecycle/entrypoint.ts manager --once --json -node --import tsx ./src/provider/lifecycle/entrypoint.ts runner --once --json -``` - -Built entrypoints: +Model/runtime implementation is injected through a trusted `TREESEED_AGENT_EXECUTOR_MODULE`. If it is absent or unhealthy, the manager advertises no executable capacity and the runner does not request assignments. ```bash node ./dist/provider/lifecycle/entrypoint.js doctor --json -node ./dist/provider/lifecycle/entrypoint.js manager --once --json -node ./dist/provider/lifecycle/entrypoint.js runner --once --json +node ./dist/provider/lifecycle/entrypoint.js manager --plan --json +node ./dist/provider/lifecycle/entrypoint.js runner --plan --json ``` -Verification: - -```bash -npm run build:dist -npm run test:unit -npm run test:smoke -npm run release:verify -npm run verify:local -``` - -CI runs `.github/workflows/verify.yml`. Capacity-provider image publication uses `.github/workflows/publish.yml` for tagged production releases; staging deployments build from source through Railway reconciliation. - -## Environment Registry - -`src/env.yaml` is the package-owned provider/runtime environment registry. It contains provider manager, provider runner, capacity-provider, and provider-launch entries. Workday budgets and scheduling policy are durable API control-plane records governed by active team allocation sets, not provider-runtime environment variables. - -Provider-neutral shared entries belong in `@treeseed/sdk`. Web/forms/Astro entries belong in `@treeseed/core`. Admin UI expectations belong in `@treeseed/admin`. Backend control-plane entries belong in `@treeseed/api`. - -## Public Surfaces +See [Capacity Provider Runtime](./docs/capacity-provider-runtime.md) for the lifecycle and recovery contract. -Exports include runtime APIs, service helpers, contracts, and binaries needed by provider deployments and package tests. Prefer SDK-owned contracts for shared model types so root market and admin code do not import agent runtime internals. +## Public package surface -Published binaries: +- `@treeseed/agent`: executor contracts and the catalog-driven assignment runner; +- `@treeseed/agent/provider-governance`: provider identity, connection, manifest, and secret-reference governance. -- `treeseed-agents` +Container entrypoints are private runtime surfaces. The package installs no executable. -## What Agent Does Not Own +## Non-ownership -- root market web app -- admin routes or UI pages -- reusable UI components -- backend PostgreSQL adapter, API routes, migrations, or operations runner -- SDK reconciliation engine -- CLI command parsing -- TreeDX repository service internals -- ecommerce, billing, licensing, or marketplace policy +Agent does not own: -See the root [Package Ownership](../../docs/package-ownership.md) guide for cross-package boundaries. +- REST, OAuth, OpenAPI, MCP, persistence, or policy; +- CLI command parsing or repository integration workflow; +- TreeDX service semantics or content persistence; +- GitHub, billing, commerce, deployment, or marketplace policy. diff --git a/compose.capacity-provider.yml b/compose.capacity-provider.yml index 28e7546a..2e13a84c 100644 --- a/compose.capacity-provider.yml +++ b/compose.capacity-provider.yml @@ -7,55 +7,23 @@ services: target: agent-manager command: ["manager"] restart: unless-stopped - ulimits: - core: - soft: 0 - hard: 0 - extra_hosts: - - "host.docker.internal:host-gateway" - environment: - TREESEED_PROVIDER_ROLE: manager - TREESEED_PROVIDER_STARTUP_MODE: ${TREESEED_PROVIDER_STARTUP_MODE:-live} + extra_hosts: ["host.docker.internal:host-gateway"] + environment: &provider-environment TREESEED_CAPACITY_PROVIDER_MANIFEST: /config/treeseed.capacity-provider.yaml TREESEED_PROVIDER_DATA_DIR: /data - TREESEED_PROVIDER_CHOWN_DATA: ${TREESEED_PROVIDER_CHOWN_DATA:-1} - TREESEED_PROVIDER_UID: ${TREESEED_PROVIDER_CONTAINER_UID:-65532} - TREESEED_PROVIDER_GID: ${TREESEED_PROVIDER_CONTAINER_GID:-65532} TREESEED_PROVIDER_ENVIRONMENT: ${TREESEED_PROVIDER_ENVIRONMENT:-local} - TREESEED_PROVIDER_CLASS: ${TREESEED_PROVIDER_CLASS:-agent} TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST: ${TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST:-} - TREESEED_MARKET_PROFILE_LOCAL_URL: ${TREESEED_MARKET_PROFILE_LOCAL_URL:-http://host.docker.internal:3000} - TREESEED_MARKET_PROFILE_LOCAL_AUDIENCE: ${TREESEED_MARKET_PROFILE_LOCAL_AUDIENCE:-http://127.0.0.1:3000} - TREESEED_PROVIDER_CAPABILITIES_FILE: ${TREESEED_PROVIDER_CAPABILITIES_FILE:-} - TREESEED_PROVIDER_BUDGET_FILE: ${TREESEED_PROVIDER_BUDGET_FILE:-} - TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS: ${TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS:-} - TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS: ${TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS:-} + TREESEED_SERVER_PROFILE_LOCAL_URL: ${TREESEED_SERVER_PROFILE_LOCAL_URL:-http://host.docker.internal:3002} + TREESEED_SERVER_PROFILE_LOCAL_AUDIENCE: ${TREESEED_SERVER_PROFILE_LOCAL_AUDIENCE:-http://127.0.0.1:3002} + TREESEED_AGENT_EXECUTOR_MODULE: ${TREESEED_AGENT_EXECUTOR_MODULE:-} + TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS: ${TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS:-1} + TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS: ${TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS:-1} TREESEED_PROVIDER_DAILY_AGENT_SECONDS_LIMIT: ${TREESEED_PROVIDER_DAILY_AGENT_SECONDS_LIMIT:-} TREESEED_PROVIDER_MONTHLY_AGENT_SECONDS_LIMIT: ${TREESEED_PROVIDER_MONTHLY_AGENT_SECONDS_LIMIT:-} TREESEED_MIN_FREE_DISK_BYTES: ${TREESEED_MIN_FREE_DISK_BYTES:-} - TREESEED_PROVIDER_MIN_FREE_DISK_BYTES: ${TREESEED_PROVIDER_MIN_FREE_DISK_BYTES:-} - TREESEED_PROVIDER_ASSIGNMENT_DISK_HEADROOM_BYTES: ${TREESEED_PROVIDER_ASSIGNMENT_DISK_HEADROOM_BYTES:-} - TREESEED_CODEX_AUTH_FILE: ${TREESEED_CODEX_AUTH_FILE:-} - TREESEED_CODEX_API_KEY: ${TREESEED_CODEX_API_KEY:-} - TREESEED_AI_GATEWAY_TOKEN: ${TREESEED_AI_GATEWAY_TOKEN:-} - TREESEED_CODEX_AUTH_JSON_B64: ${TREESEED_CODEX_AUTH_JSON_B64:-} - TREESEED_CODEX_AUTH_OVERWRITE: ${TREESEED_CODEX_AUTH_OVERWRITE:-} - TREESEED_CODEX_TIMEOUT_MS: ${TREESEED_CODEX_TIMEOUT_MS:-900000} - TREESEED_OPENCODE_SERVER_URL: ${TREESEED_OPENCODE_SERVER_URL:-} - TREESEED_OPENCODE_AUTH_FILE: ${TREESEED_OPENCODE_AUTH_FILE:-} - TREESEED_OPENCODE_SERVER_USERNAME: ${TREESEED_OPENCODE_SERVER_USERNAME:-} - TREESEED_OPENCODE_SERVER_PASSWORD: ${TREESEED_OPENCODE_SERVER_PASSWORD:-} - TREESEED_OPENROUTER_API_KEY: ${TREESEED_OPENROUTER_API_KEY:-} - TREESEED_GITHUB_COPILOT_TOKEN: ${TREESEED_GITHUB_COPILOT_TOKEN:-} - TREESEED_CLOUDFLARE_API_TOKEN: ${TREESEED_CLOUDFLARE_API_TOKEN:-} - TREESEED_CLOUDFLARE_ACCOUNT_ID: ${TREESEED_CLOUDFLARE_ACCOUNT_ID:-} - TREESEED_CONTENT_BUCKET_NAME: ${TREESEED_CONTENT_BUCKET_NAME:-} - TREESEED_GITHUB_TOKEN: ${TREESEED_GITHUB_TOKEN:-} - volumes: + volumes: &provider-volumes - "${TREESEED_PROVIDER_HOST_DATA_DIR:-.treeseed/local-capacity-providers/agent-standalone}:/data" - "${TREESEED_CAPACITY_PROVIDER_MANIFEST:-./treeseed.capacity-provider.yaml}:/config/treeseed.capacity-provider.yaml:ro" - - "${TREESEED_HOST_CODEX_AUTH_FILE:-/dev/null}:/run/treeseed-secrets/codex-auth.json:ro" - runner: image: ${TREESEED_AGENT_RUNNER_IMAGE:-treeseed/agent-runner:${TREESEED_AGENT_IMAGE_TAG:-local}} build: @@ -64,51 +32,7 @@ services: target: agent-runner command: ["runner"] restart: unless-stopped - ulimits: - core: - soft: 0 - hard: 0 - extra_hosts: - - "host.docker.internal:host-gateway" + extra_hosts: ["host.docker.internal:host-gateway"] environment: - TREESEED_PROVIDER_ROLE: runner - TREESEED_PROVIDER_STARTUP_MODE: ${TREESEED_PROVIDER_STARTUP_MODE:-live} - TREESEED_CAPACITY_PROVIDER_MANIFEST: /config/treeseed.capacity-provider.yaml - TREESEED_PROVIDER_DATA_DIR: /data - TREESEED_PROVIDER_CHOWN_DATA: ${TREESEED_PROVIDER_CHOWN_DATA:-1} - TREESEED_PROVIDER_UID: ${TREESEED_PROVIDER_CONTAINER_UID:-65532} - TREESEED_PROVIDER_GID: ${TREESEED_PROVIDER_CONTAINER_GID:-65532} - TREESEED_PROVIDER_ENVIRONMENT: ${TREESEED_PROVIDER_ENVIRONMENT:-local} - TREESEED_PROVIDER_CLASS: ${TREESEED_PROVIDER_CLASS:-agent} - TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST: ${TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST:-} - TREESEED_MARKET_PROFILE_LOCAL_URL: ${TREESEED_MARKET_PROFILE_LOCAL_URL:-http://host.docker.internal:3000} - TREESEED_MARKET_PROFILE_LOCAL_AUDIENCE: ${TREESEED_MARKET_PROFILE_LOCAL_AUDIENCE:-http://127.0.0.1:3000} - TREESEED_PROVIDER_CAPABILITIES_FILE: ${TREESEED_PROVIDER_CAPABILITIES_FILE:-} - TREESEED_PROVIDER_BUDGET_FILE: ${TREESEED_PROVIDER_BUDGET_FILE:-} - TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS: ${TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS:-} - TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS: ${TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS:-} - TREESEED_PROVIDER_DAILY_AGENT_SECONDS_LIMIT: ${TREESEED_PROVIDER_DAILY_AGENT_SECONDS_LIMIT:-} - TREESEED_PROVIDER_MONTHLY_AGENT_SECONDS_LIMIT: ${TREESEED_PROVIDER_MONTHLY_AGENT_SECONDS_LIMIT:-} - TREESEED_MIN_FREE_DISK_BYTES: ${TREESEED_MIN_FREE_DISK_BYTES:-} - TREESEED_PROVIDER_MIN_FREE_DISK_BYTES: ${TREESEED_PROVIDER_MIN_FREE_DISK_BYTES:-} - TREESEED_PROVIDER_ASSIGNMENT_DISK_HEADROOM_BYTES: ${TREESEED_PROVIDER_ASSIGNMENT_DISK_HEADROOM_BYTES:-} - TREESEED_CODEX_AUTH_FILE: ${TREESEED_CODEX_AUTH_FILE:-} - TREESEED_CODEX_API_KEY: ${TREESEED_CODEX_API_KEY:-} - TREESEED_AI_GATEWAY_TOKEN: ${TREESEED_AI_GATEWAY_TOKEN:-} - TREESEED_CODEX_AUTH_JSON_B64: ${TREESEED_CODEX_AUTH_JSON_B64:-} - TREESEED_CODEX_AUTH_OVERWRITE: ${TREESEED_CODEX_AUTH_OVERWRITE:-} - TREESEED_CODEX_TIMEOUT_MS: ${TREESEED_CODEX_TIMEOUT_MS:-900000} - TREESEED_OPENCODE_SERVER_URL: ${TREESEED_OPENCODE_SERVER_URL:-} - TREESEED_OPENCODE_AUTH_FILE: ${TREESEED_OPENCODE_AUTH_FILE:-} - TREESEED_OPENCODE_SERVER_USERNAME: ${TREESEED_OPENCODE_SERVER_USERNAME:-} - TREESEED_OPENCODE_SERVER_PASSWORD: ${TREESEED_OPENCODE_SERVER_PASSWORD:-} - TREESEED_OPENROUTER_API_KEY: ${TREESEED_OPENROUTER_API_KEY:-} - TREESEED_GITHUB_COPILOT_TOKEN: ${TREESEED_GITHUB_COPILOT_TOKEN:-} - TREESEED_CLOUDFLARE_API_TOKEN: ${TREESEED_CLOUDFLARE_API_TOKEN:-} - TREESEED_CLOUDFLARE_ACCOUNT_ID: ${TREESEED_CLOUDFLARE_ACCOUNT_ID:-} - TREESEED_CONTENT_BUCKET_NAME: ${TREESEED_CONTENT_BUCKET_NAME:-} - TREESEED_GITHUB_TOKEN: ${TREESEED_GITHUB_TOKEN:-} - volumes: - - "${TREESEED_PROVIDER_HOST_DATA_DIR:-.treeseed/local-capacity-providers/agent-standalone}:/data" - - "${TREESEED_CAPACITY_PROVIDER_MANIFEST:-./treeseed.capacity-provider.yaml}:/config/treeseed.capacity-provider.yaml:ro" - - "${TREESEED_HOST_CODEX_AUTH_FILE:-/dev/null}:/run/treeseed-secrets/codex-auth.json:ro" + <<: *provider-environment + volumes: *provider-volumes diff --git a/docs/capacity-provider-runtime.md b/docs/capacity-provider-runtime.md index 8c3c74f3..6a8b7441 100644 --- a/docs/capacity-provider-runtime.md +++ b/docs/capacity-provider-runtime.md @@ -1,173 +1,41 @@ -# Capacity Provider Runtime +# Capacity provider runtime -`@treeseed/agent` owns the package-built capacity provider runtime. It includes provider manager, provider runner, AgentKernel execution, mode scheduling, package-owned Docker/Compose assets, runtime images, and provider-local tests. +`@treeseed/agent` owns the provider-side execution boundary. The API owns workday admission, capacity plans, assignments, leases, authorization, and settlement. The SDK owns portable contracts and the catalog-driven remote client. -Canonical architecture: +The package has two private container roles: -- [Agent Capacity Implementation Roadmap](../../../docs/agent-capacity-implementation-roadmap.md) -- [Agent Capacity Domain Model](../../../docs/agent-capacity-domain-model.md) -- [Capacity Provider Agent Coordination Architecture](../../../docs/capacity_provider_agent_coordination_architecture.md) -- [Agent Kernel Mode Runtime](../../../docs/agent-kernel-mode-runtime.md) -- [Human-Machine Execution Providers](../../../docs/human-machine-providers.md) -- [Agent Capacity Operator Surfaces](../../../docs/agent-capacity-operator-surfaces.md) +- `manager` reconciles repository-governed provider connections and publishes observed availability. +- `runner` accepts an API-issued lease, invokes one trusted Agent executor, reports usage, and submits the exact terminal receipt. -## Runtime Roles +The package does not host an API, persist control-plane records, synthesize work, authorize itself, expose a public runtime CLI, or construct raw control-plane URLs. -The package-owned provider image starts `node ./dist/provider/lifecycle/entrypoint.js` with explicit roles: +## Trusted executor boundary -- `manager`: provider manager that checks in, reports availability, receives assignment leases, renews leases, dispatches runners, and reports provider-local pressure -- `runner`: provider runner that executes one leased assignment under an agent capacity envelope -- `doctor`: runtime diagnostics +Model/runtime implementation is injected through `TREESEED_AGENT_EXECUTOR_MODULE`. The module must export `createAgentExecutor()` and implement the package's `AgentExecutor` contract. Without a configured and healthy executor, the manager advertises no executable capacity and the runner does not request work. -Use the qualified names provider manager and provider runner in docs and code comments. Avoid unqualified "manager" when describing capacity behavior. +The executor receives only an API-issued assignment, lease identity, runner identity, and cancellation signal. Repository, TreeDX, model, and provider credentials must arrive through trusted provider receipts or host custody; they do not belong in agent definitions or source worktrees. -## Lifecycle Commands - -Use `trsd capacity` for provider lifecycle work: +## Local profile ```bash -trsd config -trsd capacity build -trsd capacity up --market local --provider local -trsd capacity status --market local --provider local -trsd capacity logs --market local --provider local -trsd capacity down --market local --provider local -trsd capacity test-local +docker compose -f compose.capacity-provider.yml build +docker compose -f compose.capacity-provider.yml run --rm manager manager --plan --json +docker compose -f compose.capacity-provider.yml run --rm runner runner --plan --json ``` -These commands manage runtime lifecycle and diagnostics through SDK reconciliation. They do not own allocation policy, assignment selection, mode-run persistence, or ledger settlement. - -Package-local human-machine provider verification uses `npm run test:human-machine-providers`. This runs the execution-provider contract tests, focused provider tests, provider runner lifecycle tests, the package build, and `capacity-provider:test-local`. - -`capacity-provider:test-local` is a strict Docker-backed proof for the provider manager and runner images plus compose topology. Docker must be available; the command fails with a clear diagnostic instead of skipping when Docker is unavailable. Before building images, the smoke checks Docker storage headroom and reports an explicit cleanup command such as `docker system prune -a --volumes` when the Docker filesystem is too full; pruning remains an operator action because it can delete shared images and volumes. - -## Secrets And Configuration - -Configure provider values with encrypted Treeseed machine config or host secret managers. - -Do not create plaintext `.env` files for provider keys. Do not render provider API keys, Codex auth material, Docker Hub tokens, Railway tokens, TreeDX credentials, or other secrets into Compose files, templates, logs, or manifests. - -`trsd capacity up` resolves encrypted machine config, applies non-empty process environment overrides, applies explicit CLI overrides, and injects values only into the parent Docker Compose process environment. - -## Assignment Protocol - -The implemented provider runtime protocol is provider-initiated and outbound-friendly. Production-completion phase status remains controlled by `docs/agent-capacity-completion.md`: - -1. The provider manager creates or renews `POST /v1/provider/availability-sessions` for each approved team membership. -2. The availability snapshot reports execution providers, native limits, observations, availability window, capabilities, runner concurrency, and provider-local pressure. -3. The API records a `ProviderAvailabilitySession` as generic supply. -4. The provider manager selects a team through weighted-deficit scheduling and calls `POST /v1/provider/assignments/next`. -5. The API leases one existing eligible `ProviderAssignment`; the manager enforces provider-local execution-provider/lane/native limits and persists a mode-0600 durable dispatch containing the lease. -6. A bounded provider runner atomically claims that dispatch and calls `AgentKernel.runAssignment` with the leased assignment, `AgentCapacityEnvelope`, and `DecisionExecutionInput`. The runner never polls a team for replacement work. -7. The AgentKernel validates mode/profile/envelope bounds, resolves the project-bundled agent handler, and executes it with optional `AgentContext.capacity` runtime context and optional `AgentContext.treeDx` repository-context adapter. -8. The provider runner records `AgentModeRun` telemetry through `POST /v1/provider/assignments/:assignmentId/mode-runs`. -9. The provider runner renews, completes, fails, or returns the assignment through the assignment lifecycle routes. -10. The API settles usage into durable mode-run, usage, reservation, and ledger bridge records where ids are supplied. - -The API does not require inbound network reachability to local or self-hosted providers. - -Provider-local coordination state is stored atomically under the provider data directory with mode-0600 files and a cross-process lock. It contains short-lived membership token state, availability-session id/sequence, polling claims, ready dispatches, running dispatches, and bounded secret-free lifecycle evidence. Global, connection, execution-provider, lane, reserved-credit, and reservation-native-unit limits are checked through that single state owner. Provider budget reserve buffers reduce the locally usable native allowance. Ready dispatches survive process restart and remain executable. On manager startup, unfinished running dispatches are observed through the provider assignment API and safely returned before their local claim is released; an unavailable control plane retains the claim for retry. A runner releases a claim only after lifecycle confirmation; an unconfirmed failure remains recoverable. Operator output omits dispatch envelopes, redacts lease tokens, and never exposes membership credentials or access tokens. - -The provider manager polls the assignment lease route; provider runners execute only manager-created dispatches through the AgentKernel and report assignment lifecycle transitions. The API may synthesize planning assignments from open planning-input requests and acting assignments from accepted capacity-plan work units before next-assignment leasing, but the provider runtime does not synthesize project work locally. Raw accepted execution inputs remain planning artifacts until the API aggregates and accepts a durable capacity plan. - -Human-machine providers keep this assignment-only protocol. Jira-like issue queues, deterministic workflows, and AI providers all run through the same availability-session, assignment lease, mode-run, complete/return/fail, and usage-settlement lifecycle. - -Provider task claim/update HTTP routes are not part of the provider runtime contract. Package-owned provider execution uses assignment APIs only. - -Lifecycle routes: - -- `POST /v1/provider/availability-sessions` -- `PUT /v1/provider/availability-sessions/:sessionId` -- `POST /v1/provider/availability-sessions/:sessionId/close` -- `POST /v1/provider/assignments/next` -- `POST /v1/provider/assignments/:assignmentId/renew` -- `POST /v1/provider/assignments/:assignmentId/return` -- `POST /v1/provider/assignments/:assignmentId/complete` -- `POST /v1/provider/assignments/:assignmentId/fail` -- `POST /v1/provider/assignments/:assignmentId/mode-runs` -- `POST /v1/provider/assignments/:assignmentId/settle` - -## Provider Versus Project Ownership - -Capacity providers supply execution capacity: - -- execution providers -- native budgets and quota observations -- local runner concurrency -- availability windows -- local model/tool surfaces -- provider-local constraints - -Projects supply work semantics: +Provider manifests use `serverProfile`, `controlPlaneUrl`, and `controlPlaneAudience`. The default local profile reads `TREESEED_SERVER_PROFILE_LOCAL_URL` and `TREESEED_SERVER_PROFILE_LOCAL_AUDIENCE`. -- project agents -- agent classes -- handlers -- prompts and configuration -- planning/acting permissions -- output contracts -- required execution capabilities +Secrets are referenced from the manifest and resolved only on the trusted provider host. They must never be committed, printed, included in receipts, or copied into an agent workspace. -Provider runners execute assigned project-bundled agents. They must not invent project work, approve decisions, mutate allocation policy, or widen assignment scope. +## Recovery and completion -## TreeDX Access +Provider-local state exists only to prevent double-spending and recover leases across process restarts. On restart the runtime re-reads authoritative assignment state. A leased or running assignment is returned before local state is released. Successful execution follows: -Provider assignments should include a project-scoped TreeDX proxy handle, not raw TreeDX service credentials. - -The runner calls the TreeSeed API using its short-lived membership access token and scoped proxy paths such as `/v1/dx/projects/:projectId/...`. Provider calls must include `x-treeseed-assignment-id` and `x-treeseed-treedx-proxy-handle-id`. The API authenticates the membership, verifies the active assignment lease, loads the durable handle record when present, checks issued/revoked/expired state, validates handle token material when configured, enforces project, repository, workspace, allowed operation, and allowed path constraints, resolves the TreeDX node, holds TreeDX node credentials, forwards only allowed operations, and records allowed or denied proxy audit evidence. - -Handlers should use `AgentContext.treeDx` for context build, repository file readback, workspace search, workspace file writes, and commits. The adapter is hydrated from the assignment proxy handle, applies handle-bound repository and workspace defaults, rejects out-of-scope path or operation requests before calling the API, and never exposes raw TreeDX node credentials. - -## Runtime Images - -Hosted deployments use package-owned role images: - -- provider manager from `treeseed/agent-manager` -- provider runner from `treeseed/agent-runner` - -Published images are multi-architecture Docker Hub images for `linux/amd64` and `linux/arm64`, with architecture-specific tags assembled into a manifest, following the TreeDX image release model. - -Local reconciliation builds one source-closure-addressed `agent-runtime` image and applies both local role tags to that exact image. Compose still runs separate provider manager and provider runner services with explicit role commands and isolated lifecycle state. A changed Agent or consumed SDK runtime closure invalidates both tags together; an unchanged closure is a no-op. Hosted publication retains the distinct manager and runner artifacts. - -The following versioned document is the separate runtime-image extension manifest, not the schema-v2 provider identity/connections manifest used by the provider manager: - -```yaml -schemaVersion: 1 -provider: - environment: local - dataDir: .treeseed/local-capacity-provider/data -runtime: - images: - tag: 1.2.3 -extensions: - runner: - enabled: true - baseImage: treeseed/agent-runner:1.2.3 - dockerfile: ./capacity-provider/runner.Dockerfile - context: . - image: example/team-agent-runner - tag: local -``` - -Manifests may select official or derived role images, but must not contain plaintext secrets. - -## Verification - -For provider runtime work, run: - -```bash -npm -w packages/agent run test:capacity-provider-runtime -npm -w packages/agent run capacity-provider:test-local -npm -w packages/agent run verify:local -``` - -Live assignment-protocol acceptance is exposed through SDK reconciliation: - -```bash -trsd reconcile test-live --provider local --mode acceptance --yes --json -trsd reconcile test-live --provider railway --environment staging --mode cleanup --yes --json -trsd reconcile test-live --provider railway --environment staging --mode acceptance --yes --json -trsd reconcile test-live --provider railway --environment staging --mode cleanup --yes --json -``` +1. start execution; +2. execute through the trusted executor; +3. report usage; +4. start closeout; +5. preflight completion; +6. submit the terminal completion receipt. -The capacity runtime proof observes a tagged assignment created through canonical admission, verifies its membership-scoped lease and mode-run evidence, and confirms TreeDX and settlement visibility. The observation proof requires `TREESEED_CAPACITY_ACCEPTANCE_API_URL`, `TREESEED_CAPACITY_ACCEPTANCE_ADMIN_TOKEN`, `TREESEED_CAPACITY_ACCEPTANCE_TEAM_ID`, `TREESEED_CAPACITY_ACCEPTANCE_PROJECT_ID`, `TREESEED_CAPACITY_ACCEPTANCE_PROVIDER_ID`, and `TREESEED_CAPACITY_ACCEPTANCE_AGENT_CLASS_ID`. The end-to-end local service guarantee provisions its own provider identity and membership credential and never accepts a static provider API key. +Errors produce typed failure or return receipts. Local cleanup never substitutes for an authoritative API terminal response. diff --git a/docs/src/content/objectives/core.md b/docs/src/content/objectives/core.md index bda86c40..cb6b374f 100644 --- a/docs/src/content/objectives/core.md +++ b/docs/src/content/objectives/core.md @@ -1,9 +1,9 @@ --- id: objective:agent-core title: TreeSeed Agent Core Objective -description: TreeSeed Agent should run Treeseed capacity providers, including provider API, provider manager, provider runner, worker runtime, AgentKernel execution, mode scheduling, provider-local capacity enforcement, Docker assets, and runtime tests. +description: TreeSeed Agent should run outbound-only capacity providers and trusted assignment executors beneath API-owned scheduling and authority. date: 2026-06-22 -summary: TreeSeed Agent exists to run Treeseed capacity providers, including provider API, provider manager, provider runner, worker runtime, AgentKernel execution, mode scheduling, provider-local capacity enforcement, Docker assets, and runtime tests while preserving its package boundary. +summary: TreeSeed Agent owns provider-local execution, recovery, evidence, and capacity enforcement without becoming a control plane. status: live timeHorizon: long-term motivation: Package-local workdays need a stable north star from the README so humans and agents can plan, execute, review, and report work without drifting across package ownership boundaries. @@ -12,10 +12,10 @@ relatedQuestions: [] relatedBooks: [] --- -TreeSeed Agent exists to run Treeseed capacity providers, including provider API, provider manager, provider runner, worker runtime, AgentKernel execution, mode scheduling, provider-local capacity enforcement, Docker assets, and runtime tests. +TreeSeed Agent exists to run outbound-only capacity providers and trusted assignment executors beneath API-owned scheduling, authorization, leases, and settlement. This core objective is the starting direction for the TreeSeed Agent Knowledge Hub. It should influence every package-local workday, research note, implementation proposal, generated artifact, approval request, and release-readiness summary. -Agent owns provider-local runtime execution and must remain assignment-only. It must not become the API control plane, hidden scheduler, web app, admin UI, package workflow owner, or TreeDX product semantics layer. +Agent owns provider-local runtime execution and must remain assignment-only. It must not become the API control plane, hidden scheduler, web app, admin UI, package workflow owner, or TreeDX product semantics layer. Without a configured trusted executor it advertises no executable capacity. Agents working in this project should keep outputs grounded in the package README, package-local source evidence, and the TreeSeed package ownership map. When a task would cross into another package's authority, the agent should describe the boundary and route the work to the correct project instead of mutating outside this hub. diff --git a/package-lock.json b/package-lock.json index 319fe23d..635755c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,16 @@ { "name": "@treeseed/agent", - "version": "0.13.0-rc.1", + "version": "0.13.0-rc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@treeseed/agent", - "version": "0.13.0-rc.1", + "version": "0.13.0-rc.2", "license": "Apache-2.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "@openai/codex-sdk": "^0.130.0", - "@treeseed/sdk": "0.13.0-rc.5", + "@treeseed/sdk": "0.13.0-rc.18", "esbuild": "0.28.0", - "hono": "^4.8.2", "typescript": "^5.9.3", "yaml": "^2.8.1" }, @@ -26,197 +23,6 @@ "node": ">=22" } }, - "node_modules/@alcalzone/ansi-tokenize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", - "integrity": "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@clack/core": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", - "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", - "license": "MIT", - "dependencies": { - "picocolors": "^1.0.0", - "sisteransi": "^1.0.5" - } - }, - "node_modules/@clack/prompts": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", - "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", - "license": "MIT", - "dependencies": { - "@clack/core": "0.5.0", - "picocolors": "^1.0.0", - "sisteransi": "^1.0.5" - } - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", - "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", - "license": "MIT OR Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", - "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", - "license": "MIT OR Apache-2.0", - "peerDependencies": { - "unenv": "2.0.0-rc.24", - "workerd": ">1.20260305.0 <2.0.0-0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, - "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260426.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260426.1.tgz", - "integrity": "sha512-Ch7DqsmYzSQRTY87pZpsGsFVz9VVBnLPnCBOHxKt1HH25a7oMu1w1PbPWqVmE0VerCLsj/TScX7Ob3v6E14TZw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260426.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260426.1.tgz", - "integrity": "sha512-0m0U8vaPRH25SpKjbSyRql6gmPe4rCsETRV2WW0qBnuMdKNr5Vh5/Uez80xVrfiCCRMTULGeg63Nqg2vg6CDOA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260426.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260426.1.tgz", - "integrity": "sha512-C8LlC8uSYzg49y51n++75esxZmMp+Uz1OKHHA/4lkv6rjOTbcHQJuEwSLppjybVIXpv7A8MBhbu9iyCTvyv1mw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260426.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260426.1.tgz", - "integrity": "sha512-ESVp/OIFMAqjQsa8BOP2BQQz5Vpfv6ncN6lNnIuNeOgsISQBdYk+LA60bwQHMud9tvmnSYtONp1zkZ8OQz+x6w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260426.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260426.1.tgz", - "integrity": "sha512-d3Xj/IjINRgNVwH+eKhpUn4xkkcEewbWXbOvBlapiirKWh5zl9m0Epi3qOqmjyRYK6MICqIGXg4qZBEt0lxudw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", @@ -633,10184 +439,1348 @@ "node": ">=18" } }, - "node_modules/@github/copilot": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.39.tgz", - "integrity": "sha512-AY0VPYf6QQm88wUcOav2B36iedWKBUaMegKRxxY2uIHESiU6HueEuQR/n7D3U2UdD0zLox3jFRjYbZAsr2CgkQ==", - "license": "SEE LICENSE IN LICENSE.md", - "bin": { - "copilot": "npm-loader.js" - }, - "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.39", - "@github/copilot-darwin-x64": "1.0.39", - "@github/copilot-linux-arm64": "1.0.39", - "@github/copilot-linux-x64": "1.0.39", - "@github/copilot-win32-arm64": "1.0.39", - "@github/copilot-win32-x64": "1.0.39" + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.39.tgz", - "integrity": "sha512-E8WfNL43NMzMTDDpCiYikaEmYCMAr6mz8LHrJtkaFuVXVkBr/q2NI3hAtwHFy8M11Fac/MeIe3/VEymWwwh3kw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", "cpu": [ "arm64" ], - "license": "SEE LICENSE IN LICENSE.md", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], - "bin": { - "copilot-darwin-arm64": "copilot" + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.39.tgz", - "integrity": "sha512-0zbC4lDVX7l8Wvq+JSCMjO0xTN69nWLejTBCl3Ev5bP6P+/7wPURcUvZKoHEaXxOULQ3AGj0DwZNAsvvQkA/6Q==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", "cpu": [ - "x64" + "arm64" ], - "license": "SEE LICENSE IN LICENSE.md", + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], - "bin": { - "copilot-darwin-x64": "copilot" - } - }, - "node_modules/@github/copilot-language-server": { - "version": "1.480.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.480.0.tgz", - "integrity": "sha512-VrhbHluNTygFpPW8G8g0wl+hWT9iGRbdnAzmmIjPo0ph0SQGs5mth0g6jE/b8NDqw+SLsVabFDBhrTvJu+wQvA==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "^3.17.5" - }, - "bin": { - "copilot-language-server": "dist/language-server.js" - }, - "optionalDependencies": { - "@github/copilot-language-server-darwin-arm64": "1.480.0", - "@github/copilot-language-server-darwin-x64": "1.480.0", - "@github/copilot-language-server-linux-arm64": "1.480.0", - "@github/copilot-language-server-linux-x64": "1.480.0", - "@github/copilot-language-server-win32-arm64": "1.480.0", - "@github/copilot-language-server-win32-x64": "1.480.0" + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@github/copilot-language-server-darwin-arm64": { - "version": "1.480.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.480.0.tgz", - "integrity": "sha512-MPgNMfigh0phF843YiTvt6Fr8Dla27RFPxjyEUkd9KSBmz/LMcvhKMGESOIfn/ExebNFXEgbQ+O/nZ8kCjQCFQ==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", "cpu": [ - "arm64" + "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@github/copilot-language-server-darwin-x64": { - "version": "1.480.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.480.0.tgz", - "integrity": "sha512-icV2UrpB6DwONHTFdmoGTWUx4kTrSG6CHb++4lk6jiheMj2rbG1T/Lbb4KkvLtlBBn1p8B+9J0axW0hD78aA8g==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@github/copilot-language-server-linux-arm64": { - "version": "1.480.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.480.0.tgz", - "integrity": "sha512-PpLtqeOQvvfSkpfn61+1fowEieihzuevFVunTmYMqihRHgR4iAxoKVpebjxjVG1KWxhDjqijaRC/2TORiKfaHg==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", "cpu": [ - "arm64" + "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@github/copilot-language-server-linux-x64": { - "version": "1.480.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.480.0.tgz", - "integrity": "sha512-9LJr5RPHWrE7isMQWPcu57QaKOUzycKeQN4vCMbryAIKj8Vv/a1ujKopfNnTEwZCnvIO7WhWoQUzjdbAB8Gnig==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", "cpu": [ - "x64" + "arm64" + ], + "dev": true, + "libc": [ + "glibc" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@github/copilot-language-server-win32-arm64": { - "version": "1.480.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-arm64/-/copilot-language-server-win32-arm64-1.480.0.tgz", - "integrity": "sha512-Z+h/8vDG/bHV0tETMZxOUO84cNrcU0IalPS1JLIgWsAXKgI6g2GWd3ljClVuT0HshgAA1lpve+AnHeIxcm+3yw==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", "cpu": [ "arm64" ], + "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@github/copilot-language-server-win32-x64": { - "version": "1.480.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.480.0.tgz", - "integrity": "sha512-zIyzU9ozZFMusza+rZrBYExuO8bc4+OGcR5wvisTdo/YtfzzjS+82UeEOJE18KUsRdJ1poHJplEez+3h09+uWg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", "cpu": [ - "x64" + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" ], "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.39.tgz", - "integrity": "sha512-x88FuByweJlHlAmUZXjq4JlmtqgoM57Fe7nXzQkGr2Y5wnc2EDydBzFYEOlYDSWozQreimaJIm0KEMAA5T8/Fg==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", "cpu": [ - "arm64" + "s390x" + ], + "dev": true, + "libc": [ + "glibc" ], - "license": "SEE LICENSE IN LICENSE.md", + "license": "MIT", "optional": true, "os": [ "linux" ], - "bin": { - "copilot-linux-arm64": "copilot" + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@github/copilot-linux-x64": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.39.tgz", - "integrity": "sha512-ssahg8r7a0VCsHVXPRmFFXx70xNAxaTM2SZfG7qPRfFB2OM8gHrW26F2oikTklDF6D+A2MfSAMpzJLBUZbPnhw==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", "cpu": [ "x64" ], - "license": "SEE LICENSE IN LICENSE.md", + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" ], - "bin": { - "copilot-linux-x64": "copilot" - } - }, - "node_modules/@github/copilot-sdk": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.3.0.tgz", - "integrity": "sha512-SUo35k56pzzgYgwmDPHcu7kZxPrzXbH66IWXaEf6pmb94DlA709F82HrrDeja087TL4djJ9OuvRFWWOKCosAsg==", - "license": "MIT", - "dependencies": { - "@github/copilot": "^1.0.36-0", - "vscode-jsonrpc": "^8.2.1", - "zod": "^4.3.6" - }, "engines": { - "node": ">=20.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.39.tgz", - "integrity": "sha512-hhBWGZQIywbp6MBxlqMX2GSmHqtUAOGwpo9b0igscecL4i0kz89QNasC+mKiN+zFEHP6I8gggOu87XPI17Io8Q==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", "cpu": [ - "arm64" + "x64" + ], + "dev": true, + "libc": [ + "musl" ], - "license": "SEE LICENSE IN LICENSE.md", + "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], - "bin": { - "copilot-win32-arm64": "copilot.exe" + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@github/copilot-win32-x64": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.39.tgz", - "integrity": "sha512-0ehlMtBiwKjmfEY3hVZggdn7qrmPMC8ueBQv/b+6UY3SMRS/M/1Y7xkOCwG84NvJsktdSsk3SlQnE2LbkTVpSA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", "cpu": [ - "x64" + "arm64" ], - "license": "SEE LICENSE IN LICENSE.md", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "win32" + "openharmony" ], - "bin": { - "copilot-win32-x64": "copilot.exe" - } - }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "license": "MIT", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@hono/node-server": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz", - "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", "cpu": [ "arm64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", "cpu": [ "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@treeseed/sdk": { + "version": "0.13.0-rc.18", + "resolved": "https://registry.npmjs.org/@treeseed/sdk/-/sdk-0.13.0-rc.18.tgz", + "integrity": "sha512-+v3q3wumXJnI7sOJUJmWRalskUBwEum2TTMX/52tvEHR7gN+QEMAqT2/BgT+5m/51XoFiy3vczThOxpzjyN/4g==", + "license": "Apache-2.0", + "dependencies": { + "esbuild": "^0.28.0", + "typescript": "^5.9.3", + "yaml": "^2.8.1", + "zod": "^3.25.76" + }, + "engines": { + "node": ">=22" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@treeseed/sdk/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mediabunny/aac-encoder": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.45.0.tgz", - "integrity": "sha512-vLQw8cY7Me6pvTTMkMhOiH9UCuINzfTOETCeDxbGNeNfDqc/7QlxloUH1Ylp/Zz2ek0O8kc6YdygV2vWAPakrA==", - "license": "MPL-2.0", - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" - }, - "peerDependencies": { - "mediabunny": "^1.0.0" - } - }, - "node_modules/@mediabunny/flac-encoder": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.45.0.tgz", - "integrity": "sha512-LfKbAMZVkxRS7PpEIVnWOY/l0KcHv+rjO7pYY3O0TPCZvbHWfrnQjn8JPacPIfuq6Yv7r4f8lhcl7yHSynoRkQ==", - "license": "MPL-2.0", - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" - }, - "peerDependencies": { - "mediabunny": "^1.0.0" - } - }, - "node_modules/@mediabunny/mp3-encoder": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.45.0.tgz", - "integrity": "sha512-Bobi6AaQYEc7TWmPJ8Q0/hcUtBN7pLUC2qjoC7oZR4FcGqGztby6k7A1SWlmswoMOEIhYsOrgDaemrSDAC0QVQ==", - "license": "MPL-2.0", - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" - }, - "peerDependencies": { - "mediabunny": "^1.0.0" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@module-federation/error-codes": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", - "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", - "license": "MIT" - }, - "node_modules/@module-federation/runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", - "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", - "license": "MIT", - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/runtime-core": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@module-federation/runtime-core": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", - "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", - "license": "MIT", - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@module-federation/runtime-tools": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", - "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", - "license": "MIT", - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/webpack-bundler-runtime": "0.22.0" - } - }, - "node_modules/@module-federation/sdk": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", - "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", - "license": "MIT" - }, - "node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", - "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", - "license": "MIT", - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@octokit/app": { - "version": "16.1.4", - "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.1.4.tgz", - "integrity": "sha512-g70WONQyGoBgqIJtv4O0MUEfOF1iJpTfFt3qAW7HaiwiJ6k607xg/gBezCa4tuq6BpO6qXn7qvq8v9tjWtgGQg==", - "license": "MIT", - "dependencies": { - "@octokit/auth-app": "^8.3.0", - "@octokit/auth-unauthenticated": "^7.0.4", - "@octokit/core": "^7.0.7", - "@octokit/oauth-app": "^8.0.4", - "@octokit/plugin-paginate-rest": "^15.0.0", - "@octokit/types": "^17.0.0", - "@octokit/webhooks": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/app/node_modules/@octokit/plugin-paginate-rest": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-15.0.0.tgz", - "integrity": "sha512-lw9A9YL5s4VPj+VEx8uMxaxQm2YTgrPDkrLEuZuu18R8TK3oPcXF4K6t52nx6xn1RcogZC9i188sAr/4XYJaQQ==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^17.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/auth-app": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.3.0.tgz", - "integrity": "sha512-/UaKmJCsOc5XBZwhnFiGNdLH/FkDF8lYtBn1QlKxtX7IpgaRB/XOXjixFtERAknyUZHxp3oDuoiG0En4VptJSg==", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.4", - "@octokit/auth-oauth-user": "^6.0.3", - "@octokit/request": "^10.0.13", - "@octokit/request-error": "^7.1.1", - "@octokit/types": "^17.0.0", - "toad-cache": "^3.7.0", - "universal-github-app-jwt": "^2.2.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/auth-oauth-app": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.4.tgz", - "integrity": "sha512-Pe3du5LrC6dlv10b0RbarBlJtIT1Urq8BFoQklK8WmBw8YKnGgrANn7cQmHiA1v8AVihgi7YutV8MncP57I4og==", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.4", - "@octokit/auth-oauth-user": "^6.0.3", - "@octokit/request": "^10.0.13", - "@octokit/types": "^17.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/auth-oauth-device": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.4.tgz", - "integrity": "sha512-M/+34rmkxMvUnnTo/uqNeVRCXJoJ+5N34eNTbL1KMtIyJpedCTsu/iFL1cWdL+w5hQMlYt6xzXoyux2gn0OVnw==", - "license": "MIT", - "dependencies": { - "@octokit/oauth-methods": "^6.0.3", - "@octokit/request": "^10.0.13", - "@octokit/types": "^17.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/auth-oauth-user": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.3.tgz", - "integrity": "sha512-t4OKUhrI5britmpRiSzPAz1TJPyUN/Hw1HEE3Hz/uElxcmnf/YCRSKtFRNc5gy4yJFTlXgk34H1lvxiotQJQvQ==", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.4", - "@octokit/oauth-methods": "^6.0.3", - "@octokit/request": "^10.0.13", - "@octokit/types": "^17.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-unauthenticated": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.4.tgz", - "integrity": "sha512-j4zgVdP8C8C53PNsj4LLI+WFtoykaQARwwrIBILTdxwZ2Ljaa9YUxb1yQ7/seGVnmWPdC3OBefX3LLbh28gKtw==", - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.1.1", - "@octokit/types": "^17.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/core": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.7.tgz", - "integrity": "sha512-DcB0M3KFgr9ECI328lhBMVsyFT2DnmNucSBTqEN3exyNKUzkkpUSCHmTRcunF41Eou2TIQKW4seewri8ON9bSA==", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.4", - "@octokit/request": "^10.0.13", - "@octokit/request-error": "^7.1.1", - "@octokit/types": "^17.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/endpoint": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.4.tgz", - "integrity": "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^17.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/graphql": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.4.tgz", - "integrity": "sha512-5s15CCiY8XXQ+FG+b1YQcl6Z2FA++nwAz/tg2VUrTmnMncP+2nnGUEYANImdnxsA2Fnq+Mbl7hDjUTw7cFAwcg==", - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.13", - "@octokit/types": "^17.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/oauth-app": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.4.tgz", - "integrity": "sha512-Ji5JpRwAJcbOJkO4ij0tW6+GrQ8efpICnAca3o5xI8/HUNjqJu3hhG4cCZdXAHMKaOaQTFX0Yl+cpu61Rsx94A==", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.4", - "@octokit/auth-oauth-user": "^6.0.3", - "@octokit/auth-unauthenticated": "^7.0.4", - "@octokit/core": "^7.0.7", - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/oauth-methods": "^6.0.3", - "@types/aws-lambda": "^8.10.83", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", - "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-methods": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.3.tgz", - "integrity": "sha512-t7MBs34Ja16BBZlOSAFslFh4wBMuofD83wVLG9zIuuEGMv/U12XAJ8ESOifgEcC1ptjhNA3sorLA9BCtVfgPuw==", - "license": "MIT", - "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.13", - "@octokit/request-error": "^7.1.1", - "@octokit/types": "^17.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", - "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", - "license": "MIT" - }, - "node_modules/@octokit/openapi-webhooks-types": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-12.1.0.tgz", - "integrity": "sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-graphql": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz", - "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", - "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", - "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-retry": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.1.tgz", - "integrity": "sha512-VCVvZ/R1+u3WuiBWpNavZ0mY4aaJNAsENrpBP9aLSR2QyOpQgd7DhM5j4AW7z4MQpnJYgwBPf0XqPQoNBRdQwg==", - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.1.1", - "@octokit/types": "^17.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=7" - } - }, - "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/plugin-throttling": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.5.tgz", - "integrity": "sha512-LIdrkrUv+DWbKeg/49rGuFJ3SU0d3hUS+B4MhNZLepBoNUFXms8Ic9edJjrlx+zycqJHjrMRudVpVb/bAXM2Lw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^17.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": "^7.0.0" - } - }, - "node_modules/@octokit/plugin-throttling/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-throttling/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/request": { - "version": "10.0.13", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.13.tgz", - "integrity": "sha512-v2269YxL9Yf+x3d+gRI63FP0vFQEiWgLyBzxe/Y+0yFDg2B/Tzf5dhh9VNfccVAQnfcfwQWyk/y6Bn7rUXXs7A==", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^11.0.3", - "@octokit/request-error": "^7.1.1", - "@octokit/types": "^17.0.0", - "content-type": "^2.0.0", - "json-with-bigint": "^3.5.3", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.1.tgz", - "integrity": "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^17.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", - "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", - "license": "MIT" - }, - "node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", - "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^28.0.0" - } - }, - "node_modules/@octokit/request/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@octokit/types": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", - "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^27.0.0" - } - }, - "node_modules/@octokit/webhooks": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.2.0.tgz", - "integrity": "sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-webhooks-types": "12.1.0", - "@octokit/request-error": "^7.0.0", - "@octokit/webhooks-methods": "^6.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/webhooks-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz", - "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@openai/codex": { - "version": "0.130.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.130.0.tgz", - "integrity": "sha512-WGDj+RZ3TXWC/7MlwprgLWOqzpwatPIINPhP3IRzHA0ni+o3QZ4i4xrS2uWwGmHUJ395J5JHwoZAAZYyfJyz6w==", - "license": "Apache-2.0", - "bin": { - "codex": "bin/codex.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.130.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.130.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.130.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.130.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.130.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.130.0-win32-x64" - } - }, - "node_modules/@openai/codex-darwin-arm64": { - "name": "@openai/codex", - "version": "0.130.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.130.0-darwin-arm64.tgz", - "integrity": "sha512-R9pkGC7kwC8yQ8el5hvBlmugQlcsG/pHMEFgZluu03X9fD2TezGxdq3KqRDRCZuMYl07ILamVEoqknuJ0cq7MA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-darwin-x64": { - "name": "@openai/codex", - "version": "0.130.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.130.0-darwin-x64.tgz", - "integrity": "sha512-gJ+7J8djevgtdra+NgDAiQQPW+O3KTsgGfE3E5dpDfww3zS5OCeV0V2dhxqnJdlOjOSDw99o0P2LqBv19mhpRw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-linux-arm64": { - "name": "@openai/codex", - "version": "0.130.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.130.0-linux-arm64.tgz", - "integrity": "sha512-tFtH0V9/hEI3d9y7zP92BXI9FM4Z3+STNQaOR52Czv18TRtCFUp7CbIUYaToopuq6UBfnE1VKr8RLhwT5FcbmA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-linux-x64": { - "name": "@openai/codex", - "version": "0.130.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.130.0-linux-x64.tgz", - "integrity": "sha512-3VcNlez99xdnEf+kB1IOpWv9fICYV9PiGj4sLCO4TCcShLnyxe+YBGa3poknkvXLnMG0qiN9SMnYS2FGrMxQcA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-sdk": { - "version": "0.130.0", - "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.130.0.tgz", - "integrity": "sha512-ICKaZ5zrIDg71AiQcsUToVoe5Icmrc3LwSM5+2z7Cf8F1x6nOaY7/ucpFlr4aH8oDe7t3dangc+MsWZTkdvDFw==", - "license": "Apache-2.0", - "dependencies": { - "@openai/codex": "0.130.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@openai/codex-win32-arm64": { - "name": "@openai/codex", - "version": "0.130.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.130.0-win32-arm64.tgz", - "integrity": "sha512-vdpmiNp57L/arZabltLXn8TyEtNa7W1meOEkr+3R6W/8ZyBt++wuqz1Orv134OT2grrcFJsIVCAIPiqUxCvBkA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-win32-x64": { - "name": "@openai/codex", - "version": "0.130.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.130.0-win32-x64.tgz", - "integrity": "sha512-FzMznm7fr5/nbjZgOujZ9Y9AbdGm7ji1FOoWiY3U+srqauvZaTgn6o6aCheSL7kuymu7nTLOO/cAyWV6NuesqQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.143.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", - "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@poppinss/colors": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", - "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", - "license": "MIT", - "dependencies": { - "kleur": "^4.1.5" - } - }, - "node_modules/@poppinss/dumper": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", - "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@sindresorhus/is": "^7.0.2", - "supports-color": "^10.0.0" - } - }, - "node_modules/@poppinss/dumper/node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@poppinss/exception": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", - "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", - "license": "MIT" - }, - "node_modules/@remotion/bundler": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.477.tgz", - "integrity": "sha512-nbTCxUtiNWg06zbIJ9ia0v8UrK/o486sE/KKvMqs6u+FCdQDO86cOEmqSoGsImsDe3TwHWTUws9V7kpcXA+E8g==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@remotion/media-parser": "4.0.477", - "@remotion/studio": "4.0.477", - "@remotion/studio-shared": "4.0.477", - "@remotion/timeline-utils": "4.0.477", - "@rspack/core": "1.7.11", - "@rspack/plugin-react-refresh": "1.6.1", - "css-loader": "7.1.4", - "esbuild": "0.28.1", - "react-refresh": "0.18.0", - "remotion": "4.0.477", - "style-loader": "4.0.0", - "webpack": "5.105.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@remotion/bundler/node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, - "node_modules/@remotion/canvas-capture": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/canvas-capture/-/canvas-capture-4.0.477.tgz", - "integrity": "sha512-/4M9rP/ltAggYXqhAMHj6wxBX7zt1CdekK8JEY7bbIi2uFHltD2qRTfT8B7DZaOjG+b6cWZ/VROusfiuvpMCNg==", - "license": "Remotion License", - "dependencies": { - "mediabunny": "1.45.0", - "remotion": "4.0.477" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/compositor-darwin-arm64": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.477.tgz", - "integrity": "sha512-d6PfHTTnlkqqY+C+40m3hgDzkevG/wONd2ZsWJEShx1QRN3UHgTuhW1xKZw/E+DpTGXdvCVmeM2QCsBrpSfnKQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@remotion/compositor-darwin-x64": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.477.tgz", - "integrity": "sha512-tnuSFZGV9Vg/9N8YjvATPfOJKD31GM+B9XFuVLdcHc7+hyikdX46xa8pKKKUVLOkeJegs8A22Wi+V1/AEnWqEQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@remotion/compositor-linux-arm64-gnu": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.477.tgz", - "integrity": "sha512-3xhtIdqufAiO49v8tbbkPDyDkd7uRRB8vx+vSPi9+THUI/h0uqi+ylENHtsWj7veEDCiTB3Y4ZjsmotEVMHQJA==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@remotion/compositor-linux-arm64-musl": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.477.tgz", - "integrity": "sha512-qenbQpmSm9XQn2uaW3yt+P5YBgQuCb+jitTfQunhR1l0tk6oC2kKPn14qKu9jAs7vZPH11bV8V76DcYUhqdbAQ==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@remotion/compositor-linux-x64-gnu": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.477.tgz", - "integrity": "sha512-P+5MRPkaRskoUgseJxEko53KvW/Rzu0Ow/htyzJjC9XpigtjYTJPKRKOjw4/ihPYJoVDH+e9s/jyOF06odzLVA==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@remotion/compositor-linux-x64-musl": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.477.tgz", - "integrity": "sha512-uas9/X85QfI6p5vTOCcjqmCBizIK6QeZlIXVrKc1fcmSzhk3udjbzx9XDDo71m6Bz8IfWeqV1ZkX/5pLJYufFg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@remotion/compositor-win32-x64-msvc": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.477.tgz", - "integrity": "sha512-7oldAftiUrFDI1U5z0zN87xfhpuJuIrLrNP16GFgj0JZN4AzRQT/iGYoH5+H2/clT910mlq4YtrfAv1RsHe4Cg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@remotion/licensing": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.477.tgz", - "integrity": "sha512-Ayor3/+CyXkyYUvzxEFcTVWe46+ld1i9jyrmZY9OqO77gCQ3khOAivjNsI/4foFgnfb7aRNvcCBYr1D9iZLn/w==", - "license": "MIT" - }, - "node_modules/@remotion/media-parser": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.477.tgz", - "integrity": "sha512-E6qAQvY2s5VzvGNi0FOiDjcEm2RWRsRO+5012ZQneAUqp54JVP/6M3oXM5i34wo9PLjMSqfXgmxLPRdF08YokQ==", - "license": "Remotion License https://remotion.dev/license" - }, - "node_modules/@remotion/media-utils": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.477.tgz", - "integrity": "sha512-ItEniXSb0OUnZJSjcOxoD6HvHoQ1wbn0FQNz/BkuVo7AQLvVeY1GGf9XWIa0mvqRRd790jYhW9QOqToeJryeeQ==", - "license": "MIT", - "dependencies": { - "mediabunny": "1.45.0", - "remotion": "4.0.477" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/player": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.477.tgz", - "integrity": "sha512-x0aWSsbXQIlW2V5R6hVksHsyw4CBWN0ctbc0b8Aq5kbAg901vZmDXMekNXyMicqPyzwPRfTeDfpPyGq5RwvOSA==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "remotion": "4.0.477" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/renderer": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.477.tgz", - "integrity": "sha512-OKHhmjLZwvXAOKvby7WCpyHGFUGUFMofKjJMGobx/yeyG0k/So/FSFBJQBBpP8XSdHY1RmFqKpMiKIO5SACB5A==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@remotion/licensing": "4.0.477", - "@remotion/streaming": "4.0.477", - "execa": "5.1.1", - "remotion": "4.0.477", - "source-map": "0.8.0-beta.0", - "ws": "8.20.1" - }, - "optionalDependencies": { - "@remotion/compositor-darwin-arm64": "4.0.477", - "@remotion/compositor-darwin-x64": "4.0.477", - "@remotion/compositor-linux-arm64-gnu": "4.0.477", - "@remotion/compositor-linux-arm64-musl": "4.0.477", - "@remotion/compositor-linux-x64-gnu": "4.0.477", - "@remotion/compositor-linux-x64-musl": "4.0.477", - "@remotion/compositor-win32-x64-msvc": "4.0.477" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/streaming": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.477.tgz", - "integrity": "sha512-GXSR5BaC2UaaPHwAEBbuwXXDedrUfa3KQ1GpwDBKfr/F2rm0QxOPgPoVzB14lt7CvaQsuig22NZD6h6BTDAH5A==", - "license": "MIT" - }, - "node_modules/@remotion/studio": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.477.tgz", - "integrity": "sha512-/Z+yYf1g6dQAQbGqthulE+Ra9PBNqAcgRYb3F6mu3iFf9zfZimP3BIZ03eYrXDDC1SeyVt9LaZu4QcXZv/VkOg==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.31", - "@remotion/canvas-capture": "4.0.477", - "@remotion/media-utils": "4.0.477", - "@remotion/player": "4.0.477", - "@remotion/renderer": "4.0.477", - "@remotion/studio-shared": "4.0.477", - "@remotion/timeline-utils": "4.0.477", - "@remotion/web-renderer": "4.0.477", - "@remotion/zod-types": "4.0.477", - "mediabunny": "1.45.0", - "memfs": "3.4.3", - "open": "8.4.2", - "remotion": "4.0.477", - "semver": "7.5.3", - "zod": "4.3.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/studio-shared": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.477.tgz", - "integrity": "sha512-uZqv9tTBddFbMgAuLr0huCu7f8A8LIlq1r+6Ap5tnRlbj1olmNVGWpA2jkChY8jJZADMIED8klhS4amV5nKiZQ==", - "license": "MIT", - "dependencies": { - "remotion": "4.0.477" - } - }, - "node_modules/@remotion/studio/node_modules/@remotion/zod-types": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.477.tgz", - "integrity": "sha512-3gglLjiN6AJo1ayqbxZCBhpLJIieD3XGUnBgJ7b1Ew+C881wjOSH8O5vt+pvrO1E0RizjqbbBkbV99FwLFrC/A==", - "license": "MIT", - "dependencies": { - "remotion": "4.0.477" - }, - "peerDependencies": { - "zod": "4.3.6" - } - }, - "node_modules/@remotion/studio/node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@remotion/timeline-utils": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.477.tgz", - "integrity": "sha512-OsOjNPCNwp08z0KG6OlcQD0UCsAuyIRD4xG+BIXvKaYvsyUiX0+KmsGD7ULANcd/uj6oj3xldNp0yqOi0v6IYA==", - "license": "MIT", - "dependencies": { - "mediabunny": "1.45.0" - } - }, - "node_modules/@remotion/web-renderer": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.477.tgz", - "integrity": "sha512-w8xkoDNg14DbfQXGvx5XfgaPu38dHzX1BFG2lUgMKoB7RfVoXMbU/VHEQ3Xm+vYGxW2Pa3oRgCWqUVprikR4+g==", - "license": "UNLICENSED", - "dependencies": { - "@mediabunny/aac-encoder": "1.45.0", - "@mediabunny/flac-encoder": "1.45.0", - "@mediabunny/mp3-encoder": "1.45.0", - "@remotion/licensing": "4.0.477", - "mediabunny": "1.45.0", - "remotion": "4.0.477" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@repomix/strip-comments": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@repomix/strip-comments/-/strip-comments-2.4.2.tgz", - "integrity": "sha512-7a18ODb043eszMBr6mpVWz802xIRMzdmptarVxTtnMIW7ZQzba/v8jLp3kcHUHb76uRkyJRPpGSwdm7+8GmsEA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@repomix/tree-sitter-wasms": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@repomix/tree-sitter-wasms/-/tree-sitter-wasms-0.1.17.tgz", - "integrity": "sha512-tc3HnFqdMF1pXhIMzG3aTaBDpIiHK2tPfn3fwqA6P3WTbHa+1EuuTubbKshvmN7xCHP5Ojz0/VW4R+XvR88KOw==", - "license": "Unlicense" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", - "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", - "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", - "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", - "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", - "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", - "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", - "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", - "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", - "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", - "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", - "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", - "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rspack/binding": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz", - "integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==", - "license": "MIT", - "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.7.11", - "@rspack/binding-darwin-x64": "1.7.11", - "@rspack/binding-linux-arm64-gnu": "1.7.11", - "@rspack/binding-linux-arm64-musl": "1.7.11", - "@rspack/binding-linux-x64-gnu": "1.7.11", - "@rspack/binding-linux-x64-musl": "1.7.11", - "@rspack/binding-wasm32-wasi": "1.7.11", - "@rspack/binding-win32-arm64-msvc": "1.7.11", - "@rspack/binding-win32-ia32-msvc": "1.7.11", - "@rspack/binding-win32-x64-msvc": "1.7.11" - } - }, - "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.11.tgz", - "integrity": "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rspack/binding-darwin-x64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.11.tgz", - "integrity": "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.11.tgz", - "integrity": "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.11.tgz", - "integrity": "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.11.tgz", - "integrity": "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.11.tgz", - "integrity": "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-wasm32-wasi": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.11.tgz", - "integrity": "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "1.0.7" - } - }, - "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.11.tgz", - "integrity": "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.11.tgz", - "integrity": "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.11.tgz", - "integrity": "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/core": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.11.tgz", - "integrity": "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==", - "license": "MIT", - "dependencies": { - "@module-federation/runtime-tools": "0.22.0", - "@rspack/binding": "1.7.11", - "@rspack/lite-tapable": "1.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.1" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@rspack/lite-tapable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", - "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", - "license": "MIT" - }, - "node_modules/@rspack/plugin-react-refresh": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.1.tgz", - "integrity": "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==", - "license": "MIT", - "dependencies": { - "error-stack-parser": "^2.1.4", - "html-entities": "^2.6.0" - }, - "peerDependencies": { - "react-refresh": ">=0.10.0 <1.0.0", - "webpack-hot-middleware": "2.x" - }, - "peerDependenciesMeta": { - "webpack-hot-middleware": { - "optional": true - } - } - }, - "node_modules/@secretlint/core": { - "version": "13.0.4", - "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-13.0.4.tgz", - "integrity": "sha512-Wv49KcI5XX6xjLR1wxyjORA15PtMb5ar/M27ShimVudaSi6iAM04QCA5Ozx+uEahfHNefUUKbjKGpy/9pxuW7g==", - "license": "MIT", - "dependencies": { - "@secretlint/profiler": "13.0.4", - "@secretlint/types": "13.0.4", - "debug": "^4.4.3", - "structured-source": "^4.0.0" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@secretlint/profiler": { - "version": "13.0.4", - "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-13.0.4.tgz", - "integrity": "sha512-T2hSyZmJrQbGAe+Vl9AGNlMnoB0MP6m2BLh7EH80QcesvNM2t0pCzdiBvQ/yCe76w6/gZNpHlrSVayUeT43qVw==", - "license": "MIT" - }, - "node_modules/@secretlint/secretlint-rule-preset-recommend": { - "version": "13.0.4", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-13.0.4.tgz", - "integrity": "sha512-Nbcr7tvyKuRF4BKh7RQSCEOaif4gFPH/qjK2ajcoUDGL7HAY/C6PzcsmVR1Y0qQCRqrBuMuXn1onXE+wxNNNsw==", - "license": "MIT", - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@secretlint/types": { - "version": "13.0.4", - "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-13.0.4.tgz", - "integrity": "sha512-on/DivRDZEFzRD2pZJO0wkIL+AvEY+KOoZLPCWcz4pjZnJ4NzMuHQjvTJc9KY0yht+ugcYg9AmtOUzpEk31IWA==", - "license": "MIT", - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@speed-highlight/core": { - "version": "1.2.23", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.23.tgz", - "integrity": "sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==", - "license": "CC0-1.0" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@treeseed/sdk": { - "version": "0.13.0-rc.5", - "resolved": "https://registry.npmjs.org/@treeseed/sdk/-/sdk-0.13.0-rc.5.tgz", - "integrity": "sha512-X9BhPBdu3LwUahtSGFfjgVYurBVSREDyLz4n5DrM/L4xK4lmCCoXpA2C05MJUdUQt7u3NpJGnuF8egSqTBT9uw==", - "license": "Apache-2.0", - "dependencies": { - "@github/copilot": "1.0.39", - "@github/copilot-language-server": "1.480.0", - "@github/copilot-sdk": "0.3.0", - "@remotion/bundler": "4.0.477", - "@remotion/renderer": "4.0.477", - "drizzle-orm": "^0.45.2", - "esbuild": "^0.28.0", - "extract-zip": "2.0.1", - "github-slugger": "^2.0.0", - "hono": "^4.8.2", - "ink": "^7.0.0", - "libsodium-wrappers": "0.7.15", - "libsodium-wrappers-sumo": "0.7.15", - "mdast-util-to-string": "^4.0.0", - "minisearch": "^7.2.0", - "octokit": "^5.0.3", - "pg": "^8.21.0", - "playwright": "^1.60.0", - "railway": "3.4.1", - "react": "^19.2.5", - "remark-mdx": "^3.1.1", - "remark-parse": "^11.0.0", - "remotion": "4.0.477", - "repomix": "^1.13.1", - "semver": "^7.7.4", - "tar": "7.5.13", - "typescript": "^5.9.3", - "unified": "^11.0.5", - "wrangler": "4.86.0", - "yaml": "^2.8.1", - "zod": "^3.25.76" - }, - "bin": { - "treeseed-content-publish": "dist/scripts/content/publish-content.js", - "treeseed-sdk-verify": "dist/scripts/verification/verify-driver.js" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@treeseed/sdk/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@treeseed/sdk/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/aws-lambda": { - "version": "8.10.162", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.162.tgz", - "integrity": "sha512-Fn658grtLOci1oxi1391vvDWJRKNGWRSqfxRkmN/Iy3c0tQH1USMKEXcPYHLvope+ZgTFocx9FRQJx1muBL6qw==", - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/dom-mediacapture-transform": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.12.tgz", - "integrity": "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==", - "license": "MIT", - "dependencies": { - "@types/dom-webcodecs": "*" - } - }, - "node_modules/@types/dom-webcodecs": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", - "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", - "license": "MIT" - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", - "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/parse-path": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", - "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/auto-bind": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", - "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", - "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", - "license": "Apache-2.0" - }, - "node_modules/binary-extensions": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-3.1.0.tgz", - "integrity": "sha512-Jvvd9hy1w+xUad8+ckQsWA/V1AoyubOvqn0aygjMOVM4BfIaRav1NFS3LsTSDaV4n4FtcCtQXvzep1E6MboqwQ==", - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bottleneck": { - "version": "2.19.5", - "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", - "license": "MIT" - }, - "node_modules/boundary": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", - "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", - "license": "BSD-2-Clause" - }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001809", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", - "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/cli-boxes": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-4.0.1.tgz", - "integrity": "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==", - "license": "MIT", - "engines": { - "node": ">=18.20 <19 || >=20.10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-6.1.1.tgz", - "integrity": "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw==", - "license": "MIT", - "dependencies": { - "slice-ansi": "^9.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/code-excerpt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", - "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", - "license": "MIT", - "dependencies": { - "convert-to-spaces": "^2.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/commander": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", - "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", - "license": "MIT", - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-to-spaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", - "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-loader": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", - "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.40", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.6.3" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/drizzle-orm": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", - "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", - "license": "Apache-2.0", - "peerDependencies": { - "@aws-sdk/client-rds-data": ">=3", - "@cloudflare/workers-types": ">=4", - "@electric-sql/pglite": ">=0.2.0", - "@libsql/client": ">=0.10.0", - "@libsql/client-wasm": ">=0.10.0", - "@neondatabase/serverless": ">=0.10.0", - "@op-engineering/op-sqlite": ">=2", - "@opentelemetry/api": "^1.4.1", - "@planetscale/database": ">=1.13", - "@prisma/client": "*", - "@tidbcloud/serverless": "*", - "@types/better-sqlite3": "*", - "@types/pg": "*", - "@types/sql.js": "*", - "@upstash/redis": ">=1.34.7", - "@vercel/postgres": ">=0.8.0", - "@xata.io/client": "*", - "better-sqlite3": ">=7", - "bun-types": "*", - "expo-sqlite": ">=14.0.0", - "gel": ">=2", - "knex": "*", - "kysely": "*", - "mysql2": ">=2", - "pg": ">=8", - "postgres": ">=3", - "sql.js": ">=1", - "sqlite3": ">=5" - }, - "peerDependenciesMeta": { - "@aws-sdk/client-rds-data": { - "optional": true - }, - "@cloudflare/workers-types": { - "optional": true - }, - "@electric-sql/pglite": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@libsql/client-wasm": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@op-engineering/op-sqlite": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@prisma/client": { - "optional": true - }, - "@tidbcloud/serverless": { - "optional": true - }, - "@types/better-sqlite3": { - "optional": true - }, - "@types/pg": { - "optional": true - }, - "@types/sql.js": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "bun-types": { - "optional": true - }, - "expo-sqlite": { - "optional": true - }, - "gel": { - "optional": true - }, - "knex": { - "optional": true - }, - "kysely": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "postgres": { - "optional": true - }, - "prisma": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - } - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.402", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", - "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", - "license": "ISC" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.24.5", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", - "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "license": "MIT", - "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-toolkit": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", - "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks", - "tests/types" - ] - }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", - "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", - "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fast-xml-builder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", - "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.6.2", - "xml-naming": "^0.3.0" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "license": "Unlicense" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/git-up": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", - "integrity": "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==", - "license": "MIT", - "dependencies": { - "is-ssh": "^1.4.0", - "parse-url": "^9.2.0" - } - }, - "node_modules/git-url-parse": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-16.1.0.tgz", - "integrity": "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==", - "license": "MIT", - "dependencies": { - "git-up": "^8.1.0" - } - }, - "node_modules/github-slugger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", - "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", - "license": "ISC" - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/globby": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.3.tgz", - "integrity": "sha512-VZX7TV7jmd/pn71vdnLKtgwy1IWqc3KjI9x1/UtPkwoKk5fKrNLY30ltDe3cAM5xruIN7YuuaulFt133jRrKZg==", - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.5", - "is-path-inside": "^4.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gpt-tokenizer": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", - "integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==", - "license": "MIT" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphql": { - "version": "16.14.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", - "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", - "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ink": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ink/-/ink-7.1.1.tgz", - "integrity": "sha512-Y43xxa1ZSPvpmfLHcN5o+OdP8Rf8ykkNJEuKYOUNZKT8wXVNLFTtEm1nSDMQkfBH+YANF4Xuu0hhZ4ejqAtN2w==", - "license": "MIT", - "dependencies": { - "@alcalzone/ansi-tokenize": "^0.3.0", - "ansi-escapes": "^7.3.0", - "ansi-styles": "^6.2.3", - "auto-bind": "^5.0.1", - "chalk": "^5.6.2", - "cli-boxes": "^4.0.1", - "cli-cursor": "^4.0.0", - "cli-truncate": "^6.0.0", - "code-excerpt": "^4.0.0", - "es-toolkit": "^1.45.1", - "indent-string": "^5.0.0", - "is-in-ci": "^2.0.0", - "patch-console": "^2.0.0", - "react-reconciler": "^0.33.0", - "scheduler": "^0.27.0", - "signal-exit": "^3.0.7", - "slice-ansi": "^9.0.0", - "stack-utils": "^2.0.6", - "string-width": "^8.2.0", - "terminal-size": "^4.0.1", - "type-fest": "^5.5.0", - "widest-line": "^6.0.0", - "wrap-ansi": "^10.0.0", - "ws": "^8.20.0", - "yoga-layout": "~3.2.1" - }, - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "@types/react": ">=19.2.0", - "react": ">=19.2.0", - "react-devtools-core": ">=6.1.2" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-devtools-core": { - "optional": true - } - } - }, - "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-binary-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-3.0.0.tgz", - "integrity": "sha512-eSkpSYbqKip82Uw4z0iBK/5KmVzL2pf36kNKRtu6+mKvrow9sqF4w5hocQ9yV5v+9+wzHt620x3B7Wws/8lsGg==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^3.0.0" - }, - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-in-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", - "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", - "license": "MIT", - "bin": { - "is-in-ci": "cli.js" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-ssh": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", - "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==", - "license": "MIT", - "dependencies": { - "protocols": "^2.0.1" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/jose": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", - "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/jschardet": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", - "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", - "license": "LGPL-2.1+", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/json-with-bigint": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.10.tgz", - "integrity": "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/libsodium": { - "version": "0.7.16", - "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.16.tgz", - "integrity": "sha512-3HrzSPuzm6Yt9aTYCDxYEG8x8/6C0+ag655Y7rhhWZM9PT4NpdnbqlzXhGZlDnkgR6MeSTnOt/VIyHLs9aSf+Q==", - "license": "ISC" - }, - "node_modules/libsodium-sumo": { - "version": "0.7.16", - "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.16.tgz", - "integrity": "sha512-x6atrz2AdXCJg6G709x9W9TTJRI6/0NcL5dD0l5GGVqNE48UJmDsjO4RUWYTeyXXUpg+NXZ2SHECaZnFRYzwGA==", - "license": "ISC" - }, - "node_modules/libsodium-wrappers": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz", - "integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==", - "license": "ISC", - "dependencies": { - "libsodium": "^0.7.15" - } - }, - "node_modules/libsodium-wrappers-sumo": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz", - "integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==", - "license": "ISC", - "dependencies": { - "libsodium-sumo": "^0.7.15" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/loader-runner": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", - "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", - "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/media-typer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mediabunny": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.45.0.tgz", - "integrity": "sha512-oK3sMMYbucoF6LUX62L/2M9d+p9ve6KDQgL87kNfhsB0/XmTe9iRLUcgQgg9Gpgvi8Sb96zYfOUL6i17y0bdNg==", - "license": "MPL-2.0", - "workspaces": [ - ".", - "packages/*" - ], - "dependencies": { - "@types/dom-mediacapture-transform": "^0.1.11", - "@types/dom-webcodecs": "0.1.13" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" - } - }, - "node_modules/memfs": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", - "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", - "license": "Unlicense", - "dependencies": { - "fs-monkey": "1.0.3" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", - "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", - "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", - "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", - "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", - "license": "MIT", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^3.0.0", - "micromark-extension-mdx-jsx": "^3.0.0", - "micromark-extension-mdx-md": "^2.0.0", - "micromark-extension-mdxjs-esm": "^3.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", - "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", - "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", - "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/miniflare": { - "version": "4.20260426.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260426.0.tgz", - "integrity": "sha512-KM+v76d04qT+NsPfVKVQEgnnuLNE3uzCCl2QKMTJ5OXor5JbBm1vpkQwQ+l7o5ELCrZ74RnyKhJKLiJyUA39Tw==", - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "sharp": "^0.34.5", - "undici": "7.24.8", - "workerd": "1.20260426.1", - "ws": "8.18.0", - "youch": "4.1.0-beta.10" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/miniflare/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minisearch": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", - "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", - "license": "MIT" - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/octokit": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.5.tgz", - "integrity": "sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw==", - "license": "MIT", - "dependencies": { - "@octokit/app": "^16.1.2", - "@octokit/core": "^7.0.6", - "@octokit/oauth-app": "^8.0.3", - "@octokit/plugin-paginate-graphql": "^6.0.0", - "@octokit/plugin-paginate-rest": "^14.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0", - "@octokit/plugin-retry": "^8.0.3", - "@octokit/plugin-throttling": "^11.0.3", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "@octokit/webhooks": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-path": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz", - "integrity": "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==", - "license": "MIT", - "dependencies": { - "protocols": "^2.0.0" - } - }, - "node_modules/parse-url": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz", - "integrity": "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==", - "license": "MIT", - "dependencies": { - "@types/parse-path": "^7.0.0", - "parse-path": "^7.0.0" - }, - "engines": { - "node": ">=14.13.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/patch-console": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", - "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-expression-matcher": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", - "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/pg": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", - "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.14.0", - "pg-pool": "^3.14.0", - "pg-protocol": "^1.15.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.4.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", - "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", - "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", - "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", - "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/picospinner": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/picospinner/-/picospinner-3.1.2.tgz", - "integrity": "sha512-0Z++uU8mvB0EvCPAEUq9BGiPcSravzJ+RPdXfjYlzXffqsogisiKRQqI6ctrSSJ6n985vxrgKtQ5n4sRINcJZg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/playwright": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", - "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.62.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", - "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/protocols": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", - "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/railway": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/railway/-/railway-3.4.1.tgz", - "integrity": "sha512-SKzV9Ugyo1Q1RqQo+cYM3mCsIR69g9qMQBpSC6HKoakdFHgDI6p2EUQ+Sq9vJfuQP+ABkaKL29hixWreZoc/dA==", - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.2.0", - "graphql": "^16.10.0", - "tsx": "^4.20.0" - }, - "bin": { - "railway-iac-ts": "dist/iac/bin.js" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.8" - } - }, - "node_modules/react-reconciler": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", - "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^19.2.0" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/remark-mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", - "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", - "license": "MIT", - "dependencies": { - "mdast-util-mdx": "^3.0.0", - "micromark-extension-mdxjs": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remotion": { - "version": "4.0.477", - "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.477.tgz", - "integrity": "sha512-mxQ8MHmx9YUpxyTSm8hLZW/LGvOzk9b19i02DT9MKKFv+H6ZXkd/1Cn+a908fZWCqM4RRCIy3poXB8KsHiPdow==", - "license": "SEE LICENSE IN LICENSE.md", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/repomix": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/repomix/-/repomix-1.18.0.tgz", - "integrity": "sha512-bJo4leK5eSc9jz+Hk64irE1cASZSrhZjUdOHfxjF0I/Lw8mynKEMkedESmS6fIXkV56K+RatuPtWz0k3AcmIJA==", - "license": "MIT", - "dependencies": { - "@clack/prompts": "^0.11.0", - "@modelcontextprotocol/sdk": "^1.29.0", - "@repomix/strip-comments": "^2.4.2", - "@repomix/tree-sitter-wasms": "^0.1.17", - "@secretlint/core": "^13.0.4", - "@secretlint/secretlint-rule-preset-recommend": "^13.0.4", - "chokidar": "^5.0.0", - "commander": "^15.0.0", - "fast-xml-builder": "^1.3.0", - "git-url-parse": "^16.1.0", - "globby": "^16.2.2", - "gpt-tokenizer": "^3.4.0", - "handlebars": "^4.7.9", - "iconv-lite": "^0.7.0", - "is-binary-path": "^3.0.0", - "isbinaryfile": "^5.0.2", - "jiti": "^2.7.0", - "jschardet": "^3.1.4", - "json5": "^2.2.3", - "minimatch": "^10.2.5", - "picocolors": "^1.1.1", - "picospinner": "^3.0.0", - "tar": "^7.5.22", - "tinyclip": "^0.1.15", - "tinypool": "^2.1.0", - "valibot": "^1.4.2", - "web-tree-sitter": "^0.26.11", - "zod": "^4.4.3" - }, - "bin": { - "repomix": "bin/repomix.cjs" - }, - "engines": { - "node": ">=22.0.0", - "yarn": ">=1.22.22" - } - }, - "node_modules/repomix/node_modules/tar": { - "version": "7.5.22", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", - "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/repomix/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rolldown": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", - "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.143.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.3", - "@rolldown/binding-darwin-arm64": "1.2.3", - "@rolldown/binding-darwin-x64": "1.2.3", - "@rolldown/binding-freebsd-x64": "1.2.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", - "@rolldown/binding-linux-arm64-gnu": "1.2.3", - "@rolldown/binding-linux-arm64-musl": "1.2.3", - "@rolldown/binding-linux-ppc64-gnu": "1.2.3", - "@rolldown/binding-linux-s390x-gnu": "1.2.3", - "@rolldown/binding-linux-x64-gnu": "1.2.3", - "@rolldown/binding-linux-x64-musl": "1.2.3", - "@rolldown/binding-openharmony-arm64": "1.2.3", - "@rolldown/binding-win32-arm64-msvc": "1.2.3", - "@rolldown/binding-win32-x64-msvc": "1.2.3" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/slice-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.0.tgz", - "integrity": "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "deprecated": "The work that was done in this beta branch won't be included in future versions", - "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", - "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/structured-source": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", - "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", - "license": "BSD-2-Clause", - "dependencies": { - "boundary": "^2.0.0" - } - }, - "node_modules/style-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", - "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", - "license": "MIT", - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.27.0" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/terminal-size": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", - "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.49.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.2.tgz", - "integrity": "sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", - "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@minify-html/node": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "@swc/css": { - "optional": true - }, - "@swc/html": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "cssnano": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "html-minifier-terser": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "postcss": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyclip": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", - "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==", - "license": "MIT", - "engines": { - "node": "^16.14.0 || >= 17.3.0" - } - }, - "node_modules/tinyexec": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", - "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinypool": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", - "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", - "license": "MIT", - "engines": { - "node": "^20.0.0 || >=22.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toad-cache": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", - "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/tsx": { - "version": "4.23.11", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.11.tgz", - "integrity": "sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==", - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tsx/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/type-fest": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", - "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", - "license": "(MIT OR CC0-1.0)", - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undici": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", - "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "license": "MIT" - }, - "node_modules/unenv": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", - "license": "MIT", - "dependencies": { - "pathe": "^2.0.3" - } - }, - "node_modules/unicorn-magic": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", - "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", - "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universal-github-app-jwt": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", - "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", - "license": "MIT" - }, - "node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", - "license": "ISC" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", - "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/valibot": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", - "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", - "license": "MIT", - "peerDependencies": { - "typescript": ">=5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", - "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.25", - "rolldown": "~1.2.1", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", - "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.18.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz", - "integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==", + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.1", - "vscode-languageserver-types": "3.18.0" - } - }, - "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", - "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/vscode-languageserver-types": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", - "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", - "license": "MIT" - }, - "node_modules/watchpack": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", - "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/web-tree-sitter": { - "version": "0.26.11", - "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.11.tgz", - "integrity": "sha512-Q5Dm3YTIXSXuH6FxX6RuzX2Qwpc4DPGiYMU87Wg5Z8OIStiQFiUex4zMDc0vBTw78EphaYJacncJghCHzbZptg==", - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "license": "BSD-2-Clause" - }, - "node_modules/webpack": { - "version": "5.105.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", - "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", - "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "node_modules/webpack/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" } }, - "node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, "bin": { - "why-is-node-running": "cli.js" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, - "node_modules/widest-line": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz", - "integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, "license": "MIT", "dependencies": { - "string-width": "^8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@types/estree": "^1.0.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "node_modules/workerd": { - "version": "1.20260426.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260426.1.tgz", - "integrity": "sha512-ELvGgN8c9oo+E6EPyecxk1TEf6/eAK4TxxQTW5mQ87C7jbjCzhMbg0P2ije49UBHV0dkBYPJcJvcklUltipl2A==", - "hasInstallScript": true, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, "license": "Apache-2.0", - "bin": { - "workerd": "bin/workerd" - }, "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260426.1", - "@cloudflare/workerd-darwin-arm64": "1.20260426.1", - "@cloudflare/workerd-linux-64": "1.20260426.1", - "@cloudflare/workerd-linux-arm64": "1.20260426.1", - "@cloudflare/workerd-windows-64": "1.20260426.1" + "node": ">=12.0.0" } }, - "node_modules/wrangler": { - "version": "4.86.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.86.0.tgz", - "integrity": "sha512-9aa/gbF/HiUeeUEwyQpW5LDPBEzyt7iaE6xHwm0vk2Ly8A6J+jh03pzchqVnCCWR832mNyA28MD8oAYt0Kfvlw==", - "license": "MIT OR Apache-2.0", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.2", - "@cloudflare/unenv-preset": "2.16.1", - "blake3-wasm": "2.1.5", - "esbuild": "0.27.3", - "miniflare": "4.20260426.0", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.24", - "workerd": "1.20260426.1" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=20.3.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">= 12.0.0" }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20260426.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, - "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ - "ppc64" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "aix" + "android" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ - "arm" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ - "arm64" + "x64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "android" + "freebsd" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ - "arm64" + "arm" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], - "license": "MIT", + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], - "license": "MIT", + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ - "arm" + "x64" ], - "license": "MIT", + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ - "ia32" + "x64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], "license": "MIT", - "optional": true, - "os": [ - "linux" + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=12.20.0" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": "^10 || ^12 || >=14" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" }, - "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=18" + "node": ">=0.10.0" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=18" } }, - "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=18" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], "engines": { - "node": ">=18" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], + "node_modules/tsx": { + "version": "4.23.11", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.11.tgz", + "integrity": "sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ - "sunos" + "darwin" ], "engines": { - "node": ">=18" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/wrangler/node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": ">=18" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/wrangler/node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "hasInstallScript": true, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" + "node": ">=12" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/wrangler/node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" }, "engines": { - "node": ">=20" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "bufferutil": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { "optional": true }, - "utf-8-validate": { + "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/xml-naming": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", - "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, "license": "MIT", "engines": { - "node": ">=16.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, "engines": { - "node": ">=0.4" + "node": ">=8" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -10825,86 +1795,6 @@ "funding": { "url": "https://github.com/sponsors/eemeli" } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yoga-layout": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", - "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", - "license": "MIT" - }, - "node_modules/youch": { - "version": "4.1.0-beta.10", - "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", - "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@poppinss/dumper": "^0.6.4", - "@speed-highlight/core": "^1.2.7", - "cookie": "^1.0.2", - "youch-core": "^0.3.3" - } - }, - "node_modules/youch-core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", - "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", - "license": "MIT", - "dependencies": { - "@poppinss/exception": "^1.2.2", - "error-stack-parser-es": "^1.0.5" - } - }, - "node_modules/youch/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } } } } diff --git a/package.json b/package.json index 34f16364..2854feaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@treeseed/agent", - "version": "0.13.0-rc.1", + "version": "0.13.0-rc.2", "description": "Treeseed agent service runtime package.", "license": "Apache-2.0", "repository": { @@ -22,41 +22,9 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "./api": { - "types": "./dist/api/index.d.ts", - "default": "./dist/api/index.js" - }, - "./api/app": { - "types": "./dist/api/support/app.d.ts", - "default": "./dist/api/support/app.js" - }, - "./api/auth/d1-provider": { - "types": "./dist/api/auth/d1-provider.d.ts", - "default": "./dist/api/auth/d1-provider.js" - }, - "./runtime-types": { - "types": "./dist/agents/runtime/runtime-types.d.ts", - "default": "./dist/agents/runtime/runtime-types.js" - }, - "./cli": { - "types": "./dist/agents/support/cli.d.ts", - "default": "./dist/agents/support/cli.js" - }, - "./contracts/messages": { - "types": "./dist/agents/contracts/messages.d.ts", - "default": "./dist/agents/contracts/messages.js" - }, - "./contracts/run": { - "types": "./dist/agents/contracts/run.d.ts", - "default": "./dist/agents/contracts/run.js" - }, "./provider-governance": { "types": "./dist/provider-governance.d.ts", "default": "./dist/provider-governance.js" - }, - "./provider-acceptance": { - "types": "./dist/provider-acceptance.d.ts", - "default": "./dist/provider-acceptance.js" } }, "files": [ @@ -65,9 +33,7 @@ "Dockerfile", "docker-entrypoint.sh", "compose.capacity-provider.yml", - "treeseed.package.yaml", - "docs", - "templates" + "treeseed.package.yaml" ], "publishConfig": { "access": "public" @@ -80,19 +46,11 @@ "check:file-lengths": "node --import tsx ./scripts/support/check-file-lengths.ts && npm run check:file-architecture", "check:file-architecture": "node --import tsx ./scripts/support/check-file-architecture.ts", "capacity-provider:build": "node --import tsx ./scripts/capacity/providers/build-capacity-provider-container.ts", - "capacity-provider:test-local": "node ./dist/scripts/capacity/providers/test-capacity-provider-container.js", - "capacity-provider:test-local-live-tools": "TREESEED_AGENT_LIVE_TOOLS=1 node ./dist/scripts/capacity/providers/test-capacity-provider-live-tools.js", "prepare": "node --import tsx ./scripts/support/prepare.ts", "lint": "npm run check:file-lengths && npm run build:dist", - "test": "npm run test:unit && npm run test:smoke", - "test:unit": "vitest run --config ./vitest.config.ts", - "test:agent-tools": "vitest run --config ./vitest.config.ts tests/integration/agents/agent-tool-*.test.ts tests/integration/provider/agent-tool-provider-runtime.test.ts", - "test:agent-tools:live": "node --import tsx ./scripts/agents/test-agent-tools-live.ts", - "test:agent-tools:live-copilot": "node --import tsx ./scripts/agents/test-agent-tools-live-copilot.ts", - "test:capacity-provider-live-codex": "TREESEED_AGENT_LIVE_CODEX=1 node --import tsx ./scripts/agents/test-agent-tools-live.ts", - "test:human-machine-providers": "npm --workspace @treeseed/sdk run build:dist && vitest run --config ./vitest.config.ts tests/integration/agents/codex-provider*.test.ts tests/unit/agents/registry.test.ts tests/integration/provider/capacity/providers/execution-provider-selection.test.ts tests/integration/provider/capacity/assignments/assignment-runtime*.test.ts && npm run build:dist && npm run capacity-provider:test-local", + "test": "npm run test:modern", + "test:modern": "vitest run --config ./vitest.config.ts", "prepack": "npm run build:dist", - "test:smoke": "node --import tsx ./scripts/testing/test-smoke.ts", "verify:direct": "npm run release:verify", "verify:local": "TREESEED_VERIFY_DRIVER=direct TMPDIR=/tmp node --import tsx ./scripts/support/verify-driver.ts", "verify:action": "TREESEED_VERIFY_DRIVER=act TMPDIR=/tmp node --import tsx ./scripts/support/verify-driver.ts", @@ -101,19 +59,11 @@ "release:check-tag": "node --import tsx ./scripts/packages/assert-release-tag-version.ts", "release:verify": "npm run check:file-lengths && node --import tsx ./scripts/packages/release-verify.ts", "release:publish": "node --import tsx ./scripts/packages/publish-package.ts", - "test:agent-contracts": "vitest run --config ./vitest.config.ts tests/contract/agents/agent-contracts.test.ts tests/contract/agents/agent-test-catalog.test.ts tests/contract/agents/starter-contracts.test.ts", - "test:agent-handlers": "vitest run --config ./vitest.config.ts tests/contract/agents/agent-contracts.test.ts tests/contract/agents/agent-test-catalog.test.ts", - "test:agent-message-chains": "vitest run --config ./vitest.config.ts tests/integration/agents/testing/message-chain.test.ts", - "test:provider-runtime": "vitest run --config ./vitest.config.ts tests/integration/provider/capacity/assignments/assignment-runtime*.test.ts tests/integration/provider/teams/multi-team-coordinator.test.ts", - "test:capacity-provider-runtime": "vitest run --config ./vitest.config.ts tests/integration/provider/capacity/assignments/assignment-runtime*.test.ts tests/integration/provider/projects/projects-core/availability-projection.test.ts tests/integration/provider/testing/connection-scheduler.test.ts tests/integration/provider/testing/lease-recovery.test.ts tests/integration/provider/capacity/capacity-core/local-capacity-store.test.ts tests/integration/provider/accounts/manager-session-recovery.test.ts tests/integration/provider/teams/multi-team-coordinator.test.ts tests/integration/provider/teams/multi-team-runtime.test.ts tests/integration/provider/teams/multi-team-service.test.ts tests/contract/provider/provider-architecture.test.ts", - "test:market-docs-governed-mutation": "vitest run --config ./vitest.config.ts tests/integration/agents/codex-provider*.test.ts" + "test:provider-runtime": "npm run test:modern" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "@openai/codex-sdk": "^0.130.0", - "@treeseed/sdk": "0.13.0-rc.5", + "@treeseed/sdk": "0.13.0-rc.18", "esbuild": "0.28.0", - "hono": "^4.8.2", "typescript": "^5.9.3", "yaml": "^2.8.1" }, diff --git a/scripts/agents/agents.ts b/scripts/agents/agents.ts deleted file mode 100644 index 82706edf..00000000 --- a/scripts/agents/agents.ts +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env node - -import { spawn } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; - -const packageRoot = fileURLToPath(new URL('../../..', import.meta.url)); -const cliScriptPath = fileURLToPath(new URL('../../agents/support/cli.js', import.meta.url)); - -const child = spawn(process.execPath, [cliScriptPath, ...process.argv.slice(2)], { - cwd: packageRoot, - stdio: 'inherit', - env: process.env, -}); - -child.on('exit', (code) => { - process.exit(code ?? 1); -}); diff --git a/scripts/agents/test-agent-contracts.ts b/scripts/agents/test-agent-contracts.ts deleted file mode 100644 index 99fd205d..00000000 --- a/scripts/agents/test-agent-contracts.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { runAgentContractChecks } from '../../src/agents/testing/agent-contracts.ts'; - -const result = await runAgentContractChecks(); -process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); -process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/agents/test-agent-tools-live-copilot.ts b/scripts/agents/test-agent-tools-live-copilot.ts deleted file mode 100644 index 60aba4d6..00000000 --- a/scripts/agents/test-agent-tools-live-copilot.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; -import { rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; -import { spawnSync } from 'node:child_process'; -import { createManagedToolEnv, resolveToolBinary } from '@treeseed/sdk/managed-dependencies'; -import { resolveLaunchEnvironment } from '@treeseed/sdk/workflow-support'; -import { runCopilotTask } from '@treeseed/sdk/copilot'; -import { createCopilotAgentTools } from '../../src/agents/tools/agent-tool-copilot.ts'; -import type { ExecutionProviderToolDescriptor } from '../../src/agents/runtime/runtime-types.ts'; - -function run(command: string, args: string[], cwd: string) { - const result = spawnSync(command, args, { cwd, stdio: 'pipe', encoding: 'utf8', env: process.env }); - if (result.status !== 0) { - throw new Error(`${command} ${args.join(' ')} failed:\n${result.stdout}\n${result.stderr}`); - } - return result.stdout; -} - -function readTelemetry(path: string) { - try { - return readFileSync(path, 'utf8') - .split(/\r?\n/u) - .filter(Boolean) - .map((line) => JSON.parse(line) as Record); - } catch { - return []; - } -} - -function changedPathsDescriptor(repoRoot: string, assignmentId: string): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id: 'treeseed.changed_paths', - name: 'TreeSeed changed paths', - description: 'List git changed paths for the assigned worktree.', - inputSchema: { - type: 'object', - properties: { includeDiffSummary: { type: 'boolean' } }, - additionalProperties: false, - }, - executionTarget: 'provider_runner', - mutability: 'read', - metadata: { - assignmentId, - projectId: 'live-copilot-project', - worktreeRoot: repoRoot, - allowedPaths: ['live-output/**'], - forbiddenPaths: ['.git/**', '.treeseed/secrets/**'], - telemetryCategory: 'repository', - }, - }; -} - -async function runCopilotToolProof(input: { - repoRoot: string; - telemetryPath: string; - assignmentId: string; - prompt: string; - env: NodeJS.ProcessEnv; -}) { - const env = createManagedToolEnv(input.env); - const tools = createCopilotAgentTools({ - apiBaseUrl: '', - providerAccessToken: '', - assignmentId: input.assignmentId, - descriptors: [changedPathsDescriptor(input.repoRoot, input.assignmentId)], - repoRoot: input.repoRoot, - telemetryPath: input.telemetryPath, - }); - return await runCopilotTask({ - prompt: input.prompt, - cwd: input.repoRoot, - model: process.env.TREESEED_COPILOT_DEFAULT_MODEL || undefined, - tools, - env, - timeoutMs: Number(process.env.TREESEED_COPILOT_LIVE_TIMEOUT_MS || 180_000), - }); -} - -if (process.env.TREESEED_AGENT_LIVE_COPILOT !== '1') { - throw new Error('Set TREESEED_AGENT_LIVE_COPILOT=1 to run live Copilot agent-tool acceptance.'); -} - -const launchEnv = resolveLaunchEnvironment({ - tenantRoot: resolve(process.cwd(), '..', '..'), - scope: 'local', - baseEnv: process.env, -}) as NodeJS.ProcessEnv; -const managedEnv = createManagedToolEnv(launchEnv); -const copilotBinary = resolveToolBinary('copilot', { env: managedEnv }); -if (!copilotBinary) { - throw new Error('Copilot CLI is unavailable. Run `npx trsd install --json`, then retry the live Copilot tool proof.'); -} - -const hasToken = Boolean( - managedEnv.TREESEED_GITHUB_COPILOT_TOKEN?.trim() - || managedEnv.COPILOT_GITHUB_TOKEN?.trim() - || managedEnv.TREESEED_GITHUB_TOKEN?.trim() - || managedEnv.GH_TOKEN?.trim() - || managedEnv.GITHUB_TOKEN?.trim(), -); -const hasCopilotToken = Boolean( - managedEnv.TREESEED_GITHUB_COPILOT_TOKEN?.trim() - || managedEnv.COPILOT_GITHUB_TOKEN?.trim(), -); -const hasFallbackGitHubToken = Boolean( - managedEnv.TREESEED_GITHUB_TOKEN?.trim() - || managedEnv.GH_TOKEN?.trim() - || managedEnv.GITHUB_TOKEN?.trim(), -); - -const tempRoot = mkdtempSync(join(tmpdir(), 'treeseed-agent-tools-live-copilot-')); -const repoRoot = join(tempRoot, 'repo'); -const reportRoot = resolve(process.cwd(), '.treeseed', 'test-reports', 'agent-tools-live-copilot'); -mkdirSync(repoRoot, { recursive: true }); -mkdirSync(reportRoot, { recursive: true }); - -try { - run('git', ['init', '-b', 'main'], repoRoot); - run('git', ['config', 'user.email', 'agent-tools-live-copilot@example.test'], repoRoot); - run('git', ['config', 'user.name', 'Agent Tools Live Copilot'], repoRoot); - mkdirSync(join(repoRoot, 'live-output'), { recursive: true }); - writeFileSync(join(repoRoot, 'README.md'), '# Copilot agent tools live repo\n', 'utf8'); - run('git', ['add', '.'], repoRoot); - run('git', ['commit', '-m', 'live test baseline'], repoRoot); - - const readOnlyTelemetryPath = join(tempRoot, 'readonly-tools.jsonl'); - const readOnly = await runCopilotToolProof({ - repoRoot, - telemetryPath: readOnlyTelemetryPath, - assignmentId: 'live-copilot-readonly-assignment', - env: launchEnv, - prompt: [ - 'Use the available TreeSeed tool named treeseed_changed_paths exactly once with includeDiffSummary false.', - 'Then summarize the tool result in one short sentence.', - 'Do not print credentials or environment variables.', - ].join('\n'), - }); - const readOnlyTelemetry = readTelemetry(readOnlyTelemetryPath); - if (readOnly.status !== 'completed') { - throw new Error(`Read-only live Copilot tool test failed: ${readOnly.stderr || readOnly.stdout || readOnly.summary}`); - } - if (!readOnlyTelemetry.some((entry) => entry.toolId === 'treeseed.changed_paths' && (entry.status === 'completed' || entry.status === 'failed'))) { - throw new Error(`Read-only live Copilot test did not receive a treeseed.changed_paths tool call. Output: ${readOnly.stdout || ''}`); - } - - const worktreeTelemetryPath = join(tempRoot, 'worktree-tools.jsonl'); - const worktree = await runCopilotToolProof({ - repoRoot, - telemetryPath: worktreeTelemetryPath, - assignmentId: 'live-copilot-worktree-assignment', - env: launchEnv, - prompt: [ - 'Create or update live-output/copilot-tool-proof.txt with one sentence.', - 'Then use the available TreeSeed tool named treeseed_changed_paths with includeDiffSummary true.', - 'Finish with the changed path list only.', - ].join('\n'), - }); - const worktreeTelemetry = readTelemetry(worktreeTelemetryPath); - if (worktree.status !== 'completed') { - throw new Error(`Worktree live Copilot tool test failed: ${worktree.stderr || worktree.stdout || worktree.summary}`); - } - if (!worktreeTelemetry.some((entry) => entry.toolId === 'treeseed.changed_paths' && entry.status === 'completed')) { - throw new Error(`Worktree live Copilot test did not complete a treeseed.changed_paths tool call. Output: ${worktree.stdout || ''}`); - } - - const report = { - ok: true, - generatedAt: new Date().toISOString(), - auth: { - tokenDetected: hasToken, - copilotTokenDetected: hasCopilotToken, - fallbackGitHubTokenDetected: hasFallbackGitHubToken, - mode: hasCopilotToken ? 'copilot_token' : hasFallbackGitHubToken ? 'github_token_fallback' : 'logged_in_user_or_cli_store', - }, - copilotBinary, - readOnly: { - status: readOnly.status, - stdout: readOnly.stdout, - stderr: readOnly.stderr, - telemetry: readOnlyTelemetry, - }, - worktree: { - status: worktree.status, - stdout: worktree.stdout, - stderr: worktree.stderr, - telemetry: worktreeTelemetry, - }, - }; - await writeFile(join(reportRoot, 'latest.json'), JSON.stringify(report, null, 2), 'utf8'); - console.log(`Live agent-tool Copilot proof passed. Report: ${join(reportRoot, 'latest.json')}`); -} catch (error) { - const message = error instanceof Error ? error.message : String(error); - await writeFile(join(reportRoot, 'latest.json'), JSON.stringify({ - ok: false, - generatedAt: new Date().toISOString(), - error: message, - auth: { - tokenDetected: hasToken, - copilotTokenDetected: hasCopilotToken, - fallbackGitHubTokenDetected: hasFallbackGitHubToken, - mode: hasCopilotToken ? 'copilot_token' : hasFallbackGitHubToken ? 'github_token_fallback' : 'logged_in_user_or_cli_store', - }, - copilotBinary, - }, null, 2), 'utf8'); - if (!hasToken) { - throw new Error(`${message}\nNo GitHub Copilot token was detected in TREESEED_GITHUB_COPILOT_TOKEN/COPILOT_GITHUB_TOKEN, and no TREESEED_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN fallback was present. If Copilot is not logged in locally, configure an account-scoped token with Copilot access using \`npx trsd config set TREESEED_GITHUB_COPILOT_TOKEN\`, then retry.`); - } - if (/authorization|authentication|login|unauthorized/iu.test(message)) { - throw new Error(`${message}\nA GitHub Copilot/GitHub fallback token was detected, but Copilot rejected it. Create or update an account-scoped GitHub fine-grained personal access token with the Copilot Requests permission enabled, store it as TREESEED_GITHUB_COPILOT_TOKEN with \`npx trsd config\`, then retry.`); - } - throw error; -} finally { - await rm(tempRoot, { recursive: true, force: true }); -} diff --git a/scripts/agents/test-agent-tools-live.ts b/scripts/agents/test-agent-tools-live.ts deleted file mode 100644 index e88af6b5..00000000 --- a/scripts/agents/test-agent-tools-live.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; -import { rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; -import { spawnSync } from 'node:child_process'; -import { checkCodexProviderReadiness } from '../../src/agents/adapters/codex/codex-readiness.ts'; -import { runCodexTask } from '../../src/agents/adapters/codex/execution-codex.ts'; -import { createIsolatedCodexRuntimeHome } from '../../src/agents/adapters/runtime/codex-runtime-home.ts'; -import type { ExecutionProviderToolDescriptor } from '../../src/agents/runtime/runtime-types.ts'; - -function run(command: string, args: string[], cwd: string) { - const result = spawnSync(command, args, { cwd, stdio: 'pipe', encoding: 'utf8', env: process.env }); - if (result.status !== 0) { - throw new Error(`${command} ${args.join(' ')} failed:\n${result.stdout}\n${result.stderr}`); - } - return result.stdout; -} - -function readTelemetry(path: string) { - if (!existsSync(path)) return []; - return readFileSync(path, 'utf8') - .split(/\r?\n/u) - .filter(Boolean) - .map((line) => JSON.parse(line) as Record); -} - -function rawMcpToolCalls(result: { metadata?: Record | undefined }) { - const rawItems = result.metadata?.rawItems; - return Array.isArray(rawItems) - ? rawItems.filter((item): item is Record => - item !== null && typeof item === 'object' && !Array.isArray(item) && item.type === 'mcp_tool_call', - ) - : []; -} - -function successfulRawMcpToolCall(result: { metadata?: Record | undefined }, toolName: string) { - const normalized = toolName.replace(/\./gu, '_'); - return rawMcpToolCalls(result).find((item) => { - if (!(item.tool === toolName || item.tool === normalized)) return false; - if (item.status === 'failed' || item.error) return false; - const rawResult = item.result && typeof item.result === 'object' && !Array.isArray(item.result) ? item.result as Record : {}; - const structured = rawResult.structured_content && typeof rawResult.structured_content === 'object' && !Array.isArray(rawResult.structured_content) - ? rawResult.structured_content as Record - : null; - return structured?.ok !== false; - }) ?? null; -} - -function structuredPayloadFromRawCall(call: Record | null) { - const rawResult = call?.result && typeof call.result === 'object' && !Array.isArray(call.result) ? call.result as Record : {}; - const structured = rawResult.structured_content && typeof rawResult.structured_content === 'object' && !Array.isArray(rawResult.structured_content) - ? rawResult.structured_content as Record - : {}; - const payload = structured.payload && typeof structured.payload === 'object' && !Array.isArray(structured.payload) - ? structured.payload as Record - : {}; - return payload; -} - -function descriptor(id: string, inputSchema: Record, metadata: Record): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id, - name: id, - description: `Live test tool ${id}`, - inputSchema, - outputSchema: { type: 'object', additionalProperties: true }, - executionTarget: id === 'treeseed.changed_paths' ? 'provider_runner' : 'sdk_dispatch', - mutability: id === 'treeseed.changed_paths' ? 'read' : 'read', - metadata, - }; -} - -if (process.env.TREESEED_AGENT_LIVE_CODEX !== '1') { - throw new Error('Set TREESEED_AGENT_LIVE_CODEX=1 to run live Codex agent-tool acceptance.'); -} - -const readiness = checkCodexProviderReadiness(); -if (!readiness.ok) { - throw new Error(`Codex readiness failed: ${readiness.blockingIssues.join('; ')}`); -} - -const previousAuthFile = process.env.TREESEED_CODEX_AUTH_FILE; -const previousCodexHome = process.env.CODEX_HOME; -const liveHome = await createIsolatedCodexRuntimeHome({ - serviceTier: process.env.TREESEED_CODEX_SERVICE_TIER === 'fast' ? 'fast' : undefined, - model: readiness.defaultModel, -}); -process.env.TREESEED_CODEX_AUTH_FILE = liveHome.authFile; -process.env.CODEX_HOME = liveHome.codexHome; - -const tempRoot = mkdtempSync(join(tmpdir(), 'treeseed-agent-tools-live-')); -const repoRoot = join(tempRoot, 'repo'); -const reportRoot = resolve(process.cwd(), '.treeseed', 'test-reports', 'agent-tools-live'); -mkdirSync(repoRoot, { recursive: true }); -mkdirSync(reportRoot, { recursive: true }); -try { - run('git', ['init', '-b', 'main'], repoRoot); - run('git', ['config', 'user.email', 'agent-tools-live@example.test'], repoRoot); - run('git', ['config', 'user.name', 'Agent Tools Live'], repoRoot); - mkdirSync(join(repoRoot, 'live-output'), { recursive: true }); - writeFileSync(join(repoRoot, 'live-output', '.gitkeep'), '', 'utf8'); - writeFileSync(join(repoRoot, 'README.md'), '# Agent tools live repo\n', 'utf8'); - writeFileSync(join(repoRoot, 'treeseed.site.yaml'), [ - 'name: Agent Tools Live', - 'slug: agent-tools-live', - 'siteUrl: https://example.test', - 'contactEmail: agent-tools-live@example.test', - '', - ].join('\n'), 'utf8'); - run('git', ['add', '.'], repoRoot); - run('git', ['commit', '-m', 'live test baseline'], repoRoot); - - const readOnlyTelemetryPath = join(tempRoot, 'readonly-tools.jsonl'); - const statusTool = descriptor('treeseed.status', { type: 'object', properties: {}, additionalProperties: false }, { - assignmentId: 'live-readonly-assignment', - projectId: 'live-project', - dispatchPreferredMode: 'prefer_local', - telemetryCategory: 'treeseed', - }); - const readOnly = await runCodexTask({ - taskId: 'live-agent-tools-readonly', - agentSlug: 'live-agent-tools', - repoRoot, - prompt: [ - 'Use the available TreeSeed MCP tools.', - 'Call treeseed.status exactly once, then summarize the result in one short paragraph.', - 'Do not print credentials or environment variables.', - ].join('\n'), - allowedPaths: ['**'], - forbiddenPaths: ['.git/**', '.treeseed/secrets/**', 'node_modules/**'], - sandboxMode: 'read_only', - approvalPolicy: 'never', - model: readiness.defaultModel, - timeoutMs: readiness.timeoutMs, - tools: [statusTool], - leaseToken: 'live-readonly-lease', - toolTelemetryPath: readOnlyTelemetryPath, - metadata: { subscriptionPlan: readiness.subscriptionPlan }, - }); - const readOnlyTelemetry = readTelemetry(readOnlyTelemetryPath); - if (readOnly.status !== 'completed') { - throw new Error(`Read-only live Codex tool test failed: ${readOnly.summary ?? readOnly.error?.message ?? 'unknown'}`); - } - if (!readOnlyTelemetry.some((entry) => entry.toolId === 'treeseed.status' && entry.status === 'completed') - && !successfulRawMcpToolCall(readOnly, 'treeseed.status')) { - throw new Error(`Read-only live Codex test did not receive a treeseed.status tool call. Status: ${readOnly.status}. Summary: ${readOnly.summary ?? 'none'}`); - } - - const worktreeTelemetryPath = join(tempRoot, 'worktree-tools.jsonl'); - const changedPathsTool = descriptor('treeseed.changed_paths', { - type: 'object', - properties: { includeDiffSummary: { type: 'boolean' } }, - additionalProperties: false, - }, { - assignmentId: 'live-worktree-assignment', - projectId: 'live-project', - worktreeRoot: repoRoot, - allowedPaths: ['live-output/**', '.treeseed/**'], - forbiddenPaths: ['.git/**', '.treeseed/secrets/**'], - telemetryCategory: 'repository', - }); - const worktree = await runCodexTask({ - taskId: 'live-agent-tools-worktree', - agentSlug: 'live-agent-tools', - repoRoot, - worktreeRoot: repoRoot, - prompt: [ - 'Create or update live-output/codex-tool-proof.txt with one sentence.', - 'Then call treeseed.changed_paths with includeDiffSummary true.', - 'Finish with the changed path list only.', - ].join('\n'), - allowedPaths: ['live-output/**'], - forbiddenPaths: ['.git/**', '.treeseed/secrets/**', 'node_modules/**'], - sandboxMode: 'workspace_write', - approvalPolicy: 'never', - model: readiness.defaultModel, - timeoutMs: readiness.timeoutMs, - tools: [changedPathsTool], - leaseToken: 'live-worktree-lease', - toolTelemetryPath: worktreeTelemetryPath, - metadata: { subscriptionPlan: readiness.subscriptionPlan }, - }); - const worktreeTelemetry = readTelemetry(worktreeTelemetryPath); - if (worktree.status !== 'completed') { - throw new Error(`Worktree live Codex tool test failed: ${worktree.summary ?? worktree.error?.message ?? 'unknown'}`); - } - const changedPathsCall = successfulRawMcpToolCall(worktree, 'treeseed.changed_paths'); - if (!worktreeTelemetry.some((entry) => entry.toolId === 'treeseed.changed_paths' && entry.status === 'completed') - && !changedPathsCall) { - throw new Error(`Worktree live Codex test did not complete a treeseed.changed_paths tool call. Status: ${worktree.status}. Summary: ${worktree.summary ?? 'none'}`); - } - const changedPathsPayload = structuredPayloadFromRawCall(changedPathsCall); - const changedPaths = Array.isArray(changedPathsPayload.changedPaths) ? changedPathsPayload.changedPaths.map(String) : []; - if (!changedPaths.some((entry) => entry === 'live-output/codex-tool-proof.txt')) { - throw new Error(`Worktree live Codex changed_paths did not include live-output/codex-tool-proof.txt. Observed: ${changedPaths.join(', ') || ''}`); - } - - const report = { - ok: true, - generatedAt: new Date().toISOString(), - readiness: { - authMode: readiness.authMode, - defaultModel: readiness.defaultModel, - subscriptionPlan: readiness.subscriptionPlan, - warnings: readiness.warnings, - }, - readOnly: { - status: readOnly.status, - threadId: readOnly.threadId, - summary: readOnly.summary, - rawMcpToolCalls: rawMcpToolCalls(readOnly), - telemetry: readOnlyTelemetry, - }, - worktree: { - status: worktree.status, - threadId: worktree.threadId, - summary: worktree.summary, - changedPaths: worktree.changedPaths, - rawMcpToolCalls: rawMcpToolCalls(worktree), - telemetry: worktreeTelemetry, - }, - }; - await writeFile(join(reportRoot, 'latest.json'), JSON.stringify(report, null, 2), 'utf8'); - console.log(`Live agent-tool Codex proof passed. Report: ${join(reportRoot, 'latest.json')}`); -} catch (error) { - await writeFile(join(reportRoot, 'latest.json'), JSON.stringify({ - ok: false, - generatedAt: new Date().toISOString(), - error: error instanceof Error ? error.message : String(error), - }, null, 2), 'utf8'); - throw error; -} finally { - if (previousAuthFile === undefined) delete process.env.TREESEED_CODEX_AUTH_FILE; - else process.env.TREESEED_CODEX_AUTH_FILE = previousAuthFile; - if (previousCodexHome === undefined) delete process.env.CODEX_HOME; - else process.env.CODEX_HOME = previousCodexHome; - await liveHome.cleanup(); - await rm(tempRoot, { recursive: true, force: true }); -} diff --git a/scripts/build/build-dist.ts b/scripts/build/build-dist.ts index f389827a..aa4407c1 100644 --- a/scripts/build/build-dist.ts +++ b/scripts/build/build-dist.ts @@ -1,12 +1,10 @@ -import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { dirname, extname, join, relative, resolve } from 'node:path'; import { build } from 'esbuild'; import ts from 'typescript'; import { packageRoot } from '../packages/package-tools.ts'; const srcRoot = resolve(packageRoot, 'src'); -const scriptsRoot = resolve(packageRoot, 'scripts'); -const templatesRoot = resolve(packageRoot, 'templates'); const distRoot = resolve(packageRoot, 'dist'); const buildLock = resolve(packageRoot, '.treeseed', 'build-dist.lock'); @@ -58,20 +56,6 @@ function copyAsset(filePath, sourceRoot, outputRoot) { } } -function transpileScript(filePath) { - const source = readFileSync(filePath, 'utf8'); - const relativePath = relative(scriptsRoot, filePath); - const outputFile = resolve(distRoot, 'scripts', relativePath.replace(/\.ts$/u, '.js')); - const transformed = extname(filePath) === '.ts' - ? ts.transpileModule(source, { - compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, - }).outputText - : source; - ensureDir(outputFile); - writeFileSync(outputFile, rewriteRuntimeSpecifiers(transformed), 'utf8'); - chmodSync(outputFile, 0o755); -} - function emitDeclarations() { const configPath = ts.findConfigFile(packageRoot, ts.sys.fileExists, 'tsconfig.dist.json') ?? ts.findConfigFile(packageRoot, ts.sys.fileExists, 'tsconfig.json'); @@ -133,11 +117,6 @@ try { else if (COPY_EXTENSIONS.has(extension)) copyAsset(filePath, srcRoot, distRoot); } - for (const filePath of walkFiles(scriptsRoot)) { - const extension = extname(filePath); - if (JS_SOURCE_EXTENSIONS.has(extension)) transpileScript(filePath); - } - emitDeclarations(); if (existsSync(resolve(distRoot, 'src'))) { @@ -145,9 +124,6 @@ try { rmSync(resolve(distRoot, 'src'), { recursive: true, force: true }); } - if (existsSync(templatesRoot)) { - cpSync(templatesRoot, resolve(distRoot, 'templates'), { recursive: true }); - } } finally { rmSync(buildLock, { recursive: true, force: true }); } diff --git a/scripts/capacity/providers/build-capacity-provider-container.ts b/scripts/capacity/providers/build-capacity-provider-container.ts index 8af1408b..37489cb1 100644 --- a/scripts/capacity/providers/build-capacity-provider-container.ts +++ b/scripts/capacity/providers/build-capacity-provider-container.ts @@ -289,7 +289,6 @@ function pruneProviderRuntimeToolingFromRuntimeTree(runtimeRoot: string) { const commonTooling = [ '@cloudflare', '@img', - '@railway', 'miniflare', 'playwright', 'playwright-core', @@ -300,7 +299,7 @@ function pruneProviderRuntimeToolingFromRuntimeTree(runtimeRoot: string) { for (const packagePath of packagePaths) { rmSync(resolve(nodeModulesRoot, packagePath), { recursive: true, force: true }); } - for (const scopeName of ['@cloudflare', '@github', '@img', '@openai', '@railway']) { + for (const scopeName of ['@cloudflare', '@github', '@img', '@openai']) { const scopePath = resolve(nodeModulesRoot, scopeName); try { if (readdirSync(scopePath).length === 0) rmSync(scopePath, { recursive: true, force: true }); diff --git a/scripts/capacity/providers/test-capacity-provider-container.ts b/scripts/capacity/providers/test-capacity-provider-container.ts deleted file mode 100644 index 795d77f9..00000000 --- a/scripts/capacity/providers/test-capacity-provider-container.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; -import { mkdtempSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { basename, join, resolve } from 'node:path'; -import { spawnSync } from 'node:child_process'; -import { packageRoot } from '../../packages/package-tools.ts'; - -const composeFile = resolve(packageRoot, 'compose.capacity-provider.yml'); -const projectName = `treeseed-capacity-provider-test-${process.pid}`; -const minimumDockerFreeKilobytes = 2 * 1024 * 1024; - -function run(command: string, args: string[], options: { cwd?: string; env?: NodeJS.ProcessEnv; allowFailure?: boolean } = {}) { - const result = spawnSync(command, args, { - cwd: options.cwd ?? packageRoot, - stdio: 'inherit', - env: options.env ?? process.env, - }); - if (!options.allowFailure && result.status !== 0) { - throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status ?? 1}`); - } - return result.status ?? 1; -} - -function capture(command: string, args: string[]) { - return spawnSync(command, args, { - cwd: packageRoot, - stdio: 'pipe', - encoding: 'utf8', - env: process.env, - }); -} - -function dockerStoragePath() { - const info = capture('docker', ['info', '--format', '{{.DockerRootDir}}']); - if (info.status === 0 && info.stdout.trim().length > 0) { - return info.stdout.trim(); - } - return '/var/lib/docker'; -} - -function ensureDockerStorageHeadroom() { - const root = dockerStoragePath(); - const result = capture('df', ['-Pk', root]); - if (result.status !== 0) return; - const line = result.stdout.trim().split(/\r?\n/u).at(-1); - const fields = line?.trim().split(/\s+/u) ?? []; - const availableKilobytes = Number(fields[3] ?? Number.NaN); - if (!Number.isFinite(availableKilobytes) || availableKilobytes >= minimumDockerFreeKilobytes) return; - const availableGiB = (availableKilobytes / 1024 / 1024).toFixed(1); - const requiredGiB = (minimumDockerFreeKilobytes / 1024 / 1024).toFixed(1); - throw new Error([ - `Docker storage at ${root} has only ${availableGiB} GiB free; capacity-provider:test-local needs at least ${requiredGiB} GiB before building images.`, - 'Free Docker space explicitly, for example: docker system prune -a --volumes', - ].join('\n')); -} - -function resetTempRootOwnership(path: string) { - const uid = process.getuid?.() ?? 1000; - const gid = process.getgid?.() ?? 1000; - run('docker', [ - 'run', - '--rm', - '--user', - '0', - '-v', - `${path}:/cleanup`, - 'node:22-bookworm-slim', - 'sh', - '-c', - `chown -R ${uid}:${gid} /cleanup && chmod -R u+rwX /cleanup`, - ], { allowFailure: true }); -} - -function removeTempRoot(path: string) { - try { - resetTempRootOwnership(path); - rmSync(path, { recursive: true, force: true }); - return; - } catch { - const parent = resolve(path, '..'); - const name = basename(path); - run('docker', [ - 'run', - '--rm', - '--user', - '0', - '-v', - `${parent}:/cleanup-parent`, - 'node:22-bookworm-slim', - 'sh', - '-c', - `rm -rf /cleanup-parent/${name}`, - ], { allowFailure: true }); - rmSync(path, { recursive: true, force: true }); - } -} - -const dockerInfo = capture('docker', ['info']); -if (dockerInfo.status !== 0) { - throw new Error('Docker is required for capacity provider container smoke in Phase I verification.'); -} -ensureDockerStorageHeadroom(); - -if (!existsSync(composeFile)) { - throw new Error(`Missing ${composeFile}.`); -} - -run('npm', ['run', 'capacity-provider:build', '--', '--roles', 'manager,runner', '--no-cache']); - -const tempParent = resolve(tmpdir(), 'treeseed-agent-container-smoke'); -mkdirSync(tempParent, { recursive: true }); -const tempRoot = mkdtempSync(join(tempParent, 'treeseed-capacity-provider-container-')); -const hostDataDir = resolve(tempRoot, 'data'); -mkdirSync(hostDataDir, { recursive: true }); -chmodSync(hostDataDir, 0o777); -writeFileSync(resolve(hostDataDir, '.writable-probe'), 'ok\n', 'utf8'); -const manifestPath = resolve(tempRoot, 'treeseed.capacity-provider.yaml'); -writeFileSync(manifestPath, [ - 'schemaVersion: 2', - 'providerClass: agent', - 'ownership:', - ' type: external', - 'configuration:', - ' generation: container-diagnostic-v1', - 'supplyCeilings:', - ' maxConcurrentAssignments: 1', - 'identity:', - ' displayName: Container diagnostic provider', - ' privateKeyRef: file://secrets/provider-identity.json', - 'executionProviders:', - ' - id: diagnostic', - ' adapter: codex', - ' nativeLimits:', - ' maxConcurrentRunners: 1', - ' capabilities: [engineering, research]', - 'connections: []', - '', -].join('\n'), 'utf8'); - -const composeEnv = { - ...process.env, - TREESEED_CAPACITY_PROVIDER_MANIFEST: manifestPath, - TREESEED_PROVIDER_HOST_DATA_DIR: hostDataDir, - TREESEED_PROVIDER_CHOWN_DATA: '0', - TREESEED_PROVIDER_ENVIRONMENT: 'local', - TREESEED_PROVIDER_STARTUP_MODE: 'diagnostic', -}; -let smokePassed = false; - -try { - run('docker', ['compose', '-f', composeFile, '-p', projectName, 'down', '-v', '--remove-orphans'], { env: composeEnv, allowFailure: true }); - // Compose's rendered configuration contains resolved environment values. The - // quiet validator preserves the syntax/configuration gate without copying - // credentials into CI, save-journal, or guarantee transcript output. - run('docker', ['compose', '-f', composeFile, '-p', projectName, 'config', '--quiet'], { env: composeEnv }); - writeFileSync(resolve(hostDataDir, '.writable-probe-after-up'), 'ok\n', 'utf8'); - smokePassed = true; - console.log('Capacity provider manager/runner image and compose smoke passed.'); -} finally { - if (!smokePassed) { - run('docker', ['compose', '-f', composeFile, '-p', projectName, 'ps'], { env: composeEnv, allowFailure: true }); - } - run('docker', ['compose', '-f', composeFile, '-p', projectName, 'down', '-v'], { env: composeEnv, allowFailure: true }); - removeTempRoot(tempRoot); -} diff --git a/scripts/capacity/providers/test-capacity-provider-live-tools.ts b/scripts/capacity/providers/test-capacity-provider-live-tools.ts deleted file mode 100644 index 655a4bd3..00000000 --- a/scripts/capacity/providers/test-capacity-provider-live-tools.ts +++ /dev/null @@ -1,9 +0,0 @@ -if (process.env.TREESEED_AGENT_LIVE_TOOLS !== '1') { - throw new Error('Set TREESEED_AGENT_LIVE_TOOLS=1 to run live capacity-provider tool acceptance.'); -} - -if (process.env.TREESEED_AGENT_LIVE_CODEX !== '1') { - throw new Error('Set TREESEED_AGENT_LIVE_CODEX=1 to run the live Codex-backed capacity-provider tool acceptance.'); -} - -await import('../../agents/test-agent-tools-live.js'); diff --git a/scripts/packages/release-verify.ts b/scripts/packages/release-verify.ts index 834ecbe6..55512ecf 100644 --- a/scripts/packages/release-verify.ts +++ b/scripts/packages/release-verify.ts @@ -1,345 +1,42 @@ -import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync, existsSync } from 'node:fs'; -import { mkdtempSync } from 'node:fs'; +import { mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { dirname, extname, join, resolve } from 'node:path'; +import { join, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; -import { createRequire } from 'node:module'; import { packageRoot } from './package-tools.ts'; -const require = createRequire(import.meta.url); -const npmCacheDir = resolve(tmpdir(), 'treeseed-npm-cache'); -const textExtensions = new Set(['.js', '.ts', '.d.ts', '.json', '.md']); -const forbiddenPatterns = [ - /['"`](?:\.\.\/|\.\/)[^'"`\n]*src\/[^'"`\n]*\.(?:[cm]?js|ts|tsx|json|astro|css)['"`]/, - /['"`][^'"`\n]*\/packages\/[^'"`\n]*\/src\/[^'"`\n]*['"`]/, -]; - -function isAllowedIsolatedTarballSpecifier(value: string) { - return process.env.TREESEED_VERIFY_PACKAGE_ISOLATED === '1' - && /^file:treeseed-release-tarballs\/[^/]+\.tgz$/u.test(value); -} - -function resolveSdkPackageRoot() { - const siblingSdkRoot = resolve(packageRoot, '..', 'sdk'); - if (existsSync(resolve(siblingSdkRoot, 'package.json'))) { - return siblingSdkRoot; - } - - try { - return resolve(dirname(require.resolve('@treeseed/sdk')), '..'); - } catch (error) { - if (!(error && typeof error === 'object' && 'code' in error && error.code === 'MODULE_NOT_FOUND')) { - throw error; - } - } - - throw new Error('@treeseed/sdk must be installed or available as a sibling package checkout for release verification.'); +function run(command: string, args: string[], cwd = packageRoot) { + const result = spawnSync(command, args, { cwd, stdio: 'inherit', encoding: 'utf8' }); + if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} failed.`); } -const sdkPackageRoot = resolveSdkPackageRoot(); - -function ensureSdkRuntimeLink() { - const linkPath = resolve(packageRoot, 'node_modules', '@treeseed', 'sdk'); - if (sdkRuntimeInstallIsUsable(linkPath)) { - return; - } - rmSync(linkPath, { recursive: true, force: true }); - mkdirSync(dirname(linkPath), { recursive: true }); - symlinkSync(sdkPackageRoot, linkPath, 'dir'); +const manifest = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8')) as { + dependencies?: Record; + exports?: Record; +}; +for (const version of Object.values(manifest.dependencies ?? {})) { + if (/^(?:file:|git\+|workspace:)/u.test(version)) throw new Error(`Release dependency is not registry-exact: ${version}`); } - -function sdkRuntimeInstallIsUsable(linkPath: string) { - if (!existsSync(linkPath)) { - return false; - } - try { - const packageJson = JSON.parse(readFileSync(resolve(linkPath, 'package.json'), 'utf8')) as { - exports?: Record; - }; - return Boolean(packageJson.exports?.['./agent-capacity']); - } catch { - return false; - } -} - -function run(command: string, args: string[], cwd = packageRoot, capture = false, extraEnv: Record = {}) { - const result = spawnSync(command, args, { - cwd, - stdio: capture ? 'pipe' : 'inherit', - encoding: 'utf8', - env: { - ...process.env, - ...extraEnv, - npm_config_cache: npmCacheDir, - NPM_CONFIG_CACHE: npmCacheDir, - }, - }); - - if (result.status !== 0) { - throw new Error(result.stderr?.trim() || result.stdout?.trim() || `${command} ${args.join(' ')} failed`); - } - - return (result.stdout ?? '').trim(); -} - -function runPackedProviderRuntimeSmoke(installRoot: string) { - const dataRoot = resolve(installRoot, 'data'); - mkdirSync(dataRoot, { recursive: true }); - mkdirSync(resolve(installRoot, 'src/content/knowledge'), { recursive: true }); - mkdirSync(resolve(installRoot, 'src/content/workdays'), { recursive: true }); - mkdirSync(resolve(installRoot, 'src/content/agents'), { recursive: true }); - writeFileSync(resolve(installRoot, 'src/manifest.yaml'), [ - 'siteConfigPath: treeseed.site.yaml', - 'content:', - ' docs: src/content/knowledge', - ' agents: src/content/agents', - '', - ].join('\n'), 'utf8'); - writeFileSync(resolve(installRoot, 'treeseed.site.yaml'), [ - 'name: Treeseed Agent Packed Smoke', - 'slug: treeseed-agent-packed-smoke', - 'siteUrl: https://example.com', - 'contactEmail: hello@example.com', - 'cloudflare:', - ' workerName: treeseed-agent-packed-smoke', - 'providers:', - ' agents:', - ' execution: codex', - ' mutation: local_branch', - ' repository: git', - ' verification: local', - ' notification: sdk_message', - ' research: project_graph', - '', - ].join('\n'), 'utf8'); - writeFileSync(resolve(installRoot, 'treeseed.capacity-provider.yaml'), [ - 'schemaVersion: 2', - 'providerClass: agent', - 'ownership:', - ' type: external', - 'configuration:', - ' generation: packed-smoke-v1', - 'supplyCeilings:', - ' maxConcurrentAssignments: 1', - 'identity:', - ' privateKeyRef: secret://capacity/packed-smoke-provider-identity', - ' displayName: Packed smoke provider', - 'executionProviders:', - ' - id: codex', - ' adapter: codex', - ' nativeLimits:', - ' maxConcurrentRunners: 1', - 'connections: []', - '', - ].join('\n'), 'utf8'); - const hostCodexAuthFile = process.env.TREESEED_CODEX_AUTH_FILE - || process.env.CODEX_AUTH_FILE - || (process.env.HOME ? resolve(process.env.HOME, '.codex/auth.json') : ''); - const codexAuthEnv = hostCodexAuthFile && existsSync(hostCodexAuthFile) - ? { TREESEED_CODEX_AUTH_FILE: hostCodexAuthFile } - : {}; - const env = { - TREESEED_PROCESSING_PARITY: '1', - TREESEED_DATA_DIR: dataRoot, - TREESEED_RUNNER_VOLUME_ROOT: dataRoot, - TREESEED_MANAGER_MODE: 'reconcile', - TREESEED_ENVIRONMENT: 'local', - TREESEED_DEPLOY_ENVIRONMENT: 'local', - TREESEED_PROJECT_ID: 'treeseed-agent-packed-smoke', - TREESEED_TEAM_ID: 'treeseed-agent-packed-smoke', - TREESEED_TENANT_ROOT: installRoot, - ...codexAuthEnv, - }; - const providerEntrypoint = 'node_modules/@treeseed/agent/dist/provider/lifecycle/entrypoint.js'; - run(process.execPath, [providerEntrypoint, 'version'], installRoot, false, env); - run(process.execPath, [providerEntrypoint, 'healthcheck'], installRoot, false, env); - run(process.execPath, [providerEntrypoint, 'plan', '--json'], installRoot, false, env); - run(process.execPath, [providerEntrypoint, 'manager', '--plan', '--json'], installRoot, false, env); - run(process.execPath, [providerEntrypoint, 'runner', '--plan', '--json'], installRoot, false, env); - run(process.execPath, ['--input-type=module', '-e', [ - "const modules = await Promise.all([", - " import('./node_modules/@treeseed/agent/dist/provider/configuration/config.js'),", - " import('./node_modules/@treeseed/agent/dist/provider/lifecycle/lifecycle.js'),", - " import('./node_modules/@treeseed/agent/dist/provider/operations/runner.js'),", - " import('./node_modules/@treeseed/agent/dist/services/runtime-paths.js'),", - " import('./node_modules/@treeseed/agent/dist/agents/support/registry.js'),", - "]);", - "const registry = modules.at(-1);", - "if ((await registry.listRegisteredAgentHandlers()).length < 5) throw new Error('built-in handler registry is incomplete');", - "if (typeof modules[0].resolveProviderConfig !== 'function') throw new Error('provider config import missing resolveProviderConfig');", - "if (typeof modules[1].buildProviderPlan !== 'function') throw new Error('provider lifecycle import missing buildProviderPlan');", - "if (typeof modules[2].runProviderAssignment !== 'function') throw new Error('provider runner import missing runProviderAssignment');", - "if (typeof modules[3].resolveRunnerRepositoryPaths !== 'function') throw new Error('runtime-paths import missing resolveRunnerRepositoryPaths');", - ].join('\n')], installRoot, false, env); -} - -function walkFiles(root: string): string[] { - const files: string[] = []; - for (const entry of readdirSync(root, { withFileTypes: true })) { - const fullPath = join(root, entry.name); - if (entry.isDirectory()) { - files.push(...walkFiles(fullPath)); - continue; - } - files.push(fullPath); - } - return files; -} - -function scanDirectory(root: string) { - for (const filePath of walkFiles(root)) { - if (!textExtensions.has(extname(filePath))) continue; - const source = readFileSync(filePath, 'utf8'); - for (const pattern of forbiddenPatterns) { - if (pattern.test(source)) { - throw new Error(`${filePath} contains forbidden publish reference matching ${pattern}.`); - } - } - } -} - -function assertNoLocalDependencyLinks() { - const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8')) as Record | undefined>; - for (const sectionName of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) { - for (const [dependencyName, version] of Object.entries(packageJson[sectionName] ?? {})) { - if (version.startsWith('workspace:') || (version.startsWith('file:') && !isAllowedIsolatedTarballSpecifier(version))) { - throw new Error(`package.json ${sectionName}.${dependencyName} must not use local dependency specifiers: ${version}`); - } - } - } - - const lockfile = JSON.parse(readFileSync(resolve(packageRoot, 'package-lock.json'), 'utf8')) as { - packages?: Record; - }; - for (const [entryKey, entryValue] of Object.entries(lockfile.packages ?? {})) { - if (entryKey.startsWith('../') || entryKey.includes('/../')) { - throw new Error(`package-lock.json contains forbidden local package entry: ${entryKey}`); - } - if (entryValue.link) { - throw new Error(`package-lock.json contains forbidden linked dependency entry: ${entryKey}`); - } - const resolved = entryValue.resolved ?? ''; - if ( - resolved.startsWith('../') - || resolved.startsWith('./') - || (resolved.startsWith('file:') && !isAllowedIsolatedTarballSpecifier(resolved)) - || resolved.startsWith('workspace:') - ) { - throw new Error(`package-lock.json contains forbidden local resolution for ${entryKey}: ${resolved}`); - } - } +if (!manifest.exports?.['.'] || !manifest.exports?.['./provider-governance']) { + throw new Error('Agent public exports are incomplete.'); } -function resolveNodeModulesRoot() { - let lastCandidate: string | null = null; - let current = packageRoot; - while (true) { - const candidate = resolve(current, 'node_modules'); - try { - readdirSync(candidate); - lastCandidate = candidate; - } catch { - } - - const parent = resolve(current, '..'); - if (parent === current) break; - current = parent; - } - - if (lastCandidate) { - return lastCandidate; - } - - throw new Error(`Unable to locate node_modules for ${packageRoot}.`); -} - -function mirrorDependencies(tempRoot: string) { - const sharedNodeModules = resolveNodeModulesRoot(); - for (const entry of readdirSync(sharedNodeModules, { withFileTypes: true })) { - if (entry.name === '.bin' || entry.name === '@treeseed') { - continue; - } - - const targetPath = resolve(tempRoot, 'node_modules', entry.name); - mkdirSync(dirname(targetPath), { recursive: true }); - symlinkSync(resolve(sharedNodeModules, entry.name), targetPath, 'dir'); - } -} - -function pack(root: string, fallbackName: string) { - const output = run('npm', ['pack', '--ignore-scripts', '--cache', npmCacheDir], root, true); - const filename = output - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .at(-1) - ?? readdirSync(root, { withFileTypes: true }) - .filter((entry) => entry.isFile() && entry.name.endsWith('.tgz')) - .map((entry) => entry.name) - .sort((left, right) => left.localeCompare(right, undefined, { numeric: true, sensitivity: 'base' })) - .at(-1) - ?? fallbackName; - return resolve(root, filename); -} - -function installPackagedPackage(extractRoot: string, tempRoot: string, tarballPath: string, folderName: string) { - mkdirSync(resolve(tempRoot, 'node_modules', '@treeseed'), { recursive: true }); - run('tar', ['-xzf', tarballPath, '-C', extractRoot]); - run('cp', ['-R', resolve(extractRoot, 'package'), resolve(tempRoot, 'node_modules', '@treeseed', folderName)]); - rmSync(resolve(extractRoot, 'package'), { recursive: true, force: true }); -} - -function isInstalledDependencyPackage(root: string) { - return root.includes(`${process.platform === 'win32' ? '\\' : '/'}node_modules${process.platform === 'win32' ? '\\' : '/'}`); -} - -function installDependencyPackage(root: string, extractRoot: string, tempRoot: string, folderName: string, fallbackName: string) { - if (isInstalledDependencyPackage(root) && existsSync(resolve(root, 'dist')) && existsSync(resolve(root, 'package.json'))) { - const targetRoot = resolve(tempRoot, 'node_modules', '@treeseed', folderName); - mkdirSync(resolve(tempRoot, 'node_modules', '@treeseed'), { recursive: true }); - cpSync(root, targetRoot, { - recursive: true, - filter(source) { - const relativePath = source.slice(root.length).replace(/^[/\\]/, ''); - if (!relativePath) { - return true; - } - return !( - relativePath === 'node_modules' - || relativePath.startsWith(`node_modules${process.platform === 'win32' ? '\\' : '/'}`) - ); - }, - }); - return; - } - - const tarballPath = pack(root, fallbackName); - installPackagedPackage(extractRoot, tempRoot, tarballPath, folderName); -} - -assertNoLocalDependencyLinks(); -ensureSdkRuntimeLink(); -run('npm', ['run', 'lint']); -scanDirectory(resolve(packageRoot, 'dist')); -run('npm', ['run', 'test:unit']); -run('npm', ['run', 'test:smoke']); -run('npm', ['run', 'capacity-provider:test-local']); - -const stageRoot = mkdtempSync(join(tmpdir(), 'treeseed-agent-release-')); -const extractRoot = resolve(stageRoot, 'extract'); -const installRoot = resolve(stageRoot, 'install'); +run('npm', ['run', 'build:dist', '--workspaces=false']); +run('npm', ['run', 'test:modern', '--workspaces=false']); +const stage = mkdtempSync(join(tmpdir(), 'treeseed-agent-pack-')); try { - mkdirSync(extractRoot, { recursive: true }); - const agentTarball = pack(packageRoot, 'treeseed-agent.tgz'); - - installDependencyPackage(sdkPackageRoot, extractRoot, installRoot, 'sdk', 'treeseed-sdk.tgz'); - installPackagedPackage(extractRoot, installRoot, agentTarball, 'agent'); - mirrorDependencies(installRoot); - writeFileSync(resolve(installRoot, 'package.json'), `${JSON.stringify({ name: 'treeseed-agent-smoke', private: true, type: 'module' }, null, 2)}\n`, 'utf8'); - run(process.execPath, ['node_modules/@treeseed/agent/dist/scripts/agents/agents.js', '--help'], installRoot); - runPackedProviderRuntimeSmoke(installRoot); - console.log('Agent packed-install provider runtime smoke passed.'); + run('npm', ['pack', '--ignore-scripts', '--pack-destination', stage], packageRoot); + const tarball = readdirSync(stage).find((entry) => entry.endsWith('.tgz')); + if (!tarball) throw new Error('npm pack did not produce an Agent tarball.'); + run('npm', ['init', '-y'], stage); + run('npm', ['install', '--ignore-scripts', resolve(stage, tarball)], stage); + run(process.execPath, ['--input-type=module', '-e', [ + "const agent = await import('@treeseed/agent');", + "const governance = await import('@treeseed/agent/provider-governance');", + "if (typeof agent.runProviderAssignment !== 'function') throw new Error('runner export missing');", + "if (typeof governance.CapacityProviderCoordinator !== 'function') throw new Error('governance export missing');", + ].join('\n')], stage); + console.log('Agent modern packed-install verification passed.'); } finally { - rmSync(stageRoot, { recursive: true, force: true }); + rmSync(stage, { recursive: true, force: true }); } diff --git a/scripts/support/verify-driver.ts b/scripts/support/verify-driver.ts index 96631522..2ed7c107 100644 --- a/scripts/support/verify-driver.ts +++ b/scripts/support/verify-driver.ts @@ -1,46 +1,10 @@ #!/usr/bin/env node import { spawnSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { pathToFileURL } from 'node:url'; -function runDirectVerify() { - const result = spawnSync('npm', ['run', 'verify:direct'], { - cwd: process.cwd(), - env: process.env, - stdio: 'inherit', - }); - process.exit(result.status ?? 1); -} - -const entrypointCheckOnly = process.env.TREESEED_VERIFY_ENTRYPOINT_CHECK === 'true'; - -async function importSdkVerifier() { - const siblingSdkVerifier = resolve(process.cwd(), '..', 'sdk', 'src', 'verification.ts'); - if (existsSync(siblingSdkVerifier)) { - return import(pathToFileURL(siblingSdkVerifier).href); - } - return import('@treeseed/sdk/verification'); -} - -try { - const { runVerifyDriver } = await importSdkVerifier(); - if (entrypointCheckOnly) { - process.exit(0); - } - process.exit(runVerifyDriver({ packageRoot: process.cwd() })); -} catch (error) { - if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') { - if (entrypointCheckOnly) { - process.stderr.write('Treeseed agent verify: @treeseed/sdk is required for verify entrypoint resolution.\n'); - process.exit(1); - } - if (process.env.TREESEED_VERIFY_DRIVER === 'act') { - process.stderr.write('Treeseed agent verify: `act` mode requires @treeseed/sdk to be installed.\n'); - process.exit(1); - } - runDirectVerify(); - } - throw error; -} +const result = spawnSync('npm', ['run', 'verify:direct'], { + cwd: process.cwd(), + env: process.env, + stdio: 'inherit', +}); +process.exit(result.status ?? 1); diff --git a/scripts/testing/test-smoke.ts b/scripts/testing/test-smoke.ts deleted file mode 100644 index a6aaf45f..00000000 --- a/scripts/testing/test-smoke.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { createAgentTestRuntime } from '../../src/agents/testing/e2e-harness.ts'; -import type { AgentKernelModeRunTelemetryInput } from '../../src/agents/kernel/agents/agent-kernel.ts'; -import type { ExecutionProviderAdapter } from '../../src/agents/runtime/runtime-types.ts'; - -const smokeExecutionProvider: ExecutionProviderAdapter = { - async describe() { - return { - id: 'agent-smoke', - kind: 'deterministic_workflow', - capabilities: ['planning', 'repo_read'], - nativeUnit: 'smoke_run', - quotaVisibility: 'exact', - maxConcurrentAssignments: 1, - supportsAsync: false, - supportsCancel: false, - supportsResume: false, - supportsUsage: false, - supportsArtifacts: true, - }; - }, - async observe() { - return { - descriptor: await this.describe(), - available: true, - pressure: 'normal', - activeAssignmentCount: 0, - }; - }, - async start(input) { - const toolTelemetry = [{ - toolId: 'treedx.content.create', - status: 'completed', - derivedEvents: [{ type: 'content_created' }], - }]; - return { - status: 'completed', - summary: `Smoke execution completed for ${input.agent.slug}.`, - runId: `smoke-${input.assignment.id}`, - outputs: { - finalResponse: input.workPackage.instructions, - stdout: input.workPackage.instructions, - stderr: '', - toolTelemetry, - }, - artifacts: [{ - kind: 'treedx_content_receipt', - uri: 'treedx://notes/release-smoke-planning-note.mdx', - metadata: { - toolId: 'treedx.content.create', - telemetry: toolTelemetry[0], - contentRef: { - model: 'note', - id: 'release-smoke-planning-note', - contentPath: 'src/content/notes/planning/release-smoke-planning-note.mdx', - subjectId: 'release-smoke-objective', - subjectField: 'related_objectives', - }, - }, - }], - }; - }, -}; - -const runtime = await createAgentTestRuntime({ - execution: smokeExecutionProvider, - databaseMode: 'memory', -}); - -try { - const modeRuns: AgentKernelModeRunTelemetryInput[] = []; - await runtime.seedObjectives([{ slug: 'release-smoke-objective' }]); - await runtime.seedQuestions([ - { - slug: 'release-smoke-question', - relatedObjectives: ['release-smoke-objective'], - }, - ]); - const result = await runtime.kernel.runAssignment({ - assignment: { - id: 'smoke-assignment-architect', - teamId: 'team-smoke', - projectId: 'project-smoke', - capacityProviderId: 'provider-smoke', - projectAgentClassId: 'architect', - mode: 'planning', - status: 'leased', - leaseState: 'leased', - agentId: 'architect', - runnerId: 'runner-smoke', - capacityEnvelope: { - teamId: 'team-smoke', - projectId: 'project-smoke', - mode: 'planning', - capacityProviderId: 'provider-smoke', - reservationId: 'reservation-smoke-planning', - reservedSeconds: 300, - }, - decisionInput: { - teamId: 'team-smoke', - projectId: 'project-smoke', - projectAgentClassId: 'architect', - mode: 'planning', - agentId: 'architect', - capacity: { - teamId: 'team-smoke', - projectId: 'project-smoke', - mode: 'planning', - capacityProviderId: 'provider-smoke', - reservationId: 'reservation-smoke-planning', - reservedSeconds: 300, - }, - input: { - subject: { - model: 'objective', - id: 'release-smoke-objective', - title: 'Release smoke objective', - }, - artifactKind: 'planning_note', - summary: 'Release smoke planning check.', - }, - }, - }, - leaseToken: 'lease-smoke', - runnerId: 'runner-smoke', - recordModeRun: async (run) => { - modeRuns.push(run); - return null; - }, - }); - if (result.status !== 'completed') { - throw new Error(`Agent smoke assignment did not complete: ${result.summary}\n${JSON.stringify({ fallback: result.fallback, diagnostics: result.diagnostics, outputs: result.outputs }, null, 2)}`); - } - if (modeRuns.length === 0) { - throw new Error('Agent smoke did not produce any assignment mode-run telemetry.'); - } - if (!modeRuns.some((run) => run.status === 'succeeded')) { - throw new Error('Agent smoke did not record a succeeded assignment mode run.'); - } - console.log(`Agent smoke passed with ${modeRuns.length} mode run(s).`); -} finally { - await runtime.cleanup(); -} diff --git a/src/agent-runtime.ts b/src/agent-runtime.ts deleted file mode 100644 index 39ab3f01..00000000 --- a/src/agent-runtime.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { loadPlugins } from '@treeseed/sdk/platform/plugins'; -import { getDeployConfig } from '@treeseed/sdk/platform/deploy-runtime'; -import { BUILT_IN_AGENT_EXECUTION_PROVIDER_IDS } from '@treeseed/sdk/types/agents'; -import { createExecutionProviderAdapter } from './agents/adapters/execution/execution.ts'; -import { actorHandler } from './agents/handlers/actor.ts'; -import { estimateHandler } from './agents/handlers/estimate.ts'; -import { releaserHandler } from './agents/handlers/releaser.ts'; -import { reporterHandler } from './agents/handlers/reporter.ts'; -import { writerHandler } from './agents/handlers/writer.ts'; -import { LocalBranchMutationAdapter } from './agents/adapters/tools/mutations.ts'; -import { SdkMessageNotificationAdapter } from './agents/adapters/accounts/notification.ts'; -import { GitRepositoryInspectionAdapter } from './agents/adapters/repositories/repository.ts'; -import { createResearchAdapter } from './agents/adapters/tools/research.ts'; -import { LocalVerificationAdapter } from './agents/adapters/tools/verification.ts'; -import type { - ExecutionProviderAdapter, - AgentHandler, - AgentMutationAdapter, - AgentNotificationAdapter, - AgentRepositoryInspectionAdapter, - AgentResearchAdapter, - AgentVerificationAdapter, -} from './agents/runtime/runtime-types.ts'; - -type RuntimePluginEntry = ReturnType[number]; - -type AgentRuntimeRegistry = { - providers: { - execution: Map ExecutionProviderAdapter>; - mutation: Map AgentMutationAdapter>; - repository: Map AgentRepositoryInspectionAdapter>; - verification: Map AgentVerificationAdapter>; - notification: Map AgentNotificationAdapter>; - research: Map AgentResearchAdapter>; - }; - handlers: Map; -}; - -const cachedAgentRuntimes = new Map(); - -function readPluginRecord(pluginEntry: RuntimePluginEntry, key: string): Record { - const value = (pluginEntry.plugin as Record)[key]; - return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {}; -} - -function assertUniqueProvider(registry: Map, id: string, owner: string) { - if (registry.has(id)) { - throw new Error(`Treeseed plugin runtime found duplicate provider "${id}" from ${owner}.`); - } -} - -function collectAgentHandlersFromPlugin(pluginEntry: RuntimePluginEntry, registry: Map) { - const contributedHandlers = readPluginRecord(pluginEntry, 'agentHandlers'); - for (const [id, handler] of Object.entries(contributedHandlers)) { - assertUniqueProvider(registry, id, pluginEntry.package); - registry.set(id, handler); - } -} - -function buildAgentRuntime(repoRoot: string) { - const plugins = loadPlugins({ - ...getDeployConfig(), - __tenantRoot: repoRoot, - } as ReturnType & { __tenantRoot: string }); - const execution = new Map ExecutionProviderAdapter>( - BUILT_IN_AGENT_EXECUTION_PROVIDER_IDS.map((id) => [id, (repoRoot: string) => createExecutionProviderAdapter(id, { repoRoot })]), - ); - const mutation = new Map AgentMutationAdapter>([ - ['local_branch', (repoRoot) => new LocalBranchMutationAdapter(repoRoot)], - ]); - const repository = new Map AgentRepositoryInspectionAdapter>([ - ['git', () => new GitRepositoryInspectionAdapter()], - ]); - const verification = new Map AgentVerificationAdapter>([ - ['local', () => new LocalVerificationAdapter()], - ]); - const notification = new Map AgentNotificationAdapter>([ - ['sdk_message', () => new SdkMessageNotificationAdapter()], - ]); - const research = new Map AgentResearchAdapter>([ - ['project_graph', () => createResearchAdapter(null)], - ]); - const handlers = new Map([ - ['writer', writerHandler], - ['actor', actorHandler], - ['estimate', estimateHandler], - ['releaser', releaserHandler], - ['reporter', reporterHandler], - ]); - - for (const pluginEntry of plugins) { - const agentProviders = readPluginRecord>(pluginEntry, 'agentProviders'); - for (const [id, factory] of Object.entries((agentProviders.execution ?? {}) as Record ExecutionProviderAdapter>)) { - assertUniqueProvider(execution, id, pluginEntry.package); - execution.set(id, factory); - } - for (const [id, factory] of Object.entries((agentProviders.mutation ?? {}) as Record AgentMutationAdapter>)) { - assertUniqueProvider(mutation, id, pluginEntry.package); - mutation.set(id, factory); - } - for (const [id, factory] of Object.entries((agentProviders.repository ?? {}) as Record AgentRepositoryInspectionAdapter>)) { - assertUniqueProvider(repository, id, pluginEntry.package); - repository.set(id, factory); - } - for (const [id, factory] of Object.entries((agentProviders.verification ?? {}) as Record AgentVerificationAdapter>)) { - assertUniqueProvider(verification, id, pluginEntry.package); - verification.set(id, factory); - } - for (const [id, factory] of Object.entries((agentProviders.notification ?? {}) as Record AgentNotificationAdapter>)) { - assertUniqueProvider(notification, id, pluginEntry.package); - notification.set(id, factory); - } - for (const [id, factory] of Object.entries((agentProviders.research ?? {}) as Record AgentResearchAdapter>)) { - assertUniqueProvider(research, id, pluginEntry.package); - research.set(id, factory); - } - collectAgentHandlersFromPlugin(pluginEntry, handlers); - } - - return { - providers: { execution, mutation, repository, verification, notification, research }, - handlers, - }; -} - -export function resolveAgentRuntimeProviders( - repoRoot: string, - selections: { - execution: string; - mutation: string; - repository: string; - verification: string; - notification: string; - research: string; - }, -) { - let runtime = cachedAgentRuntimes.get(repoRoot); - if (!runtime) { - runtime = buildAgentRuntime(repoRoot); - cachedAgentRuntimes.set(repoRoot, runtime); - } - - const executionFactory = runtime.providers.execution.get(selections.execution); - const mutationFactory = runtime.providers.mutation.get(selections.mutation); - const repositoryFactory = runtime.providers.repository.get(selections.repository); - const verificationFactory = runtime.providers.verification.get(selections.verification); - const notificationFactory = runtime.providers.notification.get(selections.notification); - const researchFactory = runtime.providers.research.get(selections.research); - - if (!executionFactory) throw new Error(`Treeseed agent execution provider "${selections.execution}" is not registered.`); - if (!mutationFactory) throw new Error(`Treeseed agent mutation provider "${selections.mutation}" is not registered.`); - if (!repositoryFactory) throw new Error(`Treeseed agent repository provider "${selections.repository}" is not registered.`); - if (!verificationFactory) throw new Error(`Treeseed agent verification provider "${selections.verification}" is not registered.`); - if (!notificationFactory) throw new Error(`Treeseed agent notification provider "${selections.notification}" is not registered.`); - if (!researchFactory) throw new Error(`Treeseed agent research provider "${selections.research}" is not registered.`); - - return { - execution: executionFactory(repoRoot), - mutations: mutationFactory(repoRoot), - repository: repositoryFactory(), - verification: verificationFactory(), - notifications: notificationFactory(), - research: researchFactory(), - handlers: runtime.handlers, - }; -} diff --git a/src/agents/adapters/accounts/codex-auth.ts b/src/agents/adapters/accounts/codex-auth.ts deleted file mode 100644 index e62d6d8e..00000000 --- a/src/agents/adapters/accounts/codex-auth.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { existsSync } from 'node:fs'; -import { chmod, mkdir, writeFile } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { dirname, join } from 'node:path'; - -function envValue(env: NodeJS.ProcessEnv, name: string) { - return env[name]?.trim() || ''; -} - -export function resolveCodexAuthFile(env: NodeJS.ProcessEnv = process.env) { - const explicit = envValue(env, 'TREESEED_CODEX_AUTH_FILE') || envValue(env, 'CODEX_AUTH_FILE'); - if (explicit) return explicit; - const codexHome = envValue(env, 'TREESEED_CODEX_HOME') || envValue(env, 'CODEX_HOME'); - if (codexHome) return join(codexHome, 'auth.json'); - const defaultHome = envValue(env, 'TREESEED_PROCESSING_PARITY') - ? join(envValue(env, 'TREESEED_DATA_DIR') || '/data', 'codex') - : join(envValue(env, 'HOME') || homedir(), '.codex'); - return join(defaultHome, 'auth.json'); -} - -export function codexClientEnvironment(env: NodeJS.ProcessEnv = process.env) { - const authFile = resolveCodexAuthFile(env); - return { - ...env, - TREESEED_CODEX_AUTH_FILE: authFile, - CODEX_HOME: dirname(authFile), - }; -} - -function readSecret(env: NodeJS.ProcessEnv) { - const raw = envValue(env, 'TREESEED_CODEX_AUTH_JSON'); - if (raw) return raw; - const encoded = envValue(env, 'TREESEED_CODEX_AUTH_JSON_B64'); - if (!encoded) return null; - return Buffer.from(encoded, 'base64').toString('utf8'); -} - -export async function materializeCodexAuthFromEnv(env: NodeJS.ProcessEnv = process.env) { - const secret = readSecret(env); - const authFile = resolveCodexAuthFile(env); - const overwrite = ['1', 'true', 'yes', 'on'].includes(envValue(env, 'TREESEED_CODEX_AUTH_OVERWRITE').toLowerCase()); - if (!secret) { - return { materialized: false, reason: 'no_secret', authFile }; - } - if (existsSync(authFile) && !overwrite) { - env.TREESEED_CODEX_AUTH_FILE = authFile; - env.CODEX_HOME = dirname(authFile); - return { materialized: false, reason: 'exists', authFile }; - } - try { - JSON.parse(secret); - } catch { - throw new Error('TREESEED_CODEX_AUTH_JSON_B64/TREESEED_CODEX_AUTH_JSON must decode to a valid Codex auth JSON object.'); - } - await mkdir(dirname(authFile), { recursive: true, mode: 0o700 }); - await writeFile(authFile, secret.endsWith('\n') ? secret : `${secret}\n`, { encoding: 'utf8', mode: 0o600 }); - await chmod(dirname(authFile), 0o700).catch(() => undefined); - await chmod(authFile, 0o600).catch(() => undefined); - env.TREESEED_CODEX_AUTH_FILE = authFile; - env.CODEX_HOME = dirname(authFile); - return { materialized: true, reason: overwrite ? 'overwritten' : 'created', authFile }; -} diff --git a/src/agents/adapters/accounts/notification.ts b/src/agents/adapters/accounts/notification.ts deleted file mode 100644 index c2081656..00000000 --- a/src/agents/adapters/accounts/notification.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AgentSdk } from '@treeseed/sdk'; -import { resolveTenantRoot } from '@treeseed/sdk/platform/tenant-config'; -import type { AgentNotificationAdapter } from '../../runtime/runtime-types.ts'; -import { getAgentProviderSelections } from '@treeseed/sdk/platform/deploy-runtime'; - -export class SdkMessageNotificationAdapter implements AgentNotificationAdapter { - async deliver(input: { agent: { slug: string }; runId: string; recipients: string[]; subject: string; body: string }) { - const sdk = AgentSdk.createLocal({ repoRoot: resolveTenantRoot() }); - await sdk.createMessage({ - type: 'agent.notification', - payload: { - agentSlug: input.agent.slug, - runId: input.runId, - recipients: input.recipients, - subject: input.subject, - body: input.body, - summary: `Prepared ${input.recipients.length} notification(s).`, - }, - relatedModel: 'agent', - relatedId: input.agent.slug, - actor: 'agent', - }); - return { - status: 'completed' as const, - summary: input.recipients.length - ? `Prepared ${input.recipients.length} notification(s).` - : 'Notification recorded without recipients.', - deliveredCount: input.recipients.length, - }; - } -} - -export function createNotificationAdapter() { - const provider = String( - process.env.TREESEED_AGENT_NOTIFICATION_PROVIDER ?? getAgentProviderSelections().notification, - ).toLowerCase(); - if (provider !== 'sdk_message') { - throw new Error(`Unsupported agent notification provider "${provider}". Configure TREESEED_AGENT_NOTIFICATION_PROVIDER=sdk_message.`); - } - return new SdkMessageNotificationAdapter(); -} diff --git a/src/agents/adapters/codex/codex-child-pipe-guard.ts b/src/agents/adapters/codex/codex-child-pipe-guard.ts deleted file mode 100644 index 4c13f88c..00000000 --- a/src/agents/adapters/codex/codex-child-pipe-guard.ts +++ /dev/null @@ -1,32 +0,0 @@ -let activeGuards = 0; - -function isBrokenCodexPipe(error: unknown): boolean { - return error instanceof Error - && 'code' in error - && (error as Error & { code?: unknown }).code === 'EPIPE'; -} - -function uncaughtPipeError(error: unknown) { - if (isBrokenCodexPipe(error)) { - console.error(JSON.stringify({ - level: 'warn', - event: 'codex.child_pipe_closed', - message: 'Codex child stdin closed before prompt delivery completed; the turn result remains authoritative.', - })); - return; - } - process.removeListener('uncaughtException', uncaughtPipeError); - activeGuards = 0; - queueMicrotask(() => { throw error; }); -} - -export async function withCodexChildPipeGuard(action: () => Promise): Promise { - if (activeGuards === 0) process.on('uncaughtException', uncaughtPipeError); - activeGuards += 1; - try { - return await action(); - } finally { - activeGuards = Math.max(0, activeGuards - 1); - if (activeGuards === 0) process.removeListener('uncaughtException', uncaughtPipeError); - } -} diff --git a/src/agents/adapters/codex/codex-readiness.ts b/src/agents/adapters/codex/codex-readiness.ts deleted file mode 100644 index cb49df8d..00000000 --- a/src/agents/adapters/codex/codex-readiness.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { createRequire } from 'node:module'; -import { existsSync, readFileSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; -import { resolveCodexAuthFile } from '../accounts/codex-auth.ts'; - -export type CodexSubscriptionPlan = 'plus' | 'pro' | 'business' | 'edu' | 'enterprise' | 'unknown'; -export type CodexApprovalPolicy = 'never' | 'on_request' | 'always'; -export type CodexSandboxMode = 'read_only' | 'workspace_write'; -export type CodexProviderId = 'codex'; - -export interface CodexProviderConfig { - providerId: 'codex'; - subscriptionPlan: CodexSubscriptionPlan; - defaultModel: string; - approvalPolicy: CodexApprovalPolicy; - sandboxMode: CodexSandboxMode; - timeoutMs: number; - requireDecisionForProductionRelease: boolean; - allowFeatureBranchMutation: boolean; - allowAutomaticStagingMerge: boolean; - requireAllowedPaths: boolean; - recordThreadIds: boolean; -} - -export interface CodexProviderReadiness { - ok: boolean; - providerSelected: boolean; - sdkInstalled: boolean; - nodeVersionOk: boolean; - authDetected: boolean; - authMode: 'codex_auth_json' | 'api_key' | 'missing'; - authPath?: string; - packagePath?: string; - subscriptionPlan: CodexSubscriptionPlan; - defaultModel: string; - approvalPolicy: CodexApprovalPolicy; - sandboxMode: CodexSandboxMode; - timeoutMs: number; - warnings: string[]; - blockingIssues: string[]; -} - -export interface CheckCodexProviderReadinessOptions { - env?: NodeJS.ProcessEnv; - nodeVersion?: string; - resolvePackage?: (specifier: string) => string; - fileExists?: (path: string) => boolean; - readFile?: (path: string) => string; -} - -const requireFromHere = createRequire(import.meta.url); - -function resolveInstalledPackage(specifier: string) { - try { - return requireFromHere.resolve(specifier); - } catch (error) { - const esmResolve = (import.meta as ImportMeta & { resolve?: (specifier: string) => string }).resolve; - if (esmResolve) { - try { - return esmResolve(specifier); - } catch { - // Fall back to package-root candidates below. - } - } - const candidates = [ - process.cwd(), - resolve(process.cwd(), '..'), - resolve(process.cwd(), '../..'), - ]; - for (const candidate of candidates) { - try { - return createRequire(resolve(candidate, 'package.json')).resolve(specifier); - } catch { - // Try the next workspace/package root. - } - } - throw error; - } -} - -const SUBSCRIPTION_PLANS = new Set([ - 'plus', - 'pro', - 'business', - 'edu', - 'enterprise', - 'unknown', -]); -const APPROVAL_POLICIES = new Set(['never', 'on_request', 'always']); -const SANDBOX_MODES = new Set(['read_only', 'workspace_write']); - -function readChoice(value: string | undefined, allowed: Set, fallback: T): T { - const normalized = value?.trim().toLowerCase().replaceAll('-', '_'); - return normalized && allowed.has(normalized as T) ? (normalized as T) : fallback; -} - -function readBoolean(value: string | undefined, fallback: boolean) { - if (!value) return fallback; - const normalized = value.trim().toLowerCase(); - if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; - if (['0', 'false', 'no', 'off'].includes(normalized)) return false; - return fallback; -} - -function readPositiveInteger(value: string | undefined, fallback: number) { - const parsed = Number.parseInt(value ?? '', 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; -} - -function parseNodeMajor(version: string) { - const normalized = version.trim().replace(/^v/, ''); - const [major] = normalized.split('.'); - return Number.parseInt(major ?? '', 10); -} - -export function resolveCodexProviderConfig(env: NodeJS.ProcessEnv = process.env): CodexProviderConfig { - return { - providerId: 'codex', - subscriptionPlan: readChoice(env.TREESEED_CODEX_SUBSCRIPTION_PLAN, SUBSCRIPTION_PLANS, 'unknown'), - defaultModel: env.TREESEED_CODEX_DEFAULT_MODEL?.trim() || 'gpt-5.5', - approvalPolicy: readChoice(env.TREESEED_CODEX_APPROVAL_POLICY, APPROVAL_POLICIES, 'never'), - sandboxMode: readChoice(env.TREESEED_CODEX_SANDBOX_MODE, SANDBOX_MODES, 'workspace_write'), - timeoutMs: readPositiveInteger(env.TREESEED_CODEX_TIMEOUT_MS, 900_000), - requireDecisionForProductionRelease: readBoolean(env.TREESEED_CODEX_REQUIRE_RELEASE_DECISION, true), - allowFeatureBranchMutation: readBoolean(env.TREESEED_CODEX_ALLOW_FEATURE_BRANCH_MUTATION, true), - allowAutomaticStagingMerge: readBoolean(env.TREESEED_CODEX_ALLOW_AUTOMATIC_STAGING_MERGE, true), - requireAllowedPaths: readBoolean(env.TREESEED_CODEX_REQUIRE_ALLOWED_PATHS, true), - recordThreadIds: readBoolean(env.TREESEED_CODEX_RECORD_THREAD_IDS, true), - }; -} - -export function checkCodexProviderReadiness( - options: CheckCodexProviderReadinessOptions = {}, -): CodexProviderReadiness { - const env = options.env ?? process.env; - const config = resolveCodexProviderConfig(env); - const resolvePackage = options.resolvePackage ?? resolveInstalledPackage; - const fileExists = options.fileExists ?? existsSync; - const readFile = options.readFile ?? ((path: string) => readFileSync(path, 'utf8')); - const providerSelected = [ - env.TREESEED_EXECUTION_PROVIDER, - env.TREESEED_AGENT_EXECUTION_PROVIDER, - ].every((value) => value === undefined) - || [ - env.TREESEED_EXECUTION_PROVIDER, - env.TREESEED_AGENT_EXECUTION_PROVIDER, - ].some((value) => value?.trim() === config.providerId); - const warnings: string[] = []; - const blockingIssues: string[] = []; - const nodeMajor = parseNodeMajor(options.nodeVersion ?? process.version); - const nodeVersionOk = Number.isFinite(nodeMajor) && nodeMajor >= 22; - - let sdkInstalled = false; - let packagePath: string | undefined; - try { - packagePath = resolvePackage('@openai/codex-sdk'); - sdkInstalled = true; - } catch { - if (providerSelected) { - blockingIssues.push('@openai/codex-sdk is required when codex is the selected execution provider.'); - } else { - warnings.push('@openai/codex-sdk is not installed; Codex execution remains unavailable until the provider is selected and installed.'); - } - } - - if (!nodeVersionOk) { - blockingIssues.push('Codex provider support requires Node.js 22 or newer in the TreeSeed agent runtime.'); - } - - const authPath = resolveCodexAuthFile(env); - const authJsonDetected = fileExists(authPath); - const apiKeyDetected = Boolean(env.TREESEED_CODEX_API_KEY?.trim()); - const authDetected = authJsonDetected || apiKeyDetected; - const authMode = authJsonDetected ? 'codex_auth_json' : apiKeyDetected ? 'api_key' : 'missing'; - if (!authDetected) { - const message = `Codex authentication was not detected. For subscription-backed Codex, run Codex login so ${authPath} exists; alternatively set TREESEED_CODEX_API_KEY as an API-billed fallback from https://platform.openai.com/api-keys. Never commit or print either credential.`; - if (providerSelected) { - blockingIssues.push(message); - } else { - warnings.push(message); - } - } - const configPath = join(dirname(authPath), 'config.toml'); - if (fileExists(configPath)) { - const source = (() => { - try { - return readFile(configPath); - } catch { - return ''; - } - })(); - if (/^\s*service_tier\s*=\s*["']default["']/imu.test(source)) { - warnings.push('Codex config service_tier=default is not accepted by this SDK; live tests will use an isolated sanitized CODEX_HOME.'); - } - } - - return { - ok: blockingIssues.length === 0, - providerSelected, - sdkInstalled, - nodeVersionOk, - authDetected, - authMode, - authPath, - packagePath, - subscriptionPlan: config.subscriptionPlan, - defaultModel: config.defaultModel, - approvalPolicy: config.approvalPolicy, - sandboxMode: config.sandboxMode, - timeoutMs: config.timeoutMs, - warnings, - blockingIssues, - }; -} diff --git a/src/agents/adapters/codex/execution-codex-core.ts b/src/agents/adapters/codex/execution-codex-core.ts deleted file mode 100644 index 8f926c2b..00000000 --- a/src/agents/adapters/codex/execution-codex-core.ts +++ /dev/null @@ -1,491 +0,0 @@ -import { isAbsolute, relative } from 'node:path'; -import { findAgentToolDefinition } from '@treeseed/sdk'; -import type { ExecutionProviderToolDescriptor } from '../../runtime/runtime-types.ts'; -import { createAgentToolMcpServerCommand } from '../../tools/agent-tool-mcp-server.ts'; -import { agentToolMcpName } from '../../tools/agent-tool-mcp-server.ts'; -import { - type CodexApprovalPolicy, - type CodexSandboxMode, - resolveCodexProviderConfig, -} from './codex-readiness.ts'; -import { codexClientEnvironment } from '../accounts/codex-auth.ts'; -import { createIsolatedCodexRuntimeHome } from '../runtime/codex-runtime-home.ts'; -import type { ResearchSourcePolicy } from '@treeseed/sdk/agent-capacity'; -import { codexDeadlineContract } from './execution-codex-deadline.ts'; -export { hasCompletedToolEvent, readToolTelemetry, treeDxContentReceipts } from './execution-codex-receipts.ts'; - -export type CodexExecutionStatus = 'completed' | 'waiting' | 'failed'; -export type CodexReasoningEffort = 'low' | 'medium' | 'high'; -export type { CodexSandboxMode } from './codex-readiness.ts'; - -export interface CodexExecutionRequest { - taskId: string; - workDayId?: string; - agentSlug: string; - repoRoot: string; - worktreeRoot?: string; - prompt: string; - threadId?: string; - approvalId?: string; - allowedPaths: string[]; - forbiddenPaths: string[]; - sandboxMode: CodexSandboxMode; - approvalPolicy: CodexApprovalPolicy; - model?: string; - reasoningEffort?: CodexReasoningEffort; - timeoutMs?: number; - tools?: ExecutionProviderToolDescriptor[]; - leaseToken?: string | null; - toolTelemetryPath?: string | null; - toolConfigPath?: string | null; - providerAccessToken?: string; - apiBaseUrl?: string; - researchSourcePolicy?: ResearchSourcePolicy; - metadata?: Record; -} - -export interface CodexExecutionResult { - provider: 'codex'; - threadId: string; - status: CodexExecutionStatus; - finalResponse?: string; - summary?: string; - changedPaths: string[]; - proposedCommands: string[]; - verificationHints: string[]; - rawEventRefs?: string[]; - error?: { - code: string; - message: string; - retryable: boolean; - }; - usage?: { - subscriptionPlan?: string; - estimatedSeconds?: number; - wallMs?: number; - wallMinutes?: number; - nativeUnit?: 'wall_minute'; - inputTokens?: number | null; - outputTokens?: number | null; - cachedInputTokens?: number | null; - filesChanged?: number; - }; - metadata?: Record; -} - -export interface CodexClient { - startThread(options?: CodexThreadOptions): CodexThread; - resumeThread(id: string, options?: CodexThreadOptions): CodexThread; - cleanup?(): Promise; -} - -export interface CodexThread { - id?: string | null; - run(input: string, options?: Record): Promise; - runStreamed?(input: string, options?: Record): Promise<{ events: AsyncIterable> }>; -} - -export interface CodexThreadOptions { - model?: string; - sandboxMode?: 'read-only' | 'workspace-write'; - workingDirectory?: string; - skipGitRepoCheck?: boolean; - modelReasoningEffort?: 'low' | 'medium' | 'high'; - approvalPolicy?: 'never' | 'on-request'; -} - -export interface CodexRunResult { - items?: unknown[]; - finalResponse?: string; - usage?: { - input_tokens?: number; - cached_input_tokens?: number; - output_tokens?: number; - reasoning_output_tokens?: number; - } | null; -} - -export interface RunCodexTaskOptions { - signal?: AbortSignal; - createCodexClient?: (request?: CodexExecutionRequest) => CodexClient | Promise; - client?: CodexClient | Promise; - now?: () => number; - onEvent?: (event: Record) => void | Promise; -} - -export interface PreparedCodexWorktree { - branchName: string; - worktreeRoot: string; - exactBaseRef?: string; - created?: boolean; -} - -export class CodexRequestSafetyError extends Error { - constructor( - readonly code: string, - message: string, - readonly retryable = false, - ) { - super(message); - this.name = 'CodexRequestSafetyError'; - } -} - -function normalizePath(value: string) { - return value.replace(/\\/gu, '/').replace(/^\.?\//u, '').replace(/\/+/gu, '/'); -} - -export function normalizeChangedPath(value: string, root: string) { - if (!isAbsolute(value)) return normalizePath(value); - const relativePath = relative(root, value); - if (!relativePath || relativePath.startsWith('..') || isAbsolute(relativePath)) return normalizePath(value); - return normalizePath(relativePath); -} - -export class CodexExecutionTimeoutError extends Error { - constructor(readonly timeoutMs: number) { - super(`Codex execution exceeded the configured timeout of ${timeoutMs}ms.`); - this.name = 'CodexExecutionTimeoutError'; - } -} - -export async function withTimeout(promise: Promise, timeoutMs: number, onTimeout?: () => void): Promise { - if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise; - let timer: ReturnType | null = null; - const timeout = new Promise((_, reject) => { - timer = setTimeout(() => { - onTimeout?.(); - reject(new CodexExecutionTimeoutError(timeoutMs)); - }, timeoutMs); - }); - try { - return await Promise.race([promise, timeout]); - } finally { - if (timer) clearTimeout(timer); - } -} - -function matchesPattern(path: string, pattern: string) { - const normalizedPath = normalizePath(path); - const normalizedPattern = normalizePath(pattern); - if (normalizedPattern === '**' || normalizedPattern === '*') { - return true; - } - if (normalizedPattern.endsWith('/**')) { - const prefix = normalizedPattern.slice(0, -3); - return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`); - } - if (normalizedPattern.endsWith('/')) { - return normalizedPath.startsWith(normalizedPattern); - } - return normalizedPath === normalizedPattern || normalizedPath.startsWith(`${normalizedPattern}/`); -} - -export function isChangedPathAllowed(path: string, allowedPaths: string[], forbiddenPaths: string[]) { - if (forbiddenPaths.some((pattern) => matchesPattern(path, pattern))) { - return false; - } - return allowedPaths.length === 0 || allowedPaths.some((pattern) => matchesPattern(path, pattern)); -} - -export function validateCodexExecutionRequest(request: CodexExecutionRequest) { - if (!request.taskId.trim()) { - throw new CodexRequestSafetyError('task_id_required', 'Codex execution requires a task id.'); - } - if (!request.agentSlug.trim()) { - throw new CodexRequestSafetyError('agent_slug_required', 'Codex execution requires an agent slug.'); - } - if (!request.repoRoot.trim()) { - throw new CodexRequestSafetyError('repo_root_required', 'Codex execution requires a repository root.'); - } - if (request.sandboxMode === 'workspace_write') { - if (request.allowedPaths.length === 0) { - throw new CodexRequestSafetyError( - 'allowed_paths_required', - 'Codex workspace-write execution requires at least one allowed path.', - true, - ); - } - if (!request.worktreeRoot?.trim()) { - throw new CodexRequestSafetyError( - 'worktree_required', - 'Codex workspace-write execution requires an assigned worktree root.', - true, - ); - } - } -} - -export function safetyResult(request: CodexExecutionRequest, error: CodexRequestSafetyError): CodexExecutionResult { - return { - provider: 'codex', - threadId: request.threadId ?? '', - status: error.retryable ? 'waiting' : 'failed', - summary: error.message, - changedPaths: [], - proposedCommands: [], - verificationHints: [], - error: { - code: error.code, - message: error.message, - retryable: error.retryable, - }, - }; -} - -function agentTools(request: CodexExecutionRequest) { - return (request.tools ?? []).filter((tool) => tool.kind === 'agent_tool'); -} - -export function codexAgentToolServer(request: CodexExecutionRequest) { - const tools = agentTools(request); - if (tools.length === 0) return null; - const assignmentId = String(request.metadata?.assignment && typeof request.metadata.assignment === 'object' - ? (request.metadata.assignment as Record).id ?? '' - : ''); - return createAgentToolMcpServerCommand({ - apiBaseUrl: request.apiBaseUrl ?? '', - providerAccessToken: request.providerAccessToken ?? '', - assignmentId, - leaseToken: request.leaseToken ?? null, - repoRoot: request.repoRoot, - telemetryPath: request.toolTelemetryPath ?? null, - descriptors: tools, - researchSourcePolicy: request.researchSourcePolicy, - }); -} - -export function codexAgentToolEnvironment(request: CodexExecutionRequest) { - const server = codexAgentToolServer(request); - if (!server) return {}; - return request.toolConfigPath - ? { TREESEED_AGENT_TOOL_CONFIG_PATH: request.toolConfigPath } - : server.env; -} - -function redactToolForPrompt(tool: ExecutionProviderToolDescriptor) { - const metadata = tool.metadata && typeof tool.metadata === 'object' && !Array.isArray(tool.metadata) - ? tool.metadata as Record - : {}; - return { - kind: tool.kind, - id: tool.id, - name: tool.name, - description: tool.description, - callName: agentToolMcpName(tool.id), - executionTarget: tool.executionTarget, - mutability: tool.mutability, - contentAction: metadata.contentAction ?? undefined, - contentModel: metadata.contentModel ?? undefined, - contentPreset: metadata.contentPreset ?? undefined, - permissionSummary: metadata.permissionSummary ?? undefined, - inputSchema: tool.inputSchema, - }; -} - -export function codexAgentToolConfig(request: CodexExecutionRequest) { - const projectRoot = request.sandboxMode === 'workspace_write' - ? request.worktreeRoot ?? request.repoRoot - : request.repoRoot; - const baseConfig = { - projects: { - [projectRoot]: { - trust_level: 'trusted', - }, - }, - } as Record; - const configuredServiceTier = process.env.TREESEED_CODEX_SERVICE_TIER?.trim(); - if (configuredServiceTier === 'fast') baseConfig.service_tier = 'fast'; - const server = codexAgentToolServer(request); - if (!server) return baseConfig; - return { - ...baseConfig, - features: { - builtin_mcp: true, - }, - mcp_servers: { - treeseed_tools: { - command: server.command, - args: server.args, - env: codexAgentToolEnvironment(request), - required: true, - startup_timeout_sec: 90, - default_tools_approval_mode: 'approve', - }, - }, - }; -} - -export async function createDefaultCodexClient(request?: CodexExecutionRequest, env: NodeJS.ProcessEnv = process.env): Promise { - const { Codex } = await import('@openai/codex-sdk'); - const runtimeHome = await createIsolatedCodexRuntimeHome({ - serviceTier: env.TREESEED_CODEX_SERVICE_TIER?.trim() === 'fast' ? 'fast' : undefined, - model: request?.model, - env, - }); - const client = new Codex({ - env: codexClientEnvironment({ - ...env, - TREESEED_CODEX_AUTH_FILE: runtimeHome.authFile, - CODEX_HOME: runtimeHome.codexHome, - }), - config: request ? codexAgentToolConfig(request) : undefined, - } as ConstructorParameters[0]); - let cleaned = false; - return { - startThread(options) { - return client.startThread(options); - }, - resumeThread(id, options) { - return client.resumeThread(id, options); - }, - async cleanup() { - if (cleaned) return; - cleaned = true; - await runtimeHome.cleanup(); - }, - }; -} - -export function mapCodexThreadOptions(request: CodexExecutionRequest): CodexThreadOptions { - const workingDirectory = request.sandboxMode === 'workspace_write' - ? request.worktreeRoot ?? request.repoRoot - : request.repoRoot; - return { - model: request.model, - sandboxMode: request.sandboxMode === 'workspace_write' ? 'workspace-write' : 'read-only', - workingDirectory, - skipGitRepoCheck: true, - modelReasoningEffort: request.reasoningEffort, - approvalPolicy: request.approvalPolicy === 'never' ? 'never' : 'on-request', - }; -} - -function formatList(values: string[]) { - return values.length > 0 ? values.map((value) => `- ${value}`).join('\n') : '- '; -} - -function formatMetadataBlock(value: unknown) { - if (value === undefined || value === null) return ''; - if (typeof value === 'string') return value.trim() || ''; - try { - return JSON.stringify(value, null, 2); - } catch { - return String(value); - } -} - -export function buildCodexPrompt(request: CodexExecutionRequest) { - // A resumed Codex thread already contains the immutable assignment boundary, - // tool catalog, and work package. Repeating them consumes the context window - // and can make a bounded completion-correction turn larger than the original. - if (request.threadId) return request.prompt.trim(); - const permissionStage = request.sandboxMode === 'workspace_write' - ? 'approved_worktree_mutation' - : 'read_only_or_planning'; - const contextPackSummary = request.metadata?.contextPackSummary ?? request.metadata?.contextSummary; - const rawWorkPackage = request.metadata?.workPackage ?? request.metadata?.workPackageYaml; - const workPackage = rawWorkPackage && typeof rawWorkPackage === 'object' && !Array.isArray(rawWorkPackage) - ? Object.fromEntries(Object.entries(rawWorkPackage as Record) - .filter(([key]) => key !== 'instructions' && key !== 'context')) - : rawWorkPackage; - const operationBoundary = request.sandboxMode === 'workspace_write' - ? [ - '- Use only the tools listed in the available TreeSeed tool catalog.', - '- Do not stage shared branches unless an explicit stage tool is present.', - '- Do not close the task directly unless an explicit close tool is present.', - '- Do not release unless an explicit release tool is present.', - ].join('\n') - : '- Treat this as read-only/planning unless the handler grants a later mutation stage.'; - const tools = agentTools(request); - const toolIds = new Set(tools.map((tool) => tool.id)); - const deadlineContract = codexDeadlineContract(request.metadata?.executionTiming, toolIds.has('treeseed.status')); - const artifactKind = typeof request.metadata?.workPackage === 'object' - && request.metadata.workPackage - && typeof (request.metadata.workPackage as { metadata?: { artifactKind?: unknown } }).metadata?.artifactKind === 'string' - ? (request.metadata.workPackage as { metadata: { artifactKind: string } }).metadata.artifactKind - : null; - const sourceCheckpointRequired = artifactKind === 'failing_test_proof' - || artifactKind === 'implementation_change' - || artifactKind === 'implementation_revision'; - const completionGates = [ - ...(toolIds.has('treeseed.verify') ? [ - '- Verification gate: call treeseed.verify with the exact bounded node/npm argv and wait for a successful verification_completed receipt. For a test-first red proof, set expectedExitCode to the behaviorally correct failing exit code.', - ] : []), - ...(toolIds.has('treeseed.checkpoint') ? [ - sourceCheckpointRequired - ? `- Source checkpoint gate: the ${artifactKind} deliverable requires treeseed.checkpoint after verification and a successful source_checkpoint_committed receipt containing commitSha.` - : '- Source checkpoint gate: if you create or change any repository file, call treeseed.checkpoint after verification and wait for a successful source_checkpoint_committed receipt containing commitSha.', - '- A final response is not completion while changed repository files lack a successful checkpoint receipt. If checkpoint fails, correct the scoped cause and retry it; otherwise report the exact tool failure as blocked.', - ] : []), - ...(toolIds.has('treeseed.review_decision') ? [ - '- Review disposition gate: after evaluating the governed evidence, call treeseed.review_decision with exactly approved or rejected and wait for a successful review_decision_recorded receipt before final response.', - ] : []), - ...(request.metadata?.workPackage - && typeof (request.metadata.workPackage as { metadata?: { researchStage?: unknown } }).metadata?.researchStage === 'string' - && (request.metadata.workPackage as { metadata: { researchStage: string } }).metadata.researchStage === 'independent-source-fetch' - && toolIds.has('research.fetch_source') ? [ - `- Research fetch gate: call the available TreeSeed tool with callName ${agentToolMcpName('research.fetch_source')} (policy id research.fetch_source) for at least two independent allowed publishers and wait for a successful research_citation_fetched receipt from each before final response. Search for and invoke the callName, not the dotted policy id.`, - ] : []), - ]; - const toolSection = tools.length > 0 - ? [ - 'Available TreeSeed tools:', - 'Use only these assignment-scoped tools when tool use is needed.', - 'Do not request raw TreeDX URLs, bearer tokens, GitHub tokens, deploy keys, provider API keys, or direct repository credentials.', - 'Do not use shell as a substitute for missing TreeSeed tools.', - 'Local shell reads are reserved for repository files in the assigned workspace; Knowledge Hub model instances must be accessed through TreeDX tools.', - 'Never inspect src/content or docs/src/content with shell commands; those paths are Knowledge Hub content and TreeDX is their only access authority.', - 'When a model-aware TreeSeed content tool is available, use it instead of hand-writing Knowledge Hub frontmatter or markdown files.', - 'Do not infer or guess Knowledge Hub file paths. Discover records through TreeSeed content queries or TreeDX graph/search tools, then reuse only exact paths returned by those tools.', - 'Batch file reads fail when any requested path is absent; include only paths already verified by a query, graph, search, or content receipt.', - 'For hierarchically placed content, pass the exact repository-relative path from the content receipt when reading, updating, linking, or validating that record.', - 'Content tools return staged TreeDX workspace changes unless a commit-capable content tool is explicitly listed.', - 'If a needed tool is absent or returns a structured scope error, adapt the work or report the missing capability in the final response.', - formatMetadataBlock(tools.map(redactToolForPrompt)), - ].join('\n') - : 'Available TreeSeed tools:\nNo assignment-scoped tools were provided; use only repository/work package context and report blocked work with the exact missing capability.'; - const taskPrompt = [ - 'You are operating as a TreeSeed implementation agent.', - '', - 'Goal:', - request.prompt, - '', - 'Current permission stage:', - permissionStage, - '', - `Task id: ${request.taskId}`, - `Workday id: ${request.workDayId ?? ''}`, - `Agent slug: ${request.agentSlug}`, - `Repository root: ${request.repoRoot}`, - `Assigned worktree root: ${request.worktreeRoot ?? ''}`, - '', - 'Allowed paths:', - formatList(request.allowedPaths), - '', - 'Forbidden paths:', - formatList(request.forbiddenPaths), - '', - 'Required behavior:', - ...deadlineContract, - '- Do not modify files outside allowed paths.', - '- Do not write outside the assigned git worktree.', - operationBoundary, - '- Do not approve decisions.', - '- Prefer small, reviewable changes.', - '- Run or suggest verification that is relevant to the change.', - '- Report uncertainty.', - '- Record commands you ran or believe should be run.', - '- If the task requires broader scope or a missing tool, stop with a clear blocked summary that names the missing permission or tool.', - ...completionGates, - '', - toolSection, - '', - 'Context pack:', - formatMetadataBlock(contextPackSummary), - '', - 'Work package:', - formatMetadataBlock(workPackage), - ].join('\n'); - return taskPrompt; -} diff --git a/src/agents/adapters/codex/execution-codex-deadline.ts b/src/agents/adapters/codex/execution-codex-deadline.ts deleted file mode 100644 index 19685b94..00000000 --- a/src/agents/adapters/codex/execution-codex-deadline.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function codexDeadlineContract(timingValue: unknown, hasStatusTool: boolean) { - if (!hasStatusTool) return []; - const timing = timingValue && typeof timingValue === 'object' && !Array.isArray(timingValue) - ? timingValue as Record : {}; - const closeoutWarningSeconds = Number(timing.closeoutWarningSeconds) > 0 ? Number(timing.closeoutWarningSeconds) : 180; - return [ - 'Assignment time contract:', - `- Productive execution allocation: ${typeof timing.executionSeconds==='number'?`${timing.executionSeconds} seconds`:''}. Preparation and closeout do not consume it.`, - `- Assignment custody deadline: ${typeof timing.closeoutDeadlineAt === 'string'?timing.closeoutDeadlineAt:typeof timing.deadlineAt === 'string' ? timing.deadlineAt : ''}.`, - `- Protected closeout allocation: ${typeof timing.closeoutSeconds==='number'?timing.closeoutSeconds:closeoutWarningSeconds} seconds.`, - '- Call treeseed_status immediately, after every major phase, and before a long-running command or mutation. Treat its phase and phase-specific remaining time as authoritative.', - '- Preparation is bounded but outside productive execution. Resolve admitted dynamic context and write the mandatory initial assignment plan; its authoritative read-back starts the execution clock.', - '- When shouldCloseOut=true, stop new exploration and scope expansion. Preserve coherent work: validate and commit content exactly once, or run only verification that fits and create a source checkpoint. Record exact artifact/checkpoint refs, verification state, remaining scope, blockers, and resume instructions before final response.', - '- If useful scope remains and proposal creation is authorized, a Zod-valid project proposal may request more capacity. It never changes the current deadline or approves itself. Otherwise report extensionRequested=true, requested seconds, and the evidence-based reason.', - ]; -} diff --git a/src/agents/adapters/codex/execution-codex-receipts.ts b/src/agents/adapters/codex/execution-codex-receipts.ts deleted file mode 100644 index 5d917555..00000000 --- a/src/agents/adapters/codex/execution-codex-receipts.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { readFile } from 'node:fs/promises'; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export async function readToolTelemetry(path: string | null) { - if (!path) return []; - const source = await readFile(path, 'utf8').catch(() => ''); - return source.split(/\r?\n/u).filter(Boolean).flatMap((line) => { - try { return [JSON.parse(line) as Record]; } catch { return []; } - }); -} - -export function hasCompletedToolEvent(entries: Record[], eventType: string) { - return entries.some((entry) => entry.status === 'completed' - && Array.isArray(entry.derivedEvents) - && entry.derivedEvents.some((event) => record(event).type === eventType)); -} - -export function treeDxContentReceipts(entries: Record[]) { - const completedEvents = entries.flatMap((entry) => entry.status === 'completed' && Array.isArray(entry.derivedEvents) - ? entry.derivedEvents.map(record) - : []); - const commitEvent = [...completedEvents].reverse().find((event) => event.type === 'content_committed'); - const mutations = new Map; telemetry: Record }>(); - for (const entry of entries) { - if (entry.status !== 'completed' || !Array.isArray(entry.derivedEvents)) continue; - for (const rawEvent of entry.derivedEvents) { - const event = record(rawEvent); - if (event.type !== 'content_created' && event.type !== 'content_updated') continue; - const ref = record(event.contentRef); - const contentPath = String(ref.contentPath ?? ref.path ?? ref.uri ?? ''); - if (!contentPath) continue; - const current = mutations.get(contentPath); - mutations.set(contentPath, { - ref: Object.fromEntries([ - ...Object.entries(current?.ref ?? {}), - ...Object.entries(ref).filter(([, value]) => value !== undefined && value !== null && value !== ''), - ]), - telemetry: entry, - }); - } - } - return [...mutations].flatMap(([contentPath, mutation]) => { - const localCommitSha=typeof mutation.ref.commitSha==='string'?mutation.ref.commitSha:''; - if(!localCommitSha&&!commitEvent)return []; - const enrichedRef = { - ...mutation.ref, - ...(!localCommitSha&&typeof commitEvent?.commitSha === 'string' ? { commitSha: commitEvent.commitSha } : {}), - ...(typeof mutation.ref.ref!=='string'&&typeof commitEvent?.branchRef === 'string' ? { ref: commitEvent.branchRef } : {}), - }; - return [{ - kind: 'treedx_content_receipt', - name: contentPath || String(mutation.ref.id ?? mutation.ref.slug ?? 'content'), - uri: `treedx://${contentPath}`, - mediaType: 'application/vnd.treeseed.content-ref+json', - metadata: { toolId: mutation.telemetry.toolId, contentRef: enrichedRef, telemetry: mutation.telemetry }, - }]; - }); -} diff --git a/src/agents/adapters/codex/execution-codex-redaction.ts b/src/agents/adapters/codex/execution-codex-redaction.ts deleted file mode 100644 index 689963b5..00000000 --- a/src/agents/adapters/codex/execution-codex-redaction.ts +++ /dev/null @@ -1,20 +0,0 @@ -const SENSITIVE_KEY = /(?:authorization|cookie|credential|password|private[_-]?key|secret|(?:access|auth|refresh|api|session)[_-]?token|token$)/iu; -const SENSITIVE_VALUE = /(?:Bearer\s+|(?:sk|gh[opsu]|github_pat|tsk)_[A-Za-z0-9_-]{8,})[^\s"']*/giu; - -function sanitizeText(value: string) { - return value.replace(SENSITIVE_VALUE, ''); -} - -export function redactCodexTraceValue(value: unknown): unknown { - if (typeof value === 'string') return sanitizeText(value); - if (Array.isArray(value)) return value.map(redactCodexTraceValue); - if (!value || typeof value !== 'object') return value; - return Object.fromEntries(Object.entries(value as Record).map(([key, entry]) => [ - key, - SENSITIVE_KEY.test(key) ? '' : redactCodexTraceValue(entry), - ])); -} - -export function redactCodexTraceRecord(value: Record) { - return redactCodexTraceValue(value) as Record; -} diff --git a/src/agents/adapters/codex/execution-codex-result.ts b/src/agents/adapters/codex/execution-codex-result.ts deleted file mode 100644 index 91ed28d7..00000000 --- a/src/agents/adapters/codex/execution-codex-result.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { - CodexExecutionTimeoutError, - CodexRequestSafetyError, - buildCodexPrompt, - createDefaultCodexClient, - isChangedPathAllowed, - mapCodexThreadOptions, - normalizeChangedPath, - safetyResult, - validateCodexExecutionRequest, - withTimeout, - type CodexExecutionRequest, - type CodexExecutionResult, - type CodexRunResult, - type CodexClient, - type RunCodexTaskOptions, -} from './execution-codex-core.ts'; -import { runCodexThread } from './execution-codex-stream.ts'; -import { withCodexChildPipeGuard } from './codex-child-pipe-guard.ts'; - -function isRecord(value: unknown): value is Record { - return value !== null && typeof value === 'object' && !Array.isArray(value); -} - -function stringValue(value: unknown) { - return typeof value === 'string' ? value : undefined; -} - -function extractCommand(item: Record) { - return item.type === 'command_execution' ? stringValue(item.command) : undefined; -} - -function extractFileChangePaths(item: Record) { - if (item.type !== 'file_change' || !Array.isArray(item.changes)) return []; - return item.changes - .map((change) => isRecord(change) ? stringValue(change.path) : undefined) - .filter((path): path is string => Boolean(path)); -} - -function commandLooksLikeVerification(command: string) { - return /\b(test|check|build|verify|lint|vitest|jest)\b|npm\s+(run\s+)?test/u.test(command); -} - -function unique(values: string[]) { - return [...new Set(values)]; -} - -function failedScopeResult(input: { - request: CodexExecutionRequest; - threadId: string; - finalResponse: string; - changedPaths: string[]; - proposedCommands: string[]; - verificationHints: string[]; - rawEventRefs: string[]; - wallMs: number; - violatingPath: string; -}) { - return { - provider: 'codex', - threadId: input.threadId, - status: 'failed', - finalResponse: input.finalResponse, - summary: `${input.violatingPath} is outside the approved Codex mutation scope.`, - changedPaths: input.changedPaths, - proposedCommands: input.proposedCommands, - verificationHints: input.verificationHints, - rawEventRefs: input.rawEventRefs, - error: { - code: 'changed_path_scope_violation', - message: `${input.violatingPath} is outside the approved Codex mutation scope.`, - retryable: true, - }, - usage: { - subscriptionPlan: String(input.request.metadata?.subscriptionPlan ?? ''), - wallMs: input.wallMs, - wallMinutes: input.wallMs / 60_000, - nativeUnit: 'wall_minute', - filesChanged: input.changedPaths.length, - }, - metadata: { - allowedPaths: input.request.allowedPaths, - forbiddenPaths: input.request.forbiddenPaths, - violatingPath: input.violatingPath, - }, - } satisfies CodexExecutionResult; -} - -export function normalizeCodexRunResult(input: { - request: CodexExecutionRequest; - result: CodexRunResult; - threadId: string; - wallMs: number; -}): CodexExecutionResult { - const items = input.result.items?.filter(isRecord) ?? []; - const prompt = buildCodexPrompt(input.request); - const threadOptions = mapCodexThreadOptions(input.request); - const rawEventRefs = unique(items.map((item) => stringValue(item.id)).filter((id): id is string => Boolean(id))); - const proposedCommands = unique(items.map(extractCommand).filter((command): command is string => Boolean(command))); - const pathRoot = input.request.worktreeRoot ?? input.request.repoRoot; - const changedPaths = unique(items.flatMap(extractFileChangePaths).map((path) => normalizeChangedPath(path, pathRoot))); - const verificationHints = unique(proposedCommands.filter(commandLooksLikeVerification)); - const finalResponse = input.result.finalResponse ?? ''; - const summary = finalResponse.split('\n').find((line) => line.trim())?.trim() || 'Codex SDK task completed.'; - - if (input.request.sandboxMode === 'workspace_write') { - const violatingPath = changedPaths.find((path) => !isChangedPathAllowed( - path, - input.request.allowedPaths, - input.request.forbiddenPaths, - )); - if (violatingPath) { - return failedScopeResult({ - request: input.request, - threadId: input.threadId, - finalResponse, - changedPaths, - proposedCommands, - verificationHints, - rawEventRefs, - wallMs: input.wallMs, - violatingPath, - }); - } - } - - return { - provider: 'codex', - threadId: input.threadId, - status: 'completed', - finalResponse, - summary, - changedPaths, - proposedCommands, - verificationHints, - rawEventRefs, - usage: { - subscriptionPlan: String(input.request.metadata?.subscriptionPlan ?? ''), - estimatedSeconds: undefined, - wallMs: input.wallMs, - wallMinutes: input.wallMs / 60_000, - nativeUnit: 'wall_minute', - inputTokens: input.result.usage?.input_tokens ?? null, - outputTokens: input.result.usage?.output_tokens ?? null, - cachedInputTokens: input.result.usage?.cached_input_tokens ?? null, - filesChanged: changedPaths.length, - }, - metadata: { - usage: input.result.usage ?? null, - rawItems: items, - request: { - model: input.request.model ?? null, - reasoningEffort: input.request.reasoningEffort ?? null, - sandboxMode: input.request.sandboxMode, - approvalPolicy: input.request.approvalPolicy, - timeoutMs: input.request.timeoutMs ?? null, - threadOptions, - allowedPaths: input.request.allowedPaths, - forbiddenPaths: input.request.forbiddenPaths, - toolCount: input.request.tools?.length ?? 0, - tools: input.request.tools ?? [], - promptCharacters: prompt.length, - prompt, - }, - }, - }; -} - -export async function runCodexTask( - request: CodexExecutionRequest, - options: RunCodexTaskOptions = {}, -): Promise { - const startedAt = options.now?.() ?? Date.now(); - let ownedClient: CodexClient | null = null; - let activeRun: Promise | null = null; - let activeThreadId = request.threadId ?? ''; - const joinActiveRun = async () => { - const running = activeRun; - if (running) await running.catch(() => undefined); - }; - const cancellation = new AbortController(); - const cancelFromAuthority = () => cancellation.abort(options.signal?.reason); - if (options.signal?.aborted) cancelFromAuthority(); - else options.signal?.addEventListener('abort', cancelFromAuthority, { once: true }); - try { - validateCodexExecutionRequest(request); - } catch (error) { - if (error instanceof CodexRequestSafetyError) { - return safetyResult(request, error); - } - throw error; - } - - try { - const createCodexClient = options.createCodexClient ?? createDefaultCodexClient; - const client = await (options.client ?? createCodexClient(request)); - if (!options.client) ownedClient = client; - const threadOptions = mapCodexThreadOptions(request); - const thread = request.threadId - ? client.resumeThread(request.threadId, threadOptions) - : client.startThread(threadOptions); - activeThreadId = thread.id ?? request.threadId ?? ''; - const prompt = buildCodexPrompt(request); - activeRun = withCodexChildPipeGuard(() => runCodexThread( - thread, - prompt, - options.onEvent, - cancellation.signal, - (threadId) => { activeThreadId = threadId; }, - )); - activeRun.catch(() => null); - const result = await withTimeout(activeRun, request.timeoutMs ?? 900_000, () => cancellation.abort()); - const wallMs = (options.now?.() ?? Date.now()) - startedAt; - return normalizeCodexRunResult({ - request, - result, - threadId: thread.id ?? request.threadId ?? '', - wallMs, - }); - } catch (error) { - if (options.signal?.aborted) { - await joinActiveRun(); - const message = options.signal.reason instanceof Error ? options.signal.reason.message : 'Assignment execution authority was revoked.'; - return { - provider: 'codex', threadId: request.threadId ?? '', status: 'failed', summary: message, - changedPaths: [], proposedCommands: [], verificationHints: [], - error: { code: 'codex_execution_authority_revoked', message, retryable: false }, - }; - } - if (error instanceof CodexExecutionTimeoutError) { - cancellation.abort(); - await activeRun?.catch(() => undefined); - const wallMs = (options.now?.() ?? Date.now()) - startedAt; - const prompt = buildCodexPrompt(request); - return { - provider: 'codex', - threadId: activeThreadId, - status: 'failed', - summary: error.message, - changedPaths: [], - proposedCommands: [], - verificationHints: [], - error: { - code: 'codex_execution_timeout', - message: error.message, - retryable: true, - }, - usage: { - subscriptionPlan: String(request.metadata?.subscriptionPlan ?? ''), - wallMs, - wallMinutes: wallMs / 60_000, - nativeUnit: 'wall_minute', - inputTokens: null, - outputTokens: null, - cachedInputTokens: null, - filesChanged: 0, - }, - metadata: { - timeoutMs: error.timeoutMs, - request: { - model: request.model ?? null, - reasoningEffort: request.reasoningEffort ?? null, - sandboxMode: request.sandboxMode, - approvalPolicy: request.approvalPolicy, - timeoutMs: request.timeoutMs ?? null, - threadOptions: mapCodexThreadOptions(request), - allowedPaths: request.allowedPaths, - forbiddenPaths: request.forbiddenPaths, - toolCount: request.tools?.length ?? 0, - tools: request.tools ?? [], - promptCharacters: prompt.length, - prompt, - }, - }, - }; - } - const message = error instanceof Error ? error.message : String(error); - const prompt = buildCodexPrompt(request); - return { - provider: 'codex', - threadId: request.threadId ?? '', - status: 'failed', - summary: `Codex SDK boundary could not be initialized: ${message}`, - changedPaths: [], - proposedCommands: [], - verificationHints: [], - error: { - code: 'codex_sdk_initialization_failed', - message, - retryable: true, - }, - metadata: { - request: { - model: request.model ?? null, - reasoningEffort: request.reasoningEffort ?? null, - sandboxMode: request.sandboxMode, - approvalPolicy: request.approvalPolicy, - timeoutMs: request.timeoutMs ?? null, - threadOptions: mapCodexThreadOptions(request), - allowedPaths: request.allowedPaths, - forbiddenPaths: request.forbiddenPaths, - toolCount: request.tools?.length ?? 0, - tools: request.tools ?? [], - promptCharacters: prompt.length, - prompt, - }, - }, - }; - } finally { - options.signal?.removeEventListener('abort', cancelFromAuthority); - await ownedClient?.cleanup?.(); - } -} diff --git a/src/agents/adapters/codex/execution-codex-stream.ts b/src/agents/adapters/codex/execution-codex-stream.ts deleted file mode 100644 index 8eb4348d..00000000 --- a/src/agents/adapters/codex/execution-codex-stream.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CodexRunResult, CodexThread } from './execution-codex-core.ts'; -import { redactCodexTraceRecord } from './execution-codex-redaction.ts'; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function text(value: unknown) { - return typeof value === 'string' ? value : ''; -} - -export async function runCodexThread( - thread: CodexThread, - prompt: string, - onEvent?: (event: Record) => void | Promise, - signal?: AbortSignal, - onThreadStarted?: (threadId: string) => void, -): Promise { - if (!thread.runStreamed) return thread.run(prompt, { signal }); - const streamed = await thread.runStreamed(prompt, { signal }); - const items = new Map>(); - let finalResponse = ''; - let usage: CodexRunResult['usage'] = null; - const result = (): CodexRunResult => ({ items: [...items.values()], finalResponse, usage }); - for await (const rawEvent of streamed.events) { - const event = redactCodexTraceRecord(rawEvent); - const eventType = text(event.type); - if (eventType === 'thread.started') { - const threadId = text(event.thread_id); - if (threadId) onThreadStarted?.(threadId); - } - await onEvent?.(event); - const item = record(event.item); - const itemId = text(item.id); - if (itemId && eventType.startsWith('item.')) items.set(itemId, item); - if (eventType === 'item.completed' && item.type === 'agent_message') finalResponse = text(item.text); - if (eventType === 'turn.completed') { - usage = record(event.usage) as CodexRunResult['usage']; - return result(); - } - if (eventType === 'turn.failed') throw new Error(text(record(event.error).message) || 'Codex turn failed.'); - if (eventType === 'error') throw new Error(text(event.message) || 'Codex event stream failed.'); - } - return result(); -} diff --git a/src/agents/adapters/codex/execution-codex.ts b/src/agents/adapters/codex/execution-codex.ts deleted file mode 100644 index 2f6a70af..00000000 --- a/src/agents/adapters/codex/execution-codex.ts +++ /dev/null @@ -1,29 +0,0 @@ -export { - CodexRequestSafetyError, - buildCodexPrompt, - codexAgentToolConfig, - codexAgentToolEnvironment, - mapCodexThreadOptions, - treeDxContentReceipts, - validateCodexExecutionRequest, -} from './execution-codex-core.ts'; -export type { - CodexExecutionRequest, - CodexExecutionResult, - CodexExecutionStatus, - CodexReasoningEffort, - CodexRunResult, - CodexClient, - CodexThread, - CodexThreadOptions, - PreparedCodexWorktree, - RunCodexTaskOptions, -} from './execution-codex-core.ts'; -export type { CodexSandboxMode } from './codex-readiness.ts'; -export { normalizeCodexRunResult, runCodexTask } from './execution-codex-result.ts'; -export { - CodexExecutionProviderAdapter, - codexExecutionTimeoutMs, - missingCodexCompletionReceipts, -} from '../reconciliation/execution-codex-adapter.ts'; -export type { CodexExecutionProviderAdapterOptions } from '../reconciliation/execution-codex-adapter.ts'; diff --git a/src/agents/adapters/execution/execution.ts b/src/agents/adapters/execution/execution.ts deleted file mode 100644 index 420555fd..00000000 --- a/src/agents/adapters/execution/execution.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { normalizeAgentCliOptions } from '../../cli-tools.ts'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; -import { getAgentProviderSelections } from '@treeseed/sdk/platform/deploy-runtime'; -import type { CopilotTaskInput, CopilotTaskResult } from '@treeseed/sdk/copilot'; -import type { ExecutionRunRef } from '@treeseed/sdk/types/agents'; -import { - CodexExecutionProviderAdapter, - type CodexExecutionProviderAdapterOptions, -} from '../codex/execution-codex.ts'; -import { OpenCodeExecutionProviderAdapter, type OpenCodeExecutionProviderOptions } from '../opencode/execution-opencode.ts'; -import { - JiraExecutionProviderAdapter, - type JiraExecutionProviderConfig, -} from '../integrations/execution-jira.ts'; -import { - GitHubIssueExecutionProviderAdapter, - type GitHubIssuesExecutionProviderConfig, -} from '../repositories/execution-github-issues.ts'; -import { - DiscordExecutionProviderAdapter, - type DiscordExecutionProviderConfig, -} from '../integrations/execution-discord.ts'; -import { - WorkflowExecutionProviderAdapter, - type WorkflowExecutionProviderAdapterOptions, -} from '../operations/execution-workflow.ts'; -import { PlatformOperationExecutionProviderAdapter } from '../operations/execution-platform-operation.ts'; -import { createCopilotAgentTools } from '../../tools/agent-tool-copilot.ts'; -import type { ResearchSourcePolicy } from '@treeseed/sdk/agent-capacity'; - -export interface CopilotProviderConfiguration { - type: 'openai'; - baseUrl?: string; - apiKey?: string; - wireApi?: 'completions' | 'responses'; -} - -type ConfiguredCopilotTaskInput = CopilotTaskInput & { - provider?: CopilotProviderConfiguration; -}; - -function invocationRunId(input: ExecutionProviderInvocation) { - return typeof input.metadata?.runId === 'string' ? input.metadata.runId : input.assignment.id; -} - -export class CopilotExecutionProviderAdapter implements ExecutionProviderAdapter { - constructor(private readonly options: { - runCopilotTask?: (input: ConfiguredCopilotTaskInput) => Promise; - env?: NodeJS.ProcessEnv; - repoRoot?: string; - researchSourcePolicy?: ResearchSourcePolicy; - provider?: CopilotProviderConfiguration; - model?: string; - } = {}) {} - - async describe() { - return { - id: 'copilot', - kind: 'ai_model' as const, - capabilities: ['planning', 'implementation', 'repo_read', 'repo_write'], - nativeUnit: 'assignment', - quotaVisibility: 'opaque' as const, - maxConcurrentAssignments: 1, - supportsAsync: false, - supportsCancel: false, - supportsResume: false, - supportsUsage: false, - supportsArtifacts: false, - }; - } - - async observe() { - return { - descriptor: await this.describe(), - available: true, - pressure: 'normal' as const, - activeAssignmentCount: 0, - }; - } - - async start(input: ExecutionProviderInvocation) { - const cli = normalizeAgentCliOptions(input.agent.cli); - const repoRoot = input.workspace?.repoRoot ?? this.options.repoRoot ?? process.cwd(); - const telemetryPath = typeof input.metadata?.toolTelemetryPath === 'string' ? input.metadata.toolTelemetryPath : null; - const copilotTools = createCopilotAgentTools({ - apiBaseUrl: this.options.env?.TREESEED_API_BASE_URL ?? process.env.TREESEED_API_BASE_URL ?? '', - providerAccessToken: this.options.env?.TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN ?? process.env.TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN ?? process.env.TREESEED_PROVIDER_ACCESS_TOKEN ?? '', - assignmentId: input.assignment.id, - leaseToken: input.leaseToken, - descriptors: input.tools ?? [], - repoRoot, - telemetryPath, - researchSourcePolicy: this.options.researchSourcePolicy, - }); - const prompt = [ - input.workPackage.instructions, - '', - 'Assignment tools and permissions:', - JSON.stringify({ - tools: input.tools ?? [], - workspace: input.workspace ?? null, - capacity: input.capacityEnvelope, - }, null, 2), - '', - 'Available TreeSeed tool call names:', - copilotTools.length > 0 - ? copilotTools.map((tool) => `- ${tool.name}`).join('\n') - : '- ', - ].join('\n'); - const runCopilotTask = (this.options.runCopilotTask - ?? (await import('@treeseed/sdk/copilot')).runCopilotTask) as ( - input: ConfiguredCopilotTaskInput, - ) => Promise; - const result = await runCopilotTask({ - prompt, - cwd: repoRoot, - model: this.options.model ?? cli.model, - allowTools: cli.allowTools, - tools: copilotTools, - env: this.options.env ?? process.env, - provider: this.options.provider, - }); - const ignoredArgs = cli.additionalArgs?.length - ? `Ignored Copilot CLI-only arguments because Treeseed uses @github/copilot-sdk internally: ${cli.additionalArgs.join(' ')}` - : ''; - return { - status: result.status, - summary: result.summary, - runId: invocationRunId(input), - outputs: { - stdout: result.stdout, - stderr: [result.stderr, ignoredArgs].filter(Boolean).join('\n'), - }, - metadata: { - provider: 'copilot', - promptCharacters: prompt.length, - toolCount: input.tools?.length ?? 0, - copilotToolCount: copilotTools.length, - toolTelemetryPath: telemetryPath, - }, - }; - } - - async collectUsage(input: ExecutionRunRef) { - return [{ - kind: 'copilot_sdk', - unit: 'unsupported', - amount: 0, - source: 'copilot', - partial: true, - metadata: { - supported: false, - reason: 'copilot_sdk_usage_not_exposed', - assignmentId: input.assignmentId, - }, - }]; - } - - async collectArtifacts(input: ExecutionRunRef) { - return [{ - kind: 'execution_trace', - name: `${input.runId}.copilot-trace`, - metadata: { - supported: true, - assignmentId: input.assignmentId, - runId: input.runId, - }, - }]; - } -} - -export function createExecutionProviderAdapter(configuredModeInput?: string, options: { - repoRoot?: string; - jira?: JiraExecutionProviderConfig | null; - githubIssues?: GitHubIssuesExecutionProviderConfig | null; - discord?: DiscordExecutionProviderConfig | null; - workflow?: WorkflowExecutionProviderAdapterOptions | null; - codex?: Omit | null; - opencode?: OpenCodeExecutionProviderOptions | null; - copilot?: CopilotProviderConfiguration | null; - copilotModel?: string; - env?: NodeJS.ProcessEnv; - researchSourcePolicy?: ResearchSourcePolicy; -} = {}) { - const configuredMode = String( - configuredModeInput ?? process.env.TREESEED_AGENT_EXECUTION_PROVIDER ?? getAgentProviderSelections().execution, - ).toLowerCase(); - if (configuredMode === 'jira') { - return new JiraExecutionProviderAdapter({ config: options.jira }); - } - if (configuredMode === 'github_issues') { - return new GitHubIssueExecutionProviderAdapter({ config: options.githubIssues }); - } - if (configuredMode === 'discord') { - return new DiscordExecutionProviderAdapter({ config: options.discord }); - } - if (configuredMode === 'workflow') { - return new WorkflowExecutionProviderAdapter(options.workflow ?? {}); - } - if (configuredMode === 'platform-operation') return new PlatformOperationExecutionProviderAdapter({ env: options.env }); - if (configuredMode === 'codex') { - return new CodexExecutionProviderAdapter({ ...(options.codex ?? {}), repoRoot: options.repoRoot, researchSourcePolicy: options.researchSourcePolicy ?? options.codex?.researchSourcePolicy }); - } - if (configuredMode === 'opencode') return new OpenCodeExecutionProviderAdapter({ ...(options.opencode ?? {}), env: options.env }); - if (configuredMode === 'copilot') { - return new CopilotExecutionProviderAdapter({ repoRoot: options.repoRoot, env: options.env, researchSourcePolicy: options.researchSourcePolicy, provider: options.copilot ?? undefined, model: options.copilotModel }); - } - throw new Error(`Unsupported execution provider "${configuredMode}". Configure codex, opencode, copilot, jira, github_issues, discord, workflow, or platform-operation.`); -} diff --git a/src/agents/adapters/integrations/execution-discord.ts b/src/agents/adapters/integrations/execution-discord.ts deleted file mode 100644 index 66713f4a..00000000 --- a/src/agents/adapters/integrations/execution-discord.ts +++ /dev/null @@ -1,290 +0,0 @@ -import type { - ExecutionArtifactRef, - ExecutionProviderDescriptor, - ExecutionProviderObserveInput, - ExecutionProviderObservation, - ExecutionRunRef, - ExecutionRunSnapshot, - ExecutionUsageActual, -} from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; - -export interface DiscordExecutionProviderConfig { - botToken: string; - channelId: string; - guildId?: string | null; - threadPrefix: string; -} - -export interface DiscordExecutionProviderAdapterOptions { - config?: DiscordExecutionProviderConfig | null; - fetchImpl?: typeof fetch; -} - -export class DiscordProviderError extends Error { - constructor( - readonly code: string, - message: string, - readonly retryable = true, - readonly status?: number, - ) { - super(message); - this.name = 'DiscordProviderError'; - } -} - -export interface DiscordThreadRef { - threadId: string; - messageId?: string | null; - threadName: string; - threadUrl?: string | null; - messageUrl?: string | null; - reused?: boolean; -} - -export const DISCORD_API_BASE_URL = 'https://discord.com/api/v10'; - -export function envValue(env: NodeJS.ProcessEnv, name: string) { - return env[name]?.trim() || ''; -} - -export function resolveDiscordExecutionProviderConfig(env: NodeJS.ProcessEnv = process.env): DiscordExecutionProviderConfig | null { - const botToken = envValue(env, 'TREESEED_DISCORD_BOT_TOKEN'); - const channelId = envValue(env, 'TREESEED_DISCORD_CHANNEL_ID'); - if (!botToken || !channelId) return null; - return { - botToken, - channelId, - guildId: envValue(env, 'TREESEED_DISCORD_GUILD_ID') || null, - threadPrefix: envValue(env, 'TREESEED_DISCORD_THREAD_PREFIX') || 'treeseed', - }; -} - -export function descriptor(): ExecutionProviderDescriptor { - return { - id: 'discord', - kind: 'human_issue_queue', - capabilities: [ - 'human_coordination', - 'announcement', - 'feedback_request', - 'decision_action', - 'human_review', - 'manual_execution', - 'planning', - 'review', - ], - capabilityAliases: ['discord', 'discord_thread', 'human_coordination'], - nativeUnit: 'thread_activity', - quotaVisibility: 'partial', - maxConcurrentAssignments: 1, - supportsAsync: true, - supportsCancel: true, - supportsResume: true, - supportsUsage: true, - supportsArtifacts: true, - }; -} - -export function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export function arrayOfRecords(value: unknown): Array> { - return Array.isArray(value) ? value.map(record).filter((entry) => Object.keys(entry).length > 0) : []; -} - -export function stringValue(...values: unknown[]) { - for (const value of values) { - if (typeof value === 'string' && value.trim()) return value.trim(); - if (typeof value === 'number' && Number.isFinite(value)) return String(value); - } - return null; -} - -export function sanitizeName(value: string) { - return value.toLowerCase().replace(/[^a-z0-9_.-]+/gu, '-').replace(/^-+|-+$/gu, '').slice(0, 80) || 'assignment'; -} - -export function threadName(config: DiscordExecutionProviderConfig, assignmentId: string) { - return `${sanitizeName(config.threadPrefix)}-${sanitizeName(assignmentId)}`.slice(0, 100); -} - -export function discordUrl(config: DiscordExecutionProviderConfig, channelId: string, messageId?: string | null) { - if (!config.guildId) return null; - return `https://discord.com/channels/${encodeURIComponent(config.guildId)}/${encodeURIComponent(channelId)}${messageId ? `/${encodeURIComponent(messageId)}` : ''}`; -} - -export function redactSensitive(value: unknown, key = ''): unknown { - if (key && /(?:token|authorization|password|credential|api[_-]?key|private[_-]?key)/iu.test(key)) return ''; - if (typeof value === 'string') { - if (/(?:secret_should_not_leak|discord_secret|Bot\s+[A-Za-z0-9._-]+)/u.test(value)) return ''; - return value; - } - if (Array.isArray(value)) return value.map((entry) => redactSensitive(entry)); - if (value && typeof value === 'object') { - return Object.fromEntries(Object.entries(value as Record).map(([entryKey, entryValue]) => [ - entryKey, - redactSensitive(entryValue, entryKey), - ])); - } - return value; -} - -export function fieldSummary(value: unknown) { - try { - return JSON.stringify(redactSensitive(value), null, 2); - } catch { - return String(value ?? ''); - } -} - -export function messageFromWorkPackage(input: ExecutionProviderInvocation) { - const handles = record(input.assignment.capabilityHandles); - const workspace = record(input.assignment.workspaceContext); - const safeAssignment = { - id: input.assignment.id, - projectId: input.assignment.projectId, - teamId: input.assignment.teamId, - capacityProviderId: input.assignment.capacityProviderId, - executionProviderId: input.assignment.executionProviderId ?? null, - mode: input.assignment.mode, - capabilityHandleKinds: Object.keys(handles), - workspaceContextKeys: Object.keys(workspace).filter((key) => !/token|secret|credential|handle/iu.test(key)), - }; - const sections = [ - `**${input.workPackage.title}**`, - input.workPackage.summary, - '', - '**Instructions**', - input.workPackage.instructions, - '', - '**Expected outputs**', - `\`\`\`json\n${fieldSummary(input.workPackage.expectedOutputs)}\n\`\`\``, - '**Constraints**', - `\`\`\`json\n${fieldSummary(input.workPackage.constraints)}\n\`\`\``, - '**Assignment**', - `\`\`\`json\n${fieldSummary(safeAssignment)}\n\`\`\``, - '', - 'Reply with one exact control line when ready:', - '- `treeseed: running`', - '- `treeseed: blocked `', - '- `treeseed: complete `', - '- `treeseed: cancel `', - ]; - const content = sections.join('\n'); - return content.length > 1900 ? `${content.slice(0, 1890)}\n...[truncated]` : content; -} - -export function authHeaders(config: DiscordExecutionProviderConfig) { - return { - authorization: `Bot ${config.botToken}`, - 'content-type': 'application/json', - }; -} - -export function safeErrorMessage(status: number, code: string) { - if (status === 401 || status === 403) return 'Discord authentication or channel permission failed.'; - if (status === 404) return 'Discord message or thread was not found.'; - if (status === 429 || status >= 500) return 'Discord provider is temporarily unavailable.'; - return `Discord request failed with status ${status} (${code}).`; -} - -export function mapHttpError(status: number, fallbackCode: string) { - if (status === 401 || status === 403) return new DiscordProviderError('discord_auth_failed', safeErrorMessage(status, fallbackCode), false, status); - if (status === 404) return new DiscordProviderError('discord_ref_missing', safeErrorMessage(status, fallbackCode), true, status); - if (status === 429 || status >= 500) return new DiscordProviderError('discord_provider_unavailable', safeErrorMessage(status, fallbackCode), true, status); - return new DiscordProviderError(fallbackCode, safeErrorMessage(status, fallbackCode), false, status); -} - -export function messageContent(message: Record) { - return stringValue(message.content) ?? ''; -} - -export function controlFromMessages(messages: Array>) { - const sorted = [...messages].sort((a, b) => String(stringValue(b.timestamp, b.id) ?? '').localeCompare(String(stringValue(a.timestamp, a.id) ?? ''))); - for (const message of sorted) { - const content = messageContent(message).trim(); - const match = /^treeseed:\s*(running|blocked|complete|cancel)\b\s*(.*)$/iu.exec(content); - if (!match) continue; - return { - command: match[1].toLowerCase(), - detail: match[2]?.trim() || '', - messageId: stringValue(message.id), - author: stringValue(record(message.author).username, record(message.author).id), - timestamp: stringValue(message.timestamp), - }; - } - return null; -} - -export function snapshotFromThread(config: DiscordExecutionProviderConfig, ref: DiscordThreadRef, messages: Array>): ExecutionRunSnapshot { - const control = controlFromMessages(messages); - if (!control) { - return { - status: 'waiting', - summary: `Discord thread ${ref.threadName} is waiting for human coordination.`, - runId: ref.threadId, - externalRef: ref.threadId, - externalUrl: ref.threadUrl ?? discordUrl(config, ref.threadId), - outputs: { - threadId: ref.threadId, - messageId: ref.messageId ?? null, - replyCount: messages.length, - }, - retryable: true, - code: 'discord_thread_waiting', - metadata: { - provider: 'discord', - channelId: config.channelId, - threadId: ref.threadId, - messageId: ref.messageId ?? null, - }, - }; - } - const status = control.command === 'running' - ? 'running' - : control.command === 'blocked' - ? 'blocked' - : control.command === 'complete' - ? 'completed' - : 'failed'; - return { - status, - summary: control.command === 'complete' - ? (control.detail || `Discord thread ${ref.threadName} is complete.`) - : control.command === 'blocked' - ? (control.detail || `Discord thread ${ref.threadName} is blocked.`) - : control.command === 'cancel' - ? (control.detail || `Discord thread ${ref.threadName} was cancelled.`) - : `Discord thread ${ref.threadName} is running.`, - runId: ref.threadId, - externalRef: ref.threadId, - externalUrl: ref.threadUrl ?? discordUrl(config, ref.threadId, control.messageId), - outputs: { - threadId: ref.threadId, - messageId: ref.messageId ?? null, - control, - replyCount: messages.length, - blockerReason: control.command === 'blocked' ? control.detail || null : null, - completionSummary: control.command === 'complete' ? control.detail || null : null, - }, - retryable: control.command === 'blocked' ? true : control.command === 'cancel' ? false : undefined, - code: control.command === 'blocked' - ? 'discord_thread_blocked' - : control.command === 'cancel' - ? 'discord_thread_cancelled' - : undefined, - metadata: { - provider: 'discord', - channelId: config.channelId, - threadId: ref.threadId, - messageId: ref.messageId ?? null, - controlMessageId: control.messageId, - controlCommand: control.command, - }, - }; -} - -export { DiscordExecutionProviderAdapter } from '../reconciliation/execution-discord-adapter.ts'; - diff --git a/src/agents/adapters/integrations/execution-jira.ts b/src/agents/adapters/integrations/execution-jira.ts deleted file mode 100644 index bbea6299..00000000 --- a/src/agents/adapters/integrations/execution-jira.ts +++ /dev/null @@ -1,330 +0,0 @@ -import type { - ExecutionArtifactRef, - ExecutionProviderDescriptor, - ExecutionProviderObserveInput, - ExecutionProviderObservation, - ExecutionRunRef, - ExecutionRunSnapshot, - ExecutionUsageActual, -} from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; - -export interface JiraStatusMapping { - doneStatuses: string[]; - blockedStatuses: string[]; - cancelledStatuses: string[]; - inProgressStatuses: string[]; -} - -export interface JiraExecutionProviderConfig extends JiraStatusMapping { - baseUrl: string; - email: string; - apiToken: string; - projectKey: string; - issueType: string; - storyPointsField?: string | null; -} - -export interface JiraIssueSnapshot { - key: string; - url: string; - status: string; - statusCategory?: string | null; - assignee?: string | null; - summary?: string | null; - timeTracking?: Record; - comments?: Array>; - attachments?: Array>; - links?: Array>; - fields?: Record; -} - -export interface JiraExecutionProviderAdapterOptions { - config?: JiraExecutionProviderConfig | null; - fetchImpl?: typeof fetch; - now?: () => Date; -} - -export class JiraProviderError extends Error { - constructor( - readonly code: string, - message: string, - readonly retryable = true, - readonly status?: number, - ) { - super(message); - this.name = 'JiraProviderError'; - } -} - -export const DEFAULT_DONE_STATUSES = ['Done', 'Resolved', 'Closed']; -export const DEFAULT_BLOCKED_STATUSES = ['Blocked']; -export const DEFAULT_CANCELLED_STATUSES = ['Cancelled', "Won't Do", 'Wont Do']; -export const DEFAULT_IN_PROGRESS_STATUSES = ['In Progress']; -export const JIRA_ASSIGNMENT_PROPERTY = 'treeseedAssignment'; - -export function envValue(env: NodeJS.ProcessEnv, name: string) { - return env[name]?.trim() || ''; -} - -export function parseList(value: string, fallback: string[]) { - if (!value.trim()) return fallback; - const values = value.split(',').map((entry) => entry.trim()).filter(Boolean); - return values.length ? values : fallback; -} - -export function normalizeBaseUrl(value: string) { - return value.replace(/\/+$/u, ''); -} - -export function resolveJiraExecutionProviderConfig( - env: NodeJS.ProcessEnv = process.env, -): JiraExecutionProviderConfig | null { - const baseUrl = envValue(env, 'TREESEED_JIRA_BASE_URL'); - const email = envValue(env, 'TREESEED_JIRA_EMAIL'); - const apiToken = envValue(env, 'TREESEED_JIRA_API_TOKEN'); - const projectKey = envValue(env, 'TREESEED_JIRA_PROJECT_KEY'); - if (!baseUrl || !email || !apiToken || !projectKey) return null; - return { - baseUrl: normalizeBaseUrl(baseUrl), - email, - apiToken, - projectKey, - issueType: envValue(env, 'TREESEED_JIRA_ISSUE_TYPE') || 'Task', - doneStatuses: parseList(envValue(env, 'TREESEED_JIRA_DONE_STATUSES'), DEFAULT_DONE_STATUSES), - blockedStatuses: parseList(envValue(env, 'TREESEED_JIRA_BLOCKED_STATUSES'), DEFAULT_BLOCKED_STATUSES), - cancelledStatuses: parseList(envValue(env, 'TREESEED_JIRA_CANCELLED_STATUSES'), DEFAULT_CANCELLED_STATUSES), - inProgressStatuses: parseList(envValue(env, 'TREESEED_JIRA_IN_PROGRESS_STATUSES'), DEFAULT_IN_PROGRESS_STATUSES), - storyPointsField: envValue(env, 'TREESEED_JIRA_STORY_POINTS_FIELD') || null, - }; -} - -export function descriptor(): ExecutionProviderDescriptor { - return { - id: 'jira', - kind: 'human_issue_queue', - capabilities: [ - 'human_review', - 'manual_execution', - 'qa_validation', - 'project_management', - 'issue_queue', - 'planning', - 'implementation', - 'review', - 'test', - ], - capabilityAliases: ['jira', 'jira_issue_queue'], - nativeUnit: 'issue_hour', - quotaVisibility: 'partial', - maxConcurrentAssignments: 1, - supportsAsync: true, - supportsCancel: true, - supportsResume: true, - supportsUsage: true, - supportsArtifacts: true, - }; -} - -export function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export function arrayOfRecords(value: unknown): Array> { - return Array.isArray(value) ? value.map(record).filter((entry) => Object.keys(entry).length > 0) : []; -} - -export function stringValue(...values: unknown[]) { - for (const value of values) { - if (typeof value === 'string' && value.trim()) return value.trim(); - } - return null; -} - -export function lowerSet(values: string[]) { - return new Set(values.map((entry) => entry.toLowerCase())); -} - -export function sanitizeJiraLabel(value: string) { - return value.toLowerCase().replace(/[^a-z0-9_.-]+/gu, '-').replace(/^-+|-+$/gu, '').slice(0, 120) || 'assignment'; -} - -export function assignmentLabel(assignmentId: string) { - return `treeseed-assignment-${sanitizeJiraLabel(assignmentId)}`; -} - -export function issueUrl(config: JiraExecutionProviderConfig, issueKey: string) { - return `${config.baseUrl}/browse/${encodeURIComponent(issueKey)}`; -} - -export function textDoc(text: string) { - return { - type: 'doc', - version: 1, - content: [{ - type: 'paragraph', - content: [{ type: 'text', text }], - }], - }; -} - -export function fieldSummary(value: unknown) { - try { - return JSON.stringify(value, (_key, inner) => { - if (typeof inner === 'string' && /token|secret|password|authorization|api[_-]?key/iu.test(inner)) { - return ''; - } - return inner; - }, 2); - } catch { - return String(value ?? ''); - } -} - -export function jiraDocFromWorkPackage(input: ExecutionProviderInvocation): Record { - const handles = record(input.assignment.capabilityHandles); - const workspace = record(input.assignment.workspaceContext); - const safeAssignment = { - id: input.assignment.id, - projectId: input.assignment.projectId, - teamId: input.assignment.teamId, - capacityProviderId: input.assignment.capacityProviderId, - executionProviderId: input.assignment.executionProviderId ?? null, - mode: input.assignment.mode, - capabilityHandleKinds: Object.keys(handles), - workspaceContextKeys: Object.keys(workspace).filter((key) => !/token|secret|credential|handle/iu.test(key)), - }; - const text = [ - `Summary:\n${input.workPackage.summary}`, - `Instructions:\n${input.workPackage.instructions}`, - `Expected outputs:\n${fieldSummary(input.workPackage.expectedOutputs)}`, - `Constraints:\n${fieldSummary(input.workPackage.constraints)}`, - `Assignment:\n${fieldSummary(safeAssignment)}`, - `Capacity envelope:\n${fieldSummary(input.capacityEnvelope)}`, - `Decision input:\n${fieldSummary(input.decisionInput.input)}`, - `Allowed paths:\n${(input.workPackage.constraints.allowedPaths ?? input.agent.execution.allowedPaths ?? []).join('\n') || ''}`, - `Forbidden paths:\n${(input.workPackage.constraints.forbiddenPaths ?? input.agent.execution.forbiddenPaths ?? []).join('\n') || ''}`, - ].join('\n\n'); - return textDoc(text); -} - -export function authHeader(config: JiraExecutionProviderConfig) { - return `Basic ${Buffer.from(`${config.email}:${config.apiToken}`).toString('base64')}`; -} - -export function safeErrorMessage(status: number, code: string) { - if (status === 401 || status === 403) return 'Jira authentication failed.'; - if (status === 404) return 'Jira issue was not found.'; - if (status === 429 || status >= 500) return 'Jira provider is temporarily unavailable.'; - return `Jira request failed with status ${status} (${code}).`; -} - -export function mapHttpError(status: number, fallbackCode: string) { - if (status === 401 || status === 403) return new JiraProviderError('jira_auth_failed', safeErrorMessage(status, fallbackCode), false, status); - if (status === 404) return new JiraProviderError('jira_issue_missing', safeErrorMessage(status, fallbackCode), true, status); - if (status === 429 || status >= 500) return new JiraProviderError('jira_provider_unavailable', safeErrorMessage(status, fallbackCode), true, status); - return new JiraProviderError(fallbackCode, safeErrorMessage(status, fallbackCode), false, status); -} - -export function safeCommentBodies(issue: JiraIssueSnapshot) { - return (issue.comments ?? []).map((comment, index) => ({ - id: stringValue(comment.id) ?? `comment-${index}`, - created: stringValue(comment.created), - updated: stringValue(comment.updated), - })); -} - -export function safeAttachmentRefs(issue: JiraIssueSnapshot) { - return (issue.attachments ?? []).map((attachment) => ({ - id: stringValue(attachment.id), - filename: stringValue(attachment.filename), - mimeType: stringValue(attachment.mimeType), - size: typeof attachment.size === 'number' ? attachment.size : null, - content: stringValue(attachment.content), - })).filter((entry) => entry.id || entry.filename || entry.content); -} - -export function safeLinkRefs(issue: JiraIssueSnapshot) { - return (issue.links ?? []).map((link, index) => { - const outwardIssue = record(link.outwardIssue); - const inwardIssue = record(link.inwardIssue); - const type = record(link.type); - return { - id: stringValue(link.id) ?? `link-${index}`, - type: stringValue(type.name), - outwardIssueKey: stringValue(outwardIssue.key), - inwardIssueKey: stringValue(inwardIssue.key), - }; - }); -} - -export function normalizeIssue(config: JiraExecutionProviderConfig, value: unknown): JiraIssueSnapshot { - const issue = record(value); - const key = stringValue(issue.key); - if (!key) { - throw new JiraProviderError('jira_payload_invalid', 'Jira issue payload did not include an issue key.', false); - } - const fields = record(issue.fields); - const status = record(fields.status); - const assignee = record(fields.assignee); - const comment = record(fields.comment); - return { - key, - url: issueUrl(config, key), - status: stringValue(status.name) ?? 'Unknown', - statusCategory: stringValue(record(status.statusCategory).name), - assignee: stringValue(assignee.displayName, assignee.emailAddress), - summary: stringValue(fields.summary), - timeTracking: record(fields.timetracking), - comments: arrayOfRecords(comment.comments), - attachments: arrayOfRecords(fields.attachment), - links: arrayOfRecords(fields.issuelinks), - fields, - }; -} - -export function statusToSnapshot(config: JiraExecutionProviderConfig, issue: JiraIssueSnapshot): Pick { - const status = issue.status.toLowerCase(); - if (lowerSet(config.doneStatuses).has(status)) return { status: 'completed' }; - if (lowerSet(config.inProgressStatuses).has(status)) return { status: 'running' }; - if (lowerSet(config.blockedStatuses).has(status)) return { status: 'blocked', retryable: true, code: 'jira_issue_blocked' }; - if (lowerSet(config.cancelledStatuses).has(status)) return { status: 'failed', retryable: false, code: 'jira_issue_cancelled' }; - if (['to do', 'todo', 'open', 'backlog', 'selected for development'].includes(status)) return { status: 'waiting' }; - return { status: 'waiting', retryable: true, code: 'jira_status_unmapped' }; -} - -export function issueSnapshot(config: JiraExecutionProviderConfig, issue: JiraIssueSnapshot): ExecutionRunSnapshot { - const mapped = statusToSnapshot(config, issue); - return { - status: mapped.status, - summary: mapped.status === 'completed' - ? `Jira issue ${issue.key} is complete.` - : `Jira issue ${issue.key} is ${issue.status}.`, - runId: issue.key, - externalRef: issue.key, - externalUrl: issue.url, - outputs: { - issueKey: issue.key, - issueUrl: issue.url, - assignee: issue.assignee ?? null, - jiraStatus: issue.status, - comments: safeCommentBodies(issue), - attachments: safeAttachmentRefs(issue), - links: safeLinkRefs(issue), - blockerReason: mapped.status === 'blocked' ? issue.summary ?? issue.status : null, - completionSummary: mapped.status === 'completed' ? issue.summary ?? `Jira issue ${issue.key} completed.` : null, - timeTracking: issue.timeTracking ?? {}, - }, - retryable: mapped.retryable, - code: mapped.code, - metadata: { - provider: 'jira', - issueKey: issue.key, - issueStatus: issue.status, - statusCategory: issue.statusCategory ?? null, - }, - }; -} - -export { JiraExecutionProviderAdapter } from '../reconciliation/execution-jira-adapter.ts'; - diff --git a/src/agents/adapters/opencode/execution-opencode.ts b/src/agents/adapters/opencode/execution-opencode.ts deleted file mode 100644 index 9c6cfd63..00000000 --- a/src/agents/adapters/opencode/execution-opencode.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { ExecutionProviderObservation, ExecutionRunRef, ExecutionRunSnapshot, ExecutionUsageActual } from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; - -type Row = Record; -export interface OpenCodeExecutionProviderOptions { baseUrl?: string; env?: NodeJS.ProcessEnv; fetchImpl?: typeof fetch; providerId?: string; model?: string; } -function record(value: unknown): Row { return value && typeof value === 'object' && !Array.isArray(value) ? value as Row : {}; } -function array(value: unknown): Row[] { return Array.isArray(value) ? value.map(record) : []; } -function text(...values: unknown[]) { return String(values.find((value) => typeof value === 'string' && value.trim()) ?? '').trim(); } -function number(...values: unknown[]) { const value = values.map(Number).find(Number.isFinite); return value ?? 0; } - -export class OpenCodeExecutionProviderAdapter implements ExecutionProviderAdapter { - private readonly snapshots = new Map(); - constructor(private readonly options: OpenCodeExecutionProviderOptions = {}) {} - private env() { return this.options.env ?? process.env; } - private baseUrl() { return text(this.options.baseUrl, this.env().TREESEED_OPENCODE_SERVER_URL, 'http://127.0.0.1:4096').replace(/\/$/u, ''); } - private headers() { - const username = text(this.env().TREESEED_OPENCODE_SERVER_USERNAME, 'opencode'); - const password = text(this.env().TREESEED_OPENCODE_SERVER_PASSWORD); - return { accept: 'application/json', 'content-type': 'application/json', ...(password ? { authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` } : {}) }; - } - private async request(path: string, init: RequestInit = {}) { - const response = await (this.options.fetchImpl ?? fetch)(`${this.baseUrl()}${path}`, { ...init, headers: { ...this.headers(), ...(init.headers ?? {}) } }); - const payload = await response.json().catch(() => ({})); - if (!response.ok) throw new Error(`OpenCode ${init.method ?? 'GET'} ${path} failed (${response.status}): ${text(record(payload).error, record(payload).message, response.statusText)}`); - return record(payload); - } - async describe() { return { id: 'opencode', kind: 'ai_model' as const, capabilities: ['planning', 'implementation', 'repo_read', 'repo_write', 'streaming', 'usage'], nativeUnit: 'token', quotaVisibility: 'exact' as const, maxConcurrentAssignments: 1, supportsAsync: true, supportsCancel: true, supportsResume: true, supportsUsage: true, supportsArtifacts: true, metadata: { serverUrl: this.baseUrl(), credentialMode: 'environment_brokered' } }; } - async observe(): Promise { - try { await this.request('/global/health'); return { descriptor: await this.describe(), available: true, pressure: 'normal', activeAssignmentCount: 0, metadata: { configured: true, openRouterCredentialBrokered: Boolean(this.env().TREESEED_OPENROUTER_API_KEY) } }; } - catch (error) { return { descriptor: await this.describe(), available: false, pressure: 'exhausted', blockedReason: error instanceof Error ? error.message : String(error), metadata: { configured: false } }; } - } - async prepare() { const observed = await this.observe(); return { accepted: observed.available === true, summary: observed.available ? 'OpenCode server is ready.' : observed.blockedReason ?? 'OpenCode is unavailable.', retryable: true, code: observed.available ? null : 'opencode_unavailable' }; } - async start(input: ExecutionProviderInvocation): Promise { - const session = await this.request('/session', { method: 'POST', body: JSON.stringify({ title: `TreeSeed ${input.assignment.id}` }) }); - const sessionId = text(session.id, record(session.session).id); - if (!sessionId) throw new Error('OpenCode did not return a session id.'); - const configuredModel = text(input.agent.cli.model, input.agent.execution.model); - const providerID = text(this.options.providerId, record(input.metadata?.executionProvider).providerId, this.env().TREESEED_OPENCODE_PROVIDER_ID, 'openrouter'); - const modelID = text(this.options.model, record(input.metadata?.executionProvider).model, configuredModel); - const prompt = [input.agent.systemPrompt, input.workPackage.instructions, 'Assignment envelope:', JSON.stringify({ capacity: input.capacityEnvelope, workspace: input.workspace, tools: input.tools ?? [] }, null, 2)].join('\n\n'); - const message = await this.request(`/session/${encodeURIComponent(sessionId)}/message`, { method: 'POST', body: JSON.stringify({ model: { providerID, modelID }, parts: [{ type: 'text', text: prompt }] }) }); - const parts = array(message.parts ?? record(message.message).parts); - const output = parts.filter((part) => part.type === 'text').map((part) => text(part.text)).filter(Boolean).join('\n'); - const usage = this.usageFromMessage(message); - const snapshot: ExecutionRunSnapshot = { status: 'completed', summary: output || 'OpenCode completed the assignment.', runId: sessionId, externalRef: sessionId, outputs: { message, text: output }, usage, artifacts: [{ kind: 'opencode_session', name: sessionId, uri: `${this.baseUrl()}/session/${sessionId}` }], metadata: { provider: 'opencode', providerID, modelID, credentialMode: 'environment_brokered', sessionId } }; - this.snapshots.set(sessionId, snapshot); return snapshot; - } - async poll(input: ExecutionRunRef) { return this.snapshots.get(input.runId) ?? { status: 'running' as const, summary: 'OpenCode session is running.', runId: input.runId, externalRef: input.externalRef ?? input.runId, metadata: { provider: 'opencode' } }; } - resume(input: ExecutionRunRef) { return this.poll(input); } - async cancel(input: ExecutionRunRef & { reason: string }) { await this.request(`/session/${encodeURIComponent(input.externalRef ?? input.runId)}/abort`, { method: 'POST', body: JSON.stringify({ reason: input.reason }) }); const snapshot = { status: 'cancelled' as const, summary: `OpenCode session aborted: ${input.reason}`, runId: input.runId, externalRef: input.externalRef ?? input.runId, code: 'opencode_aborted', retryable: false }; this.snapshots.set(input.runId, snapshot); return snapshot; } - async collectUsage(input: ExecutionRunRef) { return this.snapshots.get(input.runId)?.usage ?? []; } - async collectArtifacts(input: ExecutionRunRef) { const payload = await this.request(`/session/${encodeURIComponent(input.externalRef ?? input.runId)}/diff`).catch(() => ({})); return [{ kind: 'opencode_diff', name: `${input.runId}.diff`, metadata: { provider: 'opencode', diff: payload } }]; } - private usageFromMessage(message: Row): ExecutionUsageActual[] { - const info = record(message.info ?? record(message.message).info); const tokens = record(info.tokens ?? message.tokens); const cost = number(info.cost, message.cost); - return [ - { kind: 'input_tokens', unit: 'token', amount: number(tokens.input, tokens.prompt), source: 'opencode' }, - { kind: 'output_tokens', unit: 'token', amount: number(tokens.output, tokens.completion), source: 'opencode' }, - { kind: 'reasoning_tokens', unit: 'token', amount: number(tokens.reasoning), source: 'opencode' }, - { kind: 'cached_input_tokens', unit: 'token', amount: number(record(tokens.cache).read, tokens.cached), source: 'opencode' }, - ...(cost > 0 ? [{ kind: 'provider_cost', unit: 'USD', amount: cost, source: 'openrouter' }] : []), - ]; - } -} diff --git a/src/agents/adapters/operations/execution-platform-operation.ts b/src/agents/adapters/operations/execution-platform-operation.ts deleted file mode 100644 index 48130d8b..00000000 --- a/src/agents/adapters/operations/execution-platform-operation.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { reconcileContentPublication } from '@treeseed/sdk/platform/published-content'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function required(value: unknown, name: string) { - if (typeof value !== 'string' || !value.trim()) throw new Error(`Platform operation requires ${name}.`); - return value.trim(); -} - -function operationInput(input: ExecutionProviderInvocation) { - const workContext = record(input.workPackage.context); - const metadata = record(input.workPackage.metadata); - return record(workContext.platformOperation ?? metadata.platformOperation ?? record(input.decisionInput.input).platformOperation); -} - -export class PlatformOperationExecutionProviderAdapter implements ExecutionProviderAdapter { - constructor(private readonly options: { env?: NodeJS.ProcessEnv } = {}) {} - - async describe() { - return { - id: 'platform-operation', kind: 'deterministic_workflow' as const, - capabilities: ['platform_operation', 'r2_publication', 'repository_reconciliation', 'infrastructure_reconciliation'], - nativeUnit: 'operation', quotaVisibility: 'exact' as const, maxConcurrentAssignments: 2, - supportsAsync: false, supportsCancel: true, supportsResume: false, supportsUsage: true, supportsArtifacts: true, - }; - } - - async observe() { - const env = this.options.env ?? process.env; - const configured = Boolean(env.TREESEED_CLOUDFLARE_API_TOKEN && env.TREESEED_CLOUDFLARE_ACCOUNT_ID && env.TREESEED_CONTENT_BUCKET_NAME); - return { descriptor: await this.describe(), available: configured, pressure: configured ? 'normal' as const : 'exhausted' as const, - activeAssignmentCount: 0, blockedReason: configured ? null : 'Platform provider requires Cloudflare account, R2 bucket, and API-token bindings.' }; - } - - async start(input: ExecutionProviderInvocation) { - const operation = operationInput(input); - const kind = required(operation.kind, 'platformOperation.kind'); - const runId = typeof input.metadata?.runId === 'string' ? input.metadata.runId : input.assignment.id; - if (kind === 'diagnostic') return { status: 'completed' as const, summary: 'Platform provider diagnostic completed.', runId, - outputs: { providerClass: 'platform-operation', isolatedWorkspace: input.workspace?.repoRoot ?? null } }; - if (kind !== 'content.publish') return { status: 'failed' as const, summary: `Unsupported platform operation ${kind}.`, runId, - retryable: false, code: 'platform_operation_unsupported' }; - const env = this.options.env ?? process.env; - const receipt = await reconcileContentPublication({ - projectRoot: required(input.workspace?.repoRoot, 'isolated assignment workspace'), - contentPath: required(operation.contentPath, 'contentPath'), teamId: required(operation.teamId, 'teamId'), - projectId: required(operation.projectId, 'projectId'), sourceCommit: required(operation.sourceCommit, 'sourceCommit'), - ref: required(operation.ref, 'ref'), - channel: operation.channel === 'production' || operation.channel === 'staging' ? operation.channel : 'preview', - r2: { authMode: 'api-token', accountId: required(env.TREESEED_CLOUDFLARE_ACCOUNT_ID, 'Cloudflare account binding'), - bucket: required(env.TREESEED_CONTENT_BUCKET_NAME, 'R2 bucket binding'), apiToken: required(env.TREESEED_CLOUDFLARE_API_TOKEN, 'Cloudflare API token binding') }, - }); - return { status: 'completed' as const, summary: `Published ${receipt.uploadedObjectCount} new content objects and verified ${receipt.reusedObjectCount} existing objects.`, runId, - outputs: { receipt }, artifacts: receipt.artifacts.map((artifact) => ({ kind: artifact.kind, name: artifact.path ?? artifact.objectKey ?? artifact.sha256, - digest: artifact.sha256, mediaType: artifact.mediaType, metadata: { ...artifact } })) }; - } - - async cancel(input: { assignmentId: string; runId: string; reason: string }) { - return { status: 'cancelled' as const, summary: input.reason, runId: input.runId }; - } - - async collectUsage() { - return [{ kind: 'platform_operation', unit: 'operation', amount: 1, source: 'platform-operation', partial: false }]; - } -} diff --git a/src/agents/adapters/operations/execution-workflow.ts b/src/agents/adapters/operations/execution-workflow.ts deleted file mode 100644 index 5009f8e6..00000000 --- a/src/agents/adapters/operations/execution-workflow.ts +++ /dev/null @@ -1,499 +0,0 @@ -import type { - ExecutionArtifactRef, - ExecutionProviderDescriptor, - ExecutionProviderObserveInput, - ExecutionProviderObservation, - ExecutionRunRef, - ExecutionRunSnapshot, - ExecutionUsageActual, -} from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; - -export interface WorkflowOperationDispatchResult { - ok?: boolean; - payload?: Record; - [key: string]: unknown; -} - -export interface WorkflowExecutionProviderAdapterOptions { - dispatchWorkflowOperation?: ( - assignmentId: string, - operationId: string, - body: Record, - ) => Promise; - getWorkflowOperationRun?: (assignmentId: string, runId: string) => Promise; - now?: () => Date; -} - -interface WorkflowHandleSelection { - operationId: string; - handle: Record; - handleId: string; -} - -const WORKFLOW_PROVIDER_DESCRIPTOR: ExecutionProviderDescriptor = { - id: 'workflow', - kind: 'deterministic_workflow', - capabilities: [ - 'verification', - 'workflow_dispatch', - 'deterministic_execution', - 'external_job', - 'automation', - 'test', - 'release', - 'github_app_workflow_dispatch', - ], - capabilityAliases: ['workflow', 'github_actions', 'github_actions_workflow', 'workflow_operation'], - nativeUnit: 'runner_minute', - quotaVisibility: 'partial', - maxConcurrentAssignments: 1, - supportsAsync: true, - supportsCancel: false, - supportsResume: true, - supportsUsage: true, - supportsArtifacts: true, - metadata: { - workflowProvider: 'github_actions', - dispatchAuthority: 'assignment_scoped_workflow_operation', - credentialAuthority: 'treeseed_api_github_app', - }, -}; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function stringValue(...values: unknown[]) { - for (const value of values) { - if (typeof value === 'string' && value.trim()) return value.trim(); - if (typeof value === 'number' && Number.isFinite(value)) return String(value); - } - return null; -} - -function booleanValue(...values: unknown[]) { - for (const value of values) { - if (typeof value === 'boolean') return value; - } - return false; -} - -function workflowHandles(input: ExecutionProviderInvocation) { - const handles = record(input.assignment.capabilityHandles); - return Array.isArray(handles.workflowOperations) - ? handles.workflowOperations.filter((entry): entry is Record => Boolean(entry && typeof entry === 'object' && !Array.isArray(entry))) - : []; -} - -function resolveOperationId(input: ExecutionProviderInvocation) { - const workPackageMetadata = record(input.workPackage.metadata); - const workPackageContext = record(input.workPackage.context); - const decisionInput = record(input.decisionInput.input); - const assignmentMetadata = record(input.assignment.metadata); - return stringValue( - workPackageMetadata.workflowOperationId, - workPackageContext.workflowOperationId, - decisionInput.workflowOperationId, - assignmentMetadata.workflowOperationId, - input.workPackage.kind, - ); -} - -function resolveRequestedHandleId(input: ExecutionProviderInvocation) { - const workPackageMetadata = record(input.workPackage.metadata); - const workPackageContext = record(input.workPackage.context); - const decisionInput = record(input.decisionInput.input); - const assignmentMetadata = record(input.assignment.metadata); - return stringValue( - workPackageMetadata.workflowOperationHandleId, - workPackageContext.workflowOperationHandleId, - decisionInput.workflowOperationHandleId, - assignmentMetadata.workflowOperationHandleId, - ); -} - -function handleAllowsDispatch(handle: Record) { - const operations = Array.isArray(handle.operations) ? handle.operations.map(String) : []; - return operations.length === 0 || operations.includes('dispatch_workflow') || operations.includes('*'); -} - -function selectWorkflowHandle(input: ExecutionProviderInvocation): WorkflowHandleSelection | null { - const operationId = resolveOperationId(input); - if (!operationId) return null; - const requestedHandleId = resolveRequestedHandleId(input); - const assignmentId = input.assignment.id; - for (const handle of workflowHandles(input)) { - const handleId = stringValue(handle.id); - if (!handleId) continue; - if (requestedHandleId && handleId !== requestedHandleId) continue; - const handleKind = stringValue(handle.kind); - if (handleKind && handleKind !== 'workflow_operation') continue; - const status = stringValue(handle.status); - if (status && status !== 'active') continue; - const handleAssignmentId = stringValue(handle.assignmentId); - if (handleAssignmentId && handleAssignmentId !== assignmentId) continue; - const handleOperationId = stringValue(handle.operationId); - if (handleOperationId && handleOperationId !== operationId) continue; - if (!handleAllowsDispatch(handle)) continue; - return { operationId, handle, handleId }; - } - return null; -} - -function deniedSnapshot(input: ExecutionProviderInvocation, summary = 'Workflow execution requires an active assignment-scoped workflow operation handle.'): ExecutionRunSnapshot { - return { - status: 'failed', - summary, - runId: stringValue(input.metadata?.runId, input.assignment.id) ?? input.assignment.id, - retryable: false, - code: 'assignment_workflow_operation_denied', - metadata: { - provider: 'workflow', - assignmentId: input.assignment.id, - operationId: resolveOperationId(input), - handleId: resolveRequestedHandleId(input), - }, - }; -} - -function redactSensitive(value: unknown, key = ''): unknown { - if (key && /(?:token|authorization|password|credential|api[_-]?key|private[_-]?key)/i.test(key)) { - return ''; - } - if (typeof value === 'string') { - if (/(?:gh[psuor]_[A-Za-z0-9_]+|secret_should_not_leak|ghs_secret)/.test(value)) return ''; - return value; - } - if (Array.isArray(value)) return value.map((entry) => redactSensitive(entry)); - if (value && typeof value === 'object') { - return Object.fromEntries( - Object.entries(value as Record).map(([entryKey, entryValue]) => [ - entryKey, - redactSensitive(entryValue, entryKey), - ]), - ); - } - return value; -} - -function safeHandleMetadata(handle: Record) { - return { - id: stringValue(handle.id), - operationId: stringValue(handle.operationId), - repository: stringValue(handle.repository), - workflowFile: stringValue(handle.workflowFile), - ref: stringValue(handle.ref), - secretBearing: handle.secretBearing === true, - }; -} - -function buildWorkflowInputs(input: ExecutionProviderInvocation, selection: WorkflowHandleSelection) { - const metadata = record(input.workPackage.metadata); - const decisionInput = record(input.decisionInput.input); - return redactSensitive({ - assignmentId: input.assignment.id, - workPackageKind: input.workPackage.kind, - title: input.workPackage.title, - summary: input.workPackage.summary, - instructions: input.workPackage.instructions, - context: input.workPackage.context, - expectedOutputs: input.workPackage.expectedOutputs, - ...(record(metadata.inputs)), - ...(record(decisionInput.inputs)), - workflow: { - operationId: selection.operationId, - handle: safeHandleMetadata(selection.handle), - }, - }) as Record; -} -function dispatchPayload(result: WorkflowOperationDispatchResult | Record) { - return record((result as WorkflowOperationDispatchResult).payload ?? result); -} - -function dispatchRecord(payload: Record) { - const dispatch = record(payload.dispatch); - if (Object.keys(dispatch).length) return dispatch; - const run = record(payload.run); - return Object.keys(run).length ? run : payload; -} - -function workflowStatus(value: unknown) { - return String(value ?? '').trim().toLowerCase().replace(/\s+/g, '_'); -} - -function statusFromWorkflowStatus(status: string): ExecutionRunSnapshot['status'] { - if (['completed', 'success', 'succeeded'].includes(status)) return 'completed'; - if (['failed', 'failure', 'cancelled', 'timed_out', 'timeout'].includes(status)) return 'failed'; - if (['running', 'in_progress'].includes(status)) return 'running'; - return 'waiting'; -} - -function safeDispatchSummary(payload: Record) { - const dispatch = dispatchRecord(payload); - return redactSensitive({ - id: stringValue(dispatch.id, dispatch.runId, dispatch.workflowRunId, dispatch.jobId, payload.workflowRunId, payload.runId), - status: stringValue(dispatch.status, payload.status), - url: stringValue(dispatch.providerRunUrl, dispatch.htmlUrl, dispatch.externalUrl, dispatch.url, - payload.providerRunUrl, payload.htmlUrl, payload.externalUrl, payload.url), - logsUrl: stringValue(dispatch.logsUrl, payload.logsUrl), - artifactsUrl: stringValue(dispatch.artifactsUrl, payload.artifactsUrl), - reportUrl: stringValue(dispatch.reportUrl, payload.reportUrl), - runnerMinutes: typeof dispatch.runnerMinutes === 'number' ? dispatch.runnerMinutes : payload.runnerMinutes, - wallMs: typeof dispatch.wallMs === 'number' ? dispatch.wallMs : payload.wallMs, - durationSeconds: typeof dispatch.durationSeconds === 'number' ? dispatch.durationSeconds : payload.durationSeconds, - changedFiles: Array.isArray(dispatch.changedFiles) ? dispatch.changedFiles.map(String) : Array.isArray(payload.changedFiles) ? payload.changedFiles.map(String) : undefined, - }) as Record; -} - -function mapWorkflowSnapshot(input: { - assignmentId: string; - operationId: string; - handleId: string; - payload: Record; - fallbackRunId: string; -}) { - const dispatch = dispatchRecord(input.payload); - const normalizedStatus = workflowStatus(stringValue(dispatch.status, input.payload.status, 'dispatched')); - const status = statusFromWorkflowStatus(normalizedStatus); - const runId = stringValue( - dispatch.id, - dispatch.runId, - dispatch.workflowRunId, - dispatch.jobId, - input.payload.workflowRunId, - input.payload.runId, - `${input.assignmentId}:${input.operationId}`, - ) ?? input.fallbackRunId; - const externalUrl = stringValue( - dispatch.providerRunUrl, dispatch.htmlUrl, - dispatch.externalUrl, - dispatch.url, - input.payload.htmlUrl, - input.payload.externalUrl, - input.payload.url, - ); - const safeDispatch = safeDispatchSummary(input.payload); - const metadata = { - provider: 'workflow', - assignmentId: input.assignmentId, - operationId: input.operationId, - handleId: input.handleId, - dispatch: safeDispatch, - }; - const code = status === 'failed' ? `workflow_operation_${normalizedStatus || 'failed'}` : undefined; - return { - status, - summary: status === 'completed' - ? `Workflow operation ${input.operationId} completed.` - : status === 'failed' - ? `Workflow operation ${input.operationId} failed.` - : `Workflow operation ${input.operationId} is ${normalizedStatus || 'waiting'}.`, - runId, - externalRef: runId, - externalUrl, - outputs: { - operationId: input.operationId, - handleId: input.handleId, - workflowStatus: normalizedStatus, - externalRef: runId, - externalUrl, - }, - retryable: status === 'failed' ? false : undefined, - code, - usage: usageFromMetadata(metadata), - artifacts: artifactsFromRef({ - assignmentId: input.assignmentId, - runId, - externalRef: runId, - externalUrl, - metadata, - }), - metadata, - } satisfies ExecutionRunSnapshot; -} -function numberValue(...values: unknown[]) { - for (const value of values) { - if (typeof value === 'number' && Number.isFinite(value)) return value; - if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) return Number(value); - } - return null; -} - -function usageFromMetadata(metadata: Record | undefined): ExecutionUsageActual[] { - const dispatch = record(metadata?.dispatch); - const usage: ExecutionUsageActual[] = []; - const runnerMinutes = numberValue(dispatch.runnerMinutes, metadata?.runnerMinutes); - if (runnerMinutes && runnerMinutes > 0) { - usage.push({ kind: 'workflow_runner_time', unit: 'runner_minute', amount: runnerMinutes, source: 'workflow', partial: true }); - } - const wallMs = numberValue(dispatch.wallMs, metadata?.wallMs); - if (wallMs && wallMs > 0) { - usage.push({ kind: 'workflow_wall_time', unit: 'millisecond', amount: wallMs, source: 'workflow', partial: true }); - } - const durationSeconds = numberValue(dispatch.durationSeconds, metadata?.durationSeconds); - if (durationSeconds && durationSeconds > 0) { - usage.push({ kind: 'workflow_duration', unit: 'second', amount: durationSeconds, source: 'workflow', partial: true }); - } - return usage; -} - -function artifactsFromRef(input: ExecutionRunRef): ExecutionArtifactRef[] { - const metadata = record(input.metadata); - const dispatch = record(metadata.dispatch); - const artifacts: ExecutionArtifactRef[] = []; - if (input.externalRef || input.externalUrl) { - artifacts.push({ - kind: 'external_job', - name: input.externalRef ?? input.runId, - externalUrl: input.externalUrl ?? stringValue(dispatch.url) ?? undefined, - metadata: { - provider: 'workflow', - operationId: stringValue(metadata.operationId), - }, - }); - } - const logsUrl = stringValue(dispatch.logsUrl, metadata.logsUrl); - if (logsUrl) artifacts.push({ kind: 'workflow_logs', name: 'Workflow logs', externalUrl: logsUrl }); - const artifactsUrl = stringValue(dispatch.artifactsUrl, metadata.artifactsUrl); - if (artifactsUrl) artifacts.push({ kind: 'workflow_artifacts', name: 'Workflow artifacts', externalUrl: artifactsUrl }); - const reportUrl = stringValue(dispatch.reportUrl, metadata.reportUrl); - if (reportUrl) artifacts.push({ kind: 'workflow_report', name: 'Workflow report', externalUrl: reportUrl }); - const changedFiles = Array.isArray(dispatch.changedFiles) ? dispatch.changedFiles.map(String) : []; - for (const path of changedFiles) { - artifacts.push({ kind: 'changed_file', name: path, metadata: { provider: 'workflow' } }); - } - return artifacts; -} - -export class WorkflowExecutionProviderAdapter implements ExecutionProviderAdapter { - constructor(private readonly options: WorkflowExecutionProviderAdapterOptions = {}) {} - - async describe() { - return WORKFLOW_PROVIDER_DESCRIPTOR; - } - - async observe(_input: ExecutionProviderObserveInput): Promise { - const configured = typeof this.options.dispatchWorkflowOperation === 'function'; - return { - descriptor: await this.describe(), - available: configured, - pressure: configured ? 'normal' : 'exhausted', - activeAssignmentCount: 0, - blockedReason: configured ? null : 'Workflow execution provider requires assignment-scoped workflow dispatch support.', - metadata: { configured }, - }; - } - - async prepare(input: ExecutionProviderInvocation) { - if (!this.options.dispatchWorkflowOperation) { - return { - accepted: false, - summary: 'Workflow execution provider cannot dispatch without provider client workflow support.', - retryable: false, - code: 'assignment_workflow_operation_denied', - }; - } - if (!selectWorkflowHandle(input)) { - return { - accepted: false, - summary: 'Workflow execution requires an active assignment-scoped workflow operation handle.', - retryable: false, - code: 'assignment_workflow_operation_denied', - }; - } - return { - accepted: true, - summary: 'Workflow execution provider accepted the invocation.', - }; - } - - async start(input: ExecutionProviderInvocation): Promise { - const selection = selectWorkflowHandle(input); - if (!this.options.dispatchWorkflowOperation || !selection) return deniedSnapshot(input); - const inputs = buildWorkflowInputs(input, selection); - const metadata = record(input.workPackage.metadata); - const decisionInput = record(input.decisionInput.input); - const result = await this.options.dispatchWorkflowOperation(input.assignment.id, selection.operationId, { - leaseToken: input.leaseToken, - handleId: selection.handleId, - inputs, - wait: booleanValue(metadata.wait, decisionInput.wait), - }); - if (result.ok === false) { - return { - status: 'failed', - summary: `Workflow operation ${selection.operationId} dispatch failed.`, - runId: stringValue(input.metadata?.runId, input.assignment.id) ?? input.assignment.id, - retryable: true, - code: 'workflow_operation_dispatch_failed', - metadata: { - provider: 'workflow', - assignmentId: input.assignment.id, - operationId: selection.operationId, - handleId: selection.handleId, - dispatch: safeDispatchSummary(dispatchPayload(result)), - }, - }; - } - return mapWorkflowSnapshot({ - assignmentId: input.assignment.id, - operationId: selection.operationId, - handleId: selection.handleId, - payload: dispatchPayload(result), - fallbackRunId: stringValue(input.metadata?.runId, input.assignment.id) ?? input.assignment.id, - }); - } - - async poll(input: ExecutionRunRef): Promise { - if (this.options.getWorkflowOperationRun) { - const response = await this.options.getWorkflowOperationRun(input.assignmentId, input.runId); - const payload = dispatchPayload(response); - return mapWorkflowSnapshot({ assignmentId: input.assignmentId, - operationId: stringValue(record(input.metadata).operationId, 'workflow') ?? 'workflow', - handleId: stringValue(record(input.metadata).handleId, '') ?? '', payload, fallbackRunId: input.runId }); - } - const metadata = record(input.metadata); - const dispatch = record(metadata.dispatch); - const status = workflowStatus(stringValue(dispatch.status, metadata.status, 'waiting')); - if (['completed', 'success', 'succeeded', 'failed', 'failure', 'cancelled', 'timed_out', 'running', 'in_progress'].includes(status)) { - const snapshot = mapWorkflowSnapshot({ - assignmentId: input.assignmentId, - operationId: stringValue(metadata.operationId, 'workflow') ?? 'workflow', - handleId: stringValue(metadata.handleId, '') ?? '', - payload: { dispatch }, - fallbackRunId: input.runId, - }); - return { - ...snapshot, - runId: input.runId, - externalRef: input.externalRef ?? snapshot.externalRef, - externalUrl: input.externalUrl ?? snapshot.externalUrl, - }; - } - return { - status: 'waiting', - summary: 'Workflow operation polling is waiting for an external completion signal.', - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: true, - code: 'workflow_operation_poll_unavailable', - metadata: { - ...metadata, - provider: 'workflow', - }, - }; - } - - resume(input: ExecutionRunRef) { - return this.poll(input); - } - async collectUsage(input: ExecutionRunRef) { - return usageFromMetadata(record(input.metadata)); - } - - async collectArtifacts(input: ExecutionRunRef) { - return artifactsFromRef(input); - } -} diff --git a/src/agents/adapters/operations/operations.ts b/src/agents/adapters/operations/operations.ts deleted file mode 100644 index 4db01157..00000000 --- a/src/agents/adapters/operations/operations.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { OperationsSdk } from '@treeseed/sdk'; -import { - createAgentOperationEvent, - decideAgentOperationPermission, - deniedAgentOperationResult, - type AgentOperationGrant, - type AgentOperationRequest, - type AgentOperationResult, -} from '@treeseed/sdk/operations/agent-tools'; -import type { AgentOperationsAdapter } from '../../runtime/runtime-types.ts'; -import type { ScopedAgentSdk } from '@treeseed/sdk/sdk'; - -type WorkflowExecutor = Pick; - -const WORKFLOW_OPERATION_MAP: Record = { - switch: 'switch', - update: 'update', - dev: 'dev', - verify: 'test', - save: 'save', - stage: 'stage', - close: 'close', - release: 'release', -}; - -function commandList(value: unknown) { - return Array.isArray(value) ? value.map(String).filter(Boolean) : []; -} - -function workflowInputFor(request: AgentOperationRequest) { - const input = { ...(request.input ?? {}) }; - if (request.mode === 'plan') { - input.plan = input.plan ?? true; - } - if (request.operation === 'verify') { - const commands = commandList(request.input.commands); - if (commands.length) { - input.commands = commands; - } - } - return input; -} - -function completedResult( - request: AgentOperationRequest, - operationName: string, - workflowResult: Awaited>, -): AgentOperationResult { - const stdout = workflowResult.stdout ?? []; - const stderr = workflowResult.stderr ?? []; - return { - operation: request.operation, - status: workflowResult.ok ? 'completed' : 'failed', - summary: workflowResult.ok - ? `Executed workflow operation ${operationName}.` - : `Workflow operation ${operationName} failed.`, - changedPaths: request.changedPaths ?? [], - stagedPaths: request.operation === 'stage' ? request.changedPaths ?? [] : [], - commandsRun: [operationName], - artifacts: [], - error: workflowResult.ok - ? undefined - : { - code: 'workflow_operation_failed', - message: stderr.join('\n') || `Workflow operation ${operationName} failed.`, - retryable: true, - }, - metadata: { - workflowResult: { - operation: workflowResult.operation, - ok: workflowResult.ok, - payload: workflowResult.payload ?? null, - meta: workflowResult.meta ?? null, - report: workflowResult.report ?? null, - stdout, - stderr, - }, - }, - }; -} - -async function appendOperationEvent(input: { - sdk?: ScopedAgentSdk; - request: AgentOperationRequest; - result: AgentOperationResult; -}) { - if (!input.sdk || typeof input.sdk.createMessage !== 'function') return; - await input.sdk.createMessage({ - type: 'agent.operation_event', - payload: createAgentOperationEvent({ - request: input.request, - result: input.result, - }) as unknown as Record, - relatedModel: 'provider_assignment', - relatedId: input.request.taskId, - priority: 100, - }).catch(() => null); -} - -export class SdkOperationsAdapter implements AgentOperationsAdapter { - constructor(private readonly workflow: WorkflowExecutor = new OperationsSdk()) {} - - async runOperation(input: { - request: AgentOperationRequest; - grants: AgentOperationGrant[]; - sdk?: ScopedAgentSdk; - }) { - const decision = decideAgentOperationPermission({ - request: input.request, - grants: input.grants, - }); - if (!decision.allowed) { - const denied = deniedAgentOperationResult(input.request, decision); - await appendOperationEvent({ ...input, result: denied }); - return denied; - } - - const workflowOperation = WORKFLOW_OPERATION_MAP[input.request.operation]; - try { - const workflowResult = await this.workflow.execute({ - operationName: workflowOperation, - input: workflowInputFor(input.request), - }, { - cwd: input.request.worktreeRoot ?? input.request.repoRoot, - env: process.env, - transport: 'sdk', - }); - const result = completedResult(input.request, workflowOperation, workflowResult); - result.metadata.permission = decision; - await appendOperationEvent({ ...input, result }); - return result; - } catch (error) { - const failed: AgentOperationResult = { - operation: input.request.operation, - status: 'failed', - summary: error instanceof Error ? error.message : String(error), - changedPaths: input.request.changedPaths ?? [], - stagedPaths: [], - commandsRun: [workflowOperation], - artifacts: [], - error: { - code: 'operation_execution_failed', - message: error instanceof Error ? error.message : String(error), - retryable: true, - }, - metadata: { - permission: decision, - }, - }; - await appendOperationEvent({ ...input, result: failed }); - return failed; - } - } -} - -export function createOperationsAdapter() { - return new SdkOperationsAdapter(); -} diff --git a/src/agents/adapters/reconciliation/execution-codex-adapter.ts b/src/agents/adapters/reconciliation/execution-codex-adapter.ts deleted file mode 100644 index d694feb1..00000000 --- a/src/agents/adapters/reconciliation/execution-codex-adapter.ts +++ /dev/null @@ -1,470 +0,0 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { findAgentToolDefinition } from '@treeseed/sdk'; -import type { AgentRuntimeSpec, ExecutionRunRef, ExecutionRunSnapshot } from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation, ExecutionProviderToolDescriptor } from '../../runtime/runtime-types.ts'; -import { AgentWorktreeManager } from '../../../services/agent-worktrees.ts'; -import { checkCodexProviderReadiness, type CodexApprovalPolicy, type CodexSandboxMode, resolveCodexProviderConfig } from '../codex/codex-readiness.ts'; -import { - CodexRequestSafetyError, - codexAgentToolServer, - createDefaultCodexClient, - readToolTelemetry, - treeDxContentReceipts, - type CodexExecutionRequest, - type CodexExecutionResult, - type CodexReasoningEffort, - type CodexClient, - type PreparedCodexWorktree, -} from '../codex/execution-codex-core.ts'; -import { runCodexTask } from '../codex/execution-codex-result.ts'; -import type { ResearchSourcePolicy } from '@treeseed/sdk/agent-capacity'; -import { assignmentDeadlineExpired, assignmentExecutionTiming, capacityExecutionTiming, codexExecutionTimeoutMs, completionCorrectionBoundaryInstruction, deadlineBoundExecutionTimeoutMs, executionCompletionDeadlineMs } from './execution-timeout.ts'; -import { missingCodexCompletionReceipts } from './execution-completion-receipts.ts'; - -export { assignmentDeadlineExpired, codexExecutionTimeoutMs, deadlineBoundExecutionTimeoutMs } from './execution-timeout.ts'; -export { missingCodexCompletionReceipts } from './execution-completion-receipts.ts'; - -export interface CodexExecutionProviderAdapterOptions { - repoRoot?: string; - createCodexClient?: (request?: CodexExecutionRequest) => CodexClient | Promise; - prepareWorktree?: (input: { - agent: AgentRuntimeSpec; - runId: string; - repoRoot: string; - exactBaseRef?: string; - }) => Promise; - env?: NodeJS.ProcessEnv; - researchSourcePolicy?: ResearchSourcePolicy; - onEvent?: (event: Record) => void | Promise; -} - -const DEFAULT_CODEX_ALLOWED_PATHS = ['**']; -const DEFAULT_CODEX_FORBIDDEN_PATHS = ['.git/**', '.agent-worktrees/**', '.treeseed/secrets/**', 'node_modules/**']; - -function sanitizeBranchPart(value: string) { - return value.replace(/[^A-Za-z0-9._/-]+/gu, '-').replace(/^\/+|\/+$/gu, '') || 'agent'; -} - -async function prepareDefaultWorktree(input: { - agent: AgentRuntimeSpec; - runId: string; - repoRoot: string; - exactBaseRef?: string; -}) { - const branchPrefix = input.agent.execution.worktree?.branchPrefix - ?? input.agent.execution.branchPrefix - ?? 'agent'; - const featureBranch = [ - sanitizeBranchPart(branchPrefix), - sanitizeBranchPart(input.agent.slug), - sanitizeBranchPart(input.runId), - ].filter(Boolean).join('/'); - return new AgentWorktreeManager(input.repoRoot).createOrResumeWorktree(featureBranch, input.runId, input.exactBaseRef ?? 'HEAD'); -} - -function toolsWithWorktree( - tools: ExecutionProviderToolDescriptor[] | undefined, - worktreeRoot: string | undefined, - completion: { artifactKind: string | null; requireContentArtifact: boolean }, -) { - return (tools ?? []) - .map((tool) => ({ - ...tool, - metadata: { - ...(tool.metadata ?? {}), - worktreeRoot: worktreeRoot ?? (tool.metadata?.worktreeRoot ?? null), - requiredArtifactKind: completion.artifactKind, - requireContentArtifact: completion.requireContentArtifact, - }, - })) - .filter((tool) => { - const definition = findAgentToolDefinition(tool.id); - if (!definition?.requirements.includes('assignment_worktree')) return true; - return typeof tool.metadata?.worktreeRoot === 'string' && tool.metadata.worktreeRoot.trim().length > 0; - }); -} - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export function hasRequiredResponseSuspensionReceipt(telemetry: Record[]) { - return telemetry.some((entry) => entry.status === 'completed' && entry.toolId === 'treeseed.discussion.respond' - && record(entry.inputSummary).requiredResponse === true && record(entry.outputSummary).suspended === true); -} - -function mergeCodexResults(initial: CodexExecutionResult, correction: CodexExecutionResult): CodexExecutionResult { - const sum = (left: number | null | undefined, right: number | null | undefined) => { - if (left === null && right === null) return null; - return Number(left ?? 0) + Number(right ?? 0); - }; - const usage = initial.usage || correction.usage ? { - ...initial.usage, - ...correction.usage, - wallMs: sum(initial.usage?.wallMs, correction.usage?.wallMs) ?? undefined, - wallMinutes: sum(initial.usage?.wallMinutes, correction.usage?.wallMinutes) ?? undefined, - inputTokens: sum(initial.usage?.inputTokens, correction.usage?.inputTokens), - outputTokens: sum(initial.usage?.outputTokens, correction.usage?.outputTokens), - cachedInputTokens: sum(initial.usage?.cachedInputTokens, correction.usage?.cachedInputTokens), - filesChanged: new Set([...initial.changedPaths, ...correction.changedPaths]).size, - } : undefined; - return { - ...correction, - changedPaths: [...new Set([...initial.changedPaths, ...correction.changedPaths])], - proposedCommands: [...new Set([...initial.proposedCommands, ...correction.proposedCommands])], - verificationHints: [...new Set([...initial.verificationHints, ...correction.verificationHints])], - rawEventRefs: [...new Set([...(initial.rawEventRefs ?? []), ...(correction.rawEventRefs ?? [])])], - usage, - metadata: { - ...correction.metadata, - completionCorrection: { - attempted: true, - initialStatus: initial.status, - initialThreadId: initial.threadId, - }, - }, - }; -} - -function missingReceiptResult( - result: CodexExecutionResult, - missingReceipts: string[], - correctionAttempted: boolean, -): CodexExecutionResult { - const message = `Codex completion is missing required assignment tool receipt${missingReceipts.length === 1 ? '' : 's'}: ${missingReceipts.join(', ')}.`; - return { - ...result, - status: 'failed', - summary: message, - error: { - code: 'codex_required_tool_receipt_missing', - message, - retryable: true, - }, - metadata: { - ...result.metadata, - completionCorrection: { - attempted: correctionAttempted, - missingReceipts, - }, - }, - }; -} - -function durableReceiptCompletionResult(result: CodexExecutionResult): CodexExecutionResult { - return { - ...result, - status: 'completed', - summary: 'Assignment completion recovered from its complete durable tool receipts before provider terminalization.', - error: undefined, - metadata: { - ...result.metadata, - completionRecovery: { - source: 'durable_tool_receipts', - providerResultCode: result.error?.code ?? null, - }, - }, - }; -} - -export class CodexExecutionProviderAdapter implements ExecutionProviderAdapter { - private readonly assignmentWorktrees = new Map(); - constructor(private readonly options: CodexExecutionProviderAdapterOptions = {}) {} - - async describe() { - return { - id: 'codex', - kind: 'ai_model' as const, - capabilities: [ - 'planning', - 'implementation', - 'review', - 'repo_read', - 'repo_write', - 'verification', - ], - capabilityAliases: [], - nativeUnit: 'token_or_wall_minute', - quotaVisibility: 'partial' as const, - maxConcurrentAssignments: Math.max(1, Number(this.options.env?.TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS ?? process.env.TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS) || 4), - supportsAsync: false, - supportsCancel: false, - supportsResume: false, - supportsUsage: true, - supportsArtifacts: true, - }; - } - - async observe() { - const readiness = checkCodexProviderReadiness({ - env: { - ...(this.options.env ?? process.env), - TREESEED_AGENT_EXECUTION_PROVIDER: 'codex', - }, - }); - return { - descriptor: await this.describe(), - available: readiness.ok, - pressure: readiness.ok ? 'normal' as const : 'exhausted' as const, - activeAssignmentCount: 0, - blockedReason: readiness.ok ? null : readiness.blockingIssues.join('; '), - metadata: { - authMode: readiness.authMode, - sdkInstalled: readiness.sdkInstalled, - nodeVersionOk: readiness.nodeVersionOk, - }, - }; - } - - async start(input: ExecutionProviderInvocation): Promise { - const config = resolveCodexProviderConfig(this.options.env ?? process.env); - const repoRoot = this.options.repoRoot ?? process.cwd(); - const runId = typeof input.metadata?.runId === 'string' ? input.metadata.runId : input.assignment.id; - const sandboxMode = (input.agent.execution.sandboxMode ?? config.sandboxMode) as CodexSandboxMode; - const exactBaseRef = typeof input.metadata?.exactBaseRef === 'string' && input.metadata.exactBaseRef.trim() - ? input.metadata.exactBaseRef.trim() - : undefined; - const treeDxAuthority = input.metadata?.contentAuthority === 'treedx'; - const worktree = sandboxMode === 'workspace_write' || Boolean(exactBaseRef) - ? await (this.options.prepareWorktree ?? prepareDefaultWorktree)({ - agent: input.agent, - runId, - repoRoot, - exactBaseRef: treeDxAuthority ? undefined : exactBaseRef, - }) - : undefined; - if (worktree) this.assignmentWorktrees.set(input.assignment.id, worktree.worktreeRoot); - if (exactBaseRef && !treeDxAuthority && !worktree?.exactBaseRef?.toLowerCase().startsWith(exactBaseRef.toLowerCase())) { - throw new CodexRequestSafetyError( - 'worktree_base_ref_mismatch', - 'Prepared assignment worktree does not prove the governed exact base ref.', - false, - ); - } - const artifactKind = typeof input.workPackage.metadata?.artifactKind === 'string' - ? input.workPackage.metadata.artifactKind - : null; - const requireContentArtifact = input.workPackage.metadata?.requireContentArtifact === true; - const requiredPublishedSignals = Array.isArray(input.assignment.allowedOutputs?.publishedSignals) - ? input.assignment.allowedOutputs.publishedSignals.map(String).filter(Boolean) - : []; - const tools = toolsWithWorktree(input.tools, worktree?.worktreeRoot, { artifactKind, requireContentArtifact }); - const configuredTelemetryPath = typeof input.metadata?.toolTelemetryPath === 'string' ? input.metadata.toolTelemetryPath : null; - const telemetryDirectory = tools.length && !configuredTelemetryPath ? await mkdtemp(join(tmpdir(), 'treeseed-agent-tools-')) : null; - const toolTelemetryPath = configuredTelemetryPath ?? (telemetryDirectory ? join(telemetryDirectory, 'events.jsonl') : null); - if (toolTelemetryPath) await writeFile(toolTelemetryPath, '', { encoding: 'utf8', flag: 'a', mode: 0o600 }); - const toolConfigPath = telemetryDirectory ? join(telemetryDirectory, 'mcp-env.json') : null; - let result: Awaited>; - let toolTelemetry: Record[] = []; - let codexClient: Promise | null = null; - const timing = assignmentExecutionTiming( - capacityExecutionTiming(input.capacityEnvelope) ?? input.workPackage.context?.executionTiming - ?? input.workPackage.metadata?.executionTiming, - input.assignment, - ); - const configuredTimeoutMs = codexExecutionTimeoutMs(config.timeoutMs, input.agent.execution.timeoutSeconds); - const effectiveTimeoutMs = deadlineBoundExecutionTimeoutMs(configuredTimeoutMs, timing); - const executionDeadline = executionCompletionDeadlineMs(configuredTimeoutMs, timing); - try { - const request: CodexExecutionRequest = { - taskId: runId, - agentSlug: input.agent.slug, - repoRoot, - worktreeRoot: worktree?.worktreeRoot, - prompt: input.workPackage.instructions, - allowedPaths: input.agent.execution.allowedPaths?.length - ? input.agent.execution.allowedPaths - : DEFAULT_CODEX_ALLOWED_PATHS, - forbiddenPaths: input.agent.execution.forbiddenPaths?.length - ? input.agent.execution.forbiddenPaths - : DEFAULT_CODEX_FORBIDDEN_PATHS, - sandboxMode, - approvalPolicy: (input.agent.execution.approvalPolicy ?? config.approvalPolicy) as CodexApprovalPolicy, - model: input.agent.execution.model ?? config.defaultModel, - reasoningEffort: input.agent.execution.reasoningEffort as CodexReasoningEffort | undefined, - timeoutMs: effectiveTimeoutMs, - tools, - leaseToken: input.leaseToken, - toolTelemetryPath, - toolConfigPath, - providerAccessToken: this.options.env?.TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN ?? '', - apiBaseUrl: this.options.env?.TREESEED_API_BASE_URL ?? '', - researchSourcePolicy: this.options.researchSourcePolicy, - metadata: { - subscriptionPlan: config.subscriptionPlan, - executionTiming: timing ?? null, - worktreeBranch: worktree?.branchName, - exactBaseRef: worktree?.exactBaseRef ?? exactBaseRef ?? null, - workPackage: input.workPackage, - assignment: { - id: input.assignment.id, - projectId: input.assignment.projectId, - workDayId: input.assignment.workDayId, - mode: input.capacityEnvelope.mode, - }, - }, - }; - if (toolConfigPath) { - await writeFile(toolConfigPath, JSON.stringify(codexAgentToolServer({ ...request, toolConfigPath: null })?.env ?? {}), { encoding: 'utf8', mode: 0o600 }); - } - const createClient = this.options.createCodexClient ?? ((value?: CodexExecutionRequest) => createDefaultCodexClient(value, this.options.env ?? process.env)); - codexClient = Promise.resolve(createClient(request)); - result = await runCodexTask(request, { - client: codexClient, - onEvent: this.options.onEvent, - signal: input.signal, - }); - toolTelemetry = await readToolTelemetry(toolTelemetryPath); - const requiredResponseSuspended = hasRequiredResponseSuspensionReceipt(toolTelemetry); - const researchStage = typeof input.workPackage.metadata?.researchStage === 'string' ? input.workPackage.metadata.researchStage : null; - const minimumIndependentSources = Number(input.workPackage.metadata?.minimumIndependentSources ?? 2); - const initialMissingReceipts = !requiredResponseSuspended&&(result.status === 'completed' || result.error?.code === 'codex_execution_timeout' || researchStage === 'independent-source-fetch') - ? missingCodexCompletionReceipts(tools, toolTelemetry, artifactKind, researchStage, minimumIndependentSources, requireContentArtifact, requiredPublishedSignals) - : []; - if (initialMissingReceipts.length) { - const remainingTimeoutMs = executionDeadline - Date.now(); - if (remainingTimeoutMs > 0 && result.threadId) { - const correction = await runCodexTask({ - ...request, - threadId: result.threadId, - timeoutMs: remainingTimeoutMs, - prompt: [ - 'The assignment completion contract is not yet satisfied.', - `Missing required tool receipts: ${initialMissingReceipts.join(', ')}.`, - 'Do not redo completed work. Use the granted TreeSeed tools to satisfy only the missing gates.', - completionCorrectionBoundaryInstruction(timing), - 'Run treeseed.changed_paths if needed, call treeseed.verify with bounded argv if its receipt is missing, then call treeseed.checkpoint if its receipt is missing.', - 'If assignment_plan is missing, write the required operational plan. If assignment_terminal_status or assignment_summary is missing, write the terminal status and summary using the latest authoritative state version before committing.', - 'If content_committed is missing, commit the already-validated TreeDX workspace only after every required artifact, relation, terminal status, and summary receipt exists.', - 'If review_decision_recorded is missing, call treeseed.review_decision with the evidence-based approved or rejected disposition.', - 'If discussion_read or discussion_follow is missing, call treeseed.discussion.read and then treeseed.discussion.follow for the exact assignment discussion before responding.', - 'If discussion_final_response is missing, call treeseed.discussion.respond exactly once after both Discussion read receipts exist, using the assignment discussionId, exact replyTo and sourceMessageRefs, intended recipients, current state version, and a stable idempotency key. Do not use generic content creation for the response.', - 'If research_independent_publishers is missing, call the available TreeSeed tool with callName research_fetch_source (policy id research.fetch_source) for sources hosted on additional independent allowed domains until the required distinct-domain count is met. Search for and invoke the callName, not the dotted policy id. Then create, validate, and commit the required linked evidence note.', - 'If content_artifact_kind is missing, create the assignment-required content model and artifact kind with the required subject relation, validate it, and commit it. Auxiliary questions or notes do not replace the required deliverable.', - 'For every signal_publication: receipt that is missing, call treeseed.publish_signal with that exact declared contractId, the assignment subject identity, and a concise evidence-grounded message. Do not invent or omit a declared publication.', - 'If content_subject_linked is missing for a note path, use treeseed.content.link with placement.path set to that exact repository-relative note path and the assignment-required subject relation; validate the same exact placement, then commit the corrected workspace. Every created note, including an accidental placeholder, must be linked before completion.', - 'Wait for each successful receipt and do not send a final response until every listed receipt exists.', - ].join('\n'), - }, { - client: codexClient, - onEvent: this.options.onEvent, - signal: input.signal, - }); - result = mergeCodexResults(result, correction); - toolTelemetry = await readToolTelemetry(toolTelemetryPath); - } - const remainingReceipts = missingCodexCompletionReceipts(tools, toolTelemetry, artifactKind, researchStage, minimumIndependentSources, requireContentArtifact, requiredPublishedSignals); - if (result.error?.code === 'codex_execution_timeout' && !remainingReceipts.length) { - result = durableReceiptCompletionResult(result); - } else if (result.status !== 'failed' && remainingReceipts.length) { - result = missingReceiptResult(result, remainingReceipts, true); - } - } - } finally { - toolTelemetry = await readToolTelemetry(toolTelemetryPath); - await codexClient?.then((client) => client.cleanup?.()).catch(() => undefined); - if (telemetryDirectory) await rm(telemetryDirectory, { recursive: true, force: true }); - } - const contentReceipts = treeDxContentReceipts(toolTelemetry); - const requiredResponseSuspended = hasRequiredResponseSuspensionReceipt(toolTelemetry); - return { - status: requiredResponseSuspended ? 'returned' : result.status, - summary: requiredResponseSuspended ? 'Assignment suspended after a durable required-response checkpoint and Discussion message.' : result.summary ?? 'Codex subscription provider returned no summary.', - runId, - outputs: { - finalResponse: result.finalResponse ?? '', - stdout: result.finalResponse ?? '', - stderr: result.error?.message ?? '', - toolTelemetry, - contentReceipts, - }, - usage: result.usage - ? [{ - kind: 'codex', - unit: result.usage.nativeUnit ?? 'wall_minute', - amount: Number(result.usage.wallMinutes ?? 0), - source: 'codex', - partial: requiredResponseSuspended || result.status !== 'completed', - metadata: result.usage, - }] - : [], - artifacts: [ - ...contentReceipts, - ...(result.finalResponse ? [{ - kind: 'assistant_final_response', - name: `${runId}-final-response.md`, - mediaType: 'text/markdown', - metadata: { - threadId: result.threadId, - characters: result.finalResponse.length, - }, - }] : []), - ...result.changedPaths.map((path) => ({ - kind: 'changed_path', - name: path, - uri: `repo://${path}`, - metadata: { - threadId: result.threadId, - provider: 'codex', - }, - })), - ], - retryable: assignmentDeadlineExpired(timing) ? false : result.error?.retryable, - code: result.error?.code ?? null, - metadata: { - provider: 'codex', - worktreeRoot: worktree?.worktreeRoot ?? null, - worktreeBranch: worktree?.branchName ?? null, - baseRef: treeDxAuthority ? exactBaseRef ?? null : worktree?.exactBaseRef ?? exactBaseRef ?? null, - codex: result, - ...(requiredResponseSuspended ? { completion: { disposition: 'suspended', source: 'required_response_receipt' } } : {}), - }, - }; - } - async collectUsage(input: ExecutionRunRef) { - const usage = input.metadata?.codexUsage; - return usage && typeof usage === 'object' - ? [{ - kind: 'codex', - unit: String((usage as Record).nativeUnit ?? 'wall_minute'), - amount: Number((usage as Record).wallMinutes ?? 0), - source: 'codex', - partial: true, - metadata: usage as Record, - }] - : [{ - kind: 'codex', - unit: 'assignment', - amount: 1, - source: 'codex', - partial: true, - metadata: { - supported: false, - reason: 'codex_usage_available_on_start_snapshot', - assignmentId: input.assignmentId, - }, - }]; - } - - async collectArtifacts(input: ExecutionRunRef) { - return [{ - kind: 'execution_trace', - name: `${input.runId}.codex-trace`, - metadata: { - supported: true, - assignmentId: input.assignmentId, - runId: input.runId, - externalRef: input.externalRef ?? null, - }, - }]; - } - - async releaseAssignmentResources(input: { assignmentId: string; outcome: 'completed' | 'returned' | 'failed' | 'expired' | 'cancelled' }) { - const worktreeRoot = this.assignmentWorktrees.get(input.assignmentId); - if (!worktreeRoot || !['completed', 'returned', 'failed', 'expired'].includes(input.outcome)) return; - await new AgentWorktreeManager(this.options.repoRoot ?? process.cwd()).releaseWorktree(worktreeRoot); - this.assignmentWorktrees.delete(input.assignmentId); - } -} diff --git a/src/agents/adapters/reconciliation/execution-completion-receipts.ts b/src/agents/adapters/reconciliation/execution-completion-receipts.ts deleted file mode 100644 index 963f6949..00000000 --- a/src/agents/adapters/reconciliation/execution-completion-receipts.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { ExecutionProviderToolDescriptor } from '../../runtime/runtime-types.ts'; -import { hasCompletedToolEvent } from '../codex/execution-codex-core.ts'; -import { hasCompatibleContentArtifact,unlinkedNotePaths } from '../../tools/agent-tool-completion.ts'; - -const TOOL_COMPLETION_RECEIPTS = new Map([ - ['treeseed.verify', 'verification_completed'], - ['treeseed.review_decision', 'review_decision_recorded'], - ['treeseed.content.commit', 'content_committed'], - ['treedx.commit_workspace', 'content_committed'], -]); - -const SOURCE_CHECKPOINT_ARTIFACT_KINDS = new Set([ - 'failing_test_proof', - 'implementation_change', - 'implementation_revision', -]); - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function fetchedResearchPublishers(telemetry: Record[]) { - const publishers = new Set(); - for (const entry of telemetry) { - if (entry.status !== 'completed' || !Array.isArray(entry.derivedEvents)) continue; - for (const event of entry.derivedEvents) { - const derived = record(event); - if (derived.type !== 'research_citation_fetched') continue; - try { publishers.add(new URL(String(record(derived.citation).sourceUrl ?? '')).hostname.toLowerCase()); } - catch { /* A malformed citation cannot satisfy the independent-publisher gate. */ } - } - } - return publishers; -} - -export function missingCodexCompletionReceipts( - tools: ExecutionProviderToolDescriptor[], - telemetry: Record[], - artifactKind?: string | null, - researchStage?: string | null, - minimumIndependentSources = 2, - requireContentArtifact = false, - requiredPublishedSignals: string[] = [], -) { - const completedInputs = (toolId: string) => telemetry - .filter((entry) => entry.status === 'completed' && entry.toolId === toolId) - .map((entry) => record(entry.inputSummary)); - const terminal = new Set(['completed', 'failed', 'cancelled', 'suspended']); - const missing = tools.flatMap((tool) => { - const eventType = TOOL_COMPLETION_RECEIPTS.get(tool.id); - if (eventType === 'content_committed' && !requireContentArtifact) return []; - return eventType && !hasCompletedToolEvent(telemetry, eventType) ? [eventType] : []; - }); - if (tools.some((tool) => tool.id === 'treeseed.assignment_plan') - && !completedInputs('treeseed.assignment_plan').some((input) => input.action === 'write')) missing.push('assignment_plan'); - if (tools.some((tool) => tool.id === 'treeseed.assignment_status_update') - && !completedInputs('treeseed.assignment_status_update').some((input) => terminal.has(String(input.status ?? '')))) missing.push('assignment_terminal_status'); - if (tools.some((tool) => tool.id === 'treeseed.assignment_summary') - && !completedInputs('treeseed.assignment_summary').some((input) => input.action === 'write' && terminal.has(String(input.status ?? '')))) missing.push('assignment_summary'); - if (artifactKind && SOURCE_CHECKPOINT_ARTIFACT_KINDS.has(artifactKind) - && tools.some((tool) => tool.id === 'treeseed.checkpoint') - && !hasCompletedToolEvent(telemetry, 'source_checkpoint_committed')) missing.push('source_checkpoint_committed'); - if (artifactKind === 'discussion_response' - && !completedInputs('treeseed.discussion.respond').length) missing.push('discussion_final_response'); - if (researchStage === 'independent-source-fetch' && !tools.some((tool) => tool.id === 'research.fetch_source')) missing.push('research_fetch_tool_available'); - else if (researchStage === 'independent-source-fetch' && fetchedResearchPublishers(telemetry).size < minimumIndependentSources) missing.push(`research_independent_publishers:${minimumIndependentSources}`); - for (const path of unlinkedNotePaths(telemetry)) missing.push(`content_subject_linked:${path}`); - if (requireContentArtifact && artifactKind && !hasCompatibleContentArtifact(telemetry, artifactKind)) missing.push(`content_artifact_kind:${artifactKind}`); - const publishedSignals = new Set(telemetry - .filter((entry) => entry.status === 'completed' && entry.toolId === 'treeseed.publish_signal') - .flatMap((entry) => Array.isArray(entry.derivedEvents) ? entry.derivedEvents : []) - .map(record) - .filter((event) => event.type === 'signal_requested') - .map((event) => String(record(event.signal).contractId ?? '')) - .filter(Boolean)); - for (const contractId of requiredPublishedSignals) if (!publishedSignals.has(contractId)) missing.push(`signal_publication:${contractId}`); - return [...new Set(missing)]; -} diff --git a/src/agents/adapters/reconciliation/execution-discord-adapter.ts b/src/agents/adapters/reconciliation/execution-discord-adapter.ts deleted file mode 100644 index 49139425..00000000 --- a/src/agents/adapters/reconciliation/execution-discord-adapter.ts +++ /dev/null @@ -1,305 +0,0 @@ -import type { ExecutionArtifactRef, ExecutionProviderObservation, ExecutionProviderObserveInput, ExecutionRunRef, ExecutionRunSnapshot, ExecutionUsageActual } from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; -import { DISCORD_API_BASE_URL, DiscordExecutionProviderAdapterOptions, DiscordExecutionProviderConfig, DiscordProviderError, DiscordThreadRef, arrayOfRecords, authHeaders, descriptor, discordUrl, mapHttpError, messageFromWorkPackage, record, resolveDiscordExecutionProviderConfig, snapshotFromThread, stringValue, threadName } from '../integrations/execution-discord.ts'; - -export class DiscordExecutionProviderAdapter implements ExecutionProviderAdapter { - constructor(private readonly options: DiscordExecutionProviderAdapterOptions = {}) {} - - private config() { - return this.options.config === undefined ? resolveDiscordExecutionProviderConfig() : this.options.config; - } - - private fetchImpl() { - return this.options.fetchImpl ?? fetch; - } - - async describe() { - return descriptor(); - } - - async observe(_input: ExecutionProviderObserveInput): Promise { - const config = this.config(); - const provider = descriptor(); - if (!config) { - return { - descriptor: provider, - available: false, - pressure: 'exhausted', - blockedReason: 'Discord execution provider is not configured.', - metadata: { configured: false }, - }; - } - return { - descriptor: provider, - available: true, - pressure: 'normal', - activeAssignmentCount: 0, - metadata: { - channelId: config.channelId, - guildId: config.guildId ?? null, - threadPrefix: config.threadPrefix, - configured: true, - }, - }; - } - - async prepare() { - const config = this.config(); - if (!config) { - return { - accepted: false, - summary: 'Discord execution provider is not configured.', - retryable: false, - code: 'discord_provider_not_configured', - }; - } - return { - accepted: true, - summary: `Discord execution provider is configured for channel ${config.channelId}.`, - metadata: { channelId: config.channelId, guildId: config.guildId ?? null }, - }; - } - - async start(input: ExecutionProviderInvocation): Promise { - const config = this.requireConfig(); - const name = threadName(config, input.assignment.id); - const existing = await this.findThreadByName(config, name); - if (existing) { - const messages = await this.fetchThreadMessages(config, existing.threadId).catch(() => []); - return { - ...snapshotFromThread(config, { ...existing, reused: true }, messages), - status: 'waiting', - summary: `Discord thread ${existing.threadName} is waiting for human coordination.`, - metadata: { - provider: 'discord', - channelId: config.channelId, - threadId: existing.threadId, - messageId: existing.messageId ?? null, - reused: true, - }, - }; - } - const message = await this.createMessage(config, input); - const messageId = stringValue(message.id); - if (!messageId) throw new DiscordProviderError('discord_message_payload_invalid', 'Discord message payload did not include a message id.', false); - const thread = await this.createThread(config, messageId, name); - const threadId = stringValue(thread.id); - if (!threadId) throw new DiscordProviderError('discord_thread_payload_invalid', 'Discord thread payload did not include a thread id.', false); - const ref = { - threadId, - messageId, - threadName: stringValue(thread.name) ?? name, - threadUrl: discordUrl(config, threadId), - messageUrl: discordUrl(config, config.channelId, messageId), - reused: false, - }; - return { - status: 'waiting', - summary: `Discord thread ${ref.threadName} is waiting for human coordination.`, - runId: threadId, - externalRef: threadId, - externalUrl: ref.threadUrl, - outputs: { - threadId, - messageId, - threadUrl: ref.threadUrl, - messageUrl: ref.messageUrl, - }, - metadata: { - provider: 'discord', - channelId: config.channelId, - threadId, - messageId, - threadName: ref.threadName, - reused: false, - }, - }; - } - - async poll(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - try { - const ref = this.refFromRun(config, input); - const messages = await this.fetchThreadMessages(config, ref.threadId); - return snapshotFromThread(config, ref, messages); - } catch (error) { - return this.snapshotFromError(error, input); - } - } - - resume(input: ExecutionRunRef) { - return this.poll(input); - } - - async cancel(input: ExecutionRunRef & { reason: string }): Promise { - const config = this.requireConfig(); - const ref = this.refFromRun(config, input); - await this.requestJson(config, `/channels/${encodeURIComponent(ref.threadId)}/messages`, { - method: 'POST', - body: { content: `treeseed: cancel ${input.reason}`.slice(0, 1900) }, - code: 'discord_cancel_message_failed', - }); - return { - status: 'cancelled', - summary: `Discord thread ${ref.threadName} was cancelled for Treeseed assignment execution.`, - runId: input.runId, - externalRef: ref.threadId, - externalUrl: ref.threadUrl, - code: 'discord_thread_cancelled', - retryable: false, - metadata: { - provider: 'discord', - channelId: config.channelId, - threadId: ref.threadId, - }, - }; - } - - async collectUsage(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - const ref = this.refFromRun(config, input); - const messages = await this.fetchThreadMessages(config, ref.threadId); - return messages.length > 0 - ? [{ kind: 'discord_thread_messages', unit: 'count', amount: messages.length, source: 'discord', partial: true }] - : []; - } - - async collectArtifacts(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - const ref = this.refFromRun(config, input); - const messages = await this.fetchThreadMessages(config, ref.threadId); - const artifacts: ExecutionArtifactRef[] = [{ - kind: 'external_issue', - name: ref.threadName, - externalUrl: ref.threadUrl, - metadata: { - provider: 'discord', - channelId: config.channelId, - threadId: ref.threadId, - messageId: ref.messageId ?? null, - }, - }]; - for (const message of messages) { - artifacts.push({ - kind: 'discord_thread_message', - name: `Message ${stringValue(message.id) ?? ''}`.trim(), - externalUrl: discordUrl(config, ref.threadId, stringValue(message.id)), - metadata: { - provider: 'discord', - threadId: ref.threadId, - messageId: stringValue(message.id), - author: stringValue(record(message.author).username, record(message.author).id), - timestamp: stringValue(message.timestamp), - }, - }); - } - return artifacts; - } - - private requireConfig() { - const config = this.config(); - if (!config) throw new DiscordProviderError('discord_provider_not_configured', 'Discord execution provider is not configured.', false); - return config; - } - - private refFromRun(config: DiscordExecutionProviderConfig, input: ExecutionRunRef): DiscordThreadRef { - const metadata = record(input.metadata); - const threadId = stringValue(input.externalRef, metadata.threadId, input.runId); - if (!threadId) throw new DiscordProviderError('discord_thread_ref_missing', 'Discord execution ref did not include a thread id.', false); - const messageId = stringValue(metadata.messageId); - return { - threadId, - messageId, - threadName: stringValue(metadata.threadName) ?? threadId, - threadUrl: input.externalUrl ?? discordUrl(config, threadId), - messageUrl: messageId ? discordUrl(config, config.channelId, messageId) : null, - }; - } - - private async requestJson(config: DiscordExecutionProviderConfig, path: string, input: { - method: string; - body?: unknown; - code: string; - }): Promise { - const response = await this.fetchImpl()(`${DISCORD_API_BASE_URL}${path}`, { - method: input.method, - headers: authHeaders(config), - body: input.body === undefined ? undefined : JSON.stringify(input.body), - }); - if (!response.ok) throw mapHttpError(response.status, input.code); - if (response.status === 204) return {}; - return response.json().catch(() => ({})) as Promise; - } - - private async createMessage(config: DiscordExecutionProviderConfig, input: ExecutionProviderInvocation) { - return record(await this.requestJson(config, `/channels/${encodeURIComponent(config.channelId)}/messages`, { - method: 'POST', - body: { content: messageFromWorkPackage(input) }, - code: 'discord_message_create_failed', - })); - } - - private async createThread(config: DiscordExecutionProviderConfig, messageId: string, name: string) { - return record(await this.requestJson(config, `/channels/${encodeURIComponent(config.channelId)}/messages/${encodeURIComponent(messageId)}/threads`, { - method: 'POST', - body: { name, auto_archive_duration: 1440 }, - code: 'discord_thread_create_failed', - })); - } - - private async fetchThreadMessages(config: DiscordExecutionProviderConfig, threadId: string) { - const payload = await this.requestJson(config, `/channels/${encodeURIComponent(threadId)}/messages?limit=50`, { - method: 'GET', - code: 'discord_thread_messages_fetch_failed', - }); - return arrayOfRecords(payload); - } - - private async findThreadByName(config: DiscordExecutionProviderConfig, name: string): Promise { - const active = await this.requestJson(config, `/guilds/${encodeURIComponent(config.guildId ?? '@me')}/threads/active`, { - method: 'GET', - code: 'discord_active_threads_fetch_failed', - }).catch(() => null); - const activeMatch = arrayOfRecords(record(active).threads).find((thread) => stringValue(thread.name) === name); - if (activeMatch) { - const threadId = stringValue(activeMatch.id); - return threadId ? { threadId, threadName: name, threadUrl: discordUrl(config, threadId) } : null; - } - const archived = await this.requestJson(config, `/channels/${encodeURIComponent(config.channelId)}/threads/archived/public?limit=100`, { - method: 'GET', - code: 'discord_archived_threads_fetch_failed', - }).catch(() => null); - const archivedMatch = arrayOfRecords(record(archived).threads).find((thread) => stringValue(thread.name) === name); - if (archivedMatch) { - const threadId = stringValue(archivedMatch.id); - return threadId ? { threadId, threadName: name, threadUrl: discordUrl(config, threadId) } : null; - } - return null; - } - - private snapshotFromError(error: unknown, input: ExecutionRunRef): ExecutionRunSnapshot { - if (error instanceof DiscordProviderError) { - const returned = error.code === 'discord_ref_missing'; - return { - status: returned ? 'returned' : error.retryable ? 'waiting' : 'failed', - summary: error.message, - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: error.retryable, - code: error.code, - metadata: { provider: 'discord', status: error.status }, - }; - } - return { - status: 'failed', - summary: 'Discord provider failed with an unexpected error.', - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: false, - code: 'discord_unexpected_error', - metadata: { provider: 'discord' }, - }; - } -} diff --git a/src/agents/adapters/reconciliation/execution-github-issues-adapter.ts b/src/agents/adapters/reconciliation/execution-github-issues-adapter.ts deleted file mode 100644 index 3fb731fe..00000000 --- a/src/agents/adapters/reconciliation/execution-github-issues-adapter.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { ExecutionArtifactRef, ExecutionProviderObservation, ExecutionProviderObserveInput, ExecutionRunRef, ExecutionRunSnapshot, ExecutionUsageActual } from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; -import { GitHubIssuesExecutionProviderAdapterOptions, GitHubIssuesExecutionProviderConfig, GitHubIssuesProviderError, assignmentLabel, authHeaders, commentArtifacts, descriptor, issueBodyFromWorkPackage, issueSnapshot, issueUrl, linkedRefsFromText, mapHttpError, normalizeIssue, record, repositoryParts, resolveGitHubIssuesExecutionProviderConfig, stringValue } from '../repositories/execution-github-issues.ts'; - -export class GitHubIssueExecutionProviderAdapter implements ExecutionProviderAdapter { - constructor(private readonly options: GitHubIssuesExecutionProviderAdapterOptions = {}) {} - - private config() { - return this.options.config === undefined ? resolveGitHubIssuesExecutionProviderConfig() : this.options.config; - } - - private fetchImpl() { - return this.options.fetchImpl ?? fetch; - } - - async describe() { - return descriptor(); - } - - async observe(_input: ExecutionProviderObserveInput): Promise { - const config = this.config(); - const provider = descriptor(); - if (!config) { - return { - descriptor: provider, - available: false, - pressure: 'exhausted', - blockedReason: 'GitHub Issues execution provider is not configured.', - metadata: { configured: false }, - }; - } - return { - descriptor: provider, - available: true, - pressure: 'normal', - activeAssignmentCount: 0, - metadata: { - repository: config.repository, - configured: true, - }, - }; - } - - async prepare() { - const config = this.config(); - if (!config) { - return { - accepted: false, - summary: 'GitHub Issues execution provider is not configured.', - retryable: false, - code: 'github_issues_provider_not_configured', - }; - } - return { - accepted: true, - summary: `GitHub Issues execution provider is configured for ${config.repository}.`, - metadata: { repository: config.repository }, - }; - } - - async start(input: ExecutionProviderInvocation): Promise { - const config = this.requireConfig(); - const label = assignmentLabel(input.assignment.id); - const existing = await this.findIssueByAssignmentLabel(config, label); - if (existing) { - return { - ...issueSnapshot(config, existing), - status: issueSnapshot(config, existing).status === 'completed' ? 'completed' : 'waiting', - summary: `GitHub issue #${existing.number} is waiting for human execution.`, - metadata: { - provider: 'github_issues', - repository: config.repository, - assignmentId: input.assignment.id, - issueNumber: existing.number, - reused: true, - }, - }; - } - const created = await this.createIssue(config, input, label); - return { - status: 'waiting', - summary: `GitHub issue #${created.number} is waiting for human execution.`, - runId: String(created.number), - externalRef: String(created.number), - externalUrl: created.htmlUrl, - outputs: { - issueNumber: created.number, - issueUrl: created.htmlUrl, - repository: config.repository, - labels: created.labels, - }, - metadata: { - provider: 'github_issues', - repository: config.repository, - assignmentId: input.assignment.id, - issueNumber: created.number, - reused: false, - }, - }; - } - - async poll(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - try { - const issue = await this.fetchIssue(config, input.externalRef ?? input.runId); - return issueSnapshot(config, issue); - } catch (error) { - return this.snapshotFromError(error, input); - } - } - - resume(input: ExecutionRunRef) { - return this.poll(input); - } - - async cancel(input: ExecutionRunRef & { reason: string }): Promise { - const config = this.requireConfig(); - const issueNumber = input.externalRef ?? input.runId; - await this.requestJson(config, `/issues/${encodeURIComponent(issueNumber)}/comments`, { - method: 'POST', - body: { body: `Treeseed cancelled this assignment: ${input.reason}` }, - code: 'github_issue_cancel_comment_failed', - }).catch(() => null); - await this.requestJson(config, `/issues/${encodeURIComponent(issueNumber)}`, { - method: 'PATCH', - body: { state: 'closed', labels: [...new Set([...config.labels, ...config.cancelledLabels])] }, - code: 'github_issue_cancel_failed', - }); - return { - status: 'cancelled', - summary: `GitHub issue #${issueNumber} was cancelled for Treeseed assignment execution.`, - runId: input.runId, - externalRef: issueNumber, - externalUrl: issueUrl(config, Number(issueNumber)), - code: 'github_issue_cancelled', - retryable: false, - metadata: { - provider: 'github_issues', - repository: config.repository, - issueNumber, - }, - }; - } - - async collectUsage(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - const issue = await this.fetchIssue(config, input.externalRef ?? input.runId); - const usage: ExecutionUsageActual[] = []; - if (issue.comments && issue.comments.length > 0) { - usage.push({ kind: 'github_issue_comments', unit: 'count', amount: issue.comments.length, source: 'github_issues', partial: true }); - } - if (issue.labels.length > 0) { - usage.push({ kind: 'github_issue_labels', unit: 'count', amount: issue.labels.length, source: 'github_issues', partial: true }); - } - return usage; - } - - async collectArtifacts(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - const issue = await this.fetchIssue(config, input.externalRef ?? input.runId); - const artifacts: ExecutionArtifactRef[] = [{ - kind: 'external_issue', - name: `#${issue.number}`, - externalUrl: issue.htmlUrl, - metadata: { - provider: 'github_issues', - repository: config.repository, - state: issue.state, - labels: issue.labels, - }, - }]; - artifacts.push(...commentArtifacts(issue)); - for (const ref of linkedRefsFromText([issue.body, ...(issue.comments ?? []).map((comment) => stringValue(comment.body))].filter(Boolean).join('\n'))) { - artifacts.push({ - kind: 'github_issue_link', - name: ref, - externalUrl: ref.startsWith('http') ? ref : issue.htmlUrl, - metadata: { - provider: 'github_issues', - repository: config.repository, - issueNumber: issue.number, - }, - }); - } - return artifacts; - } - - private requireConfig() { - const config = this.config(); - if (!config) throw new GitHubIssuesProviderError('github_issues_provider_not_configured', 'GitHub Issues execution provider is not configured.', false); - repositoryParts(config.repository); - return config; - } - - private async requestJson(config: GitHubIssuesExecutionProviderConfig, path: string, input: { - method: string; - body?: unknown; - code: string; - }): Promise> { - const { owner, repo } = repositoryParts(config.repository); - const response = await this.fetchImpl()(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}${path}`, { - method: input.method, - headers: authHeaders(config), - body: input.body === undefined ? undefined : JSON.stringify(input.body), - }); - if (!response.ok) throw mapHttpError(response.status, input.code); - if (response.status === 204) return {}; - return response.json().catch(() => ({})) as Promise>; - } - - private async findIssueByAssignmentLabel(config: GitHubIssuesExecutionProviderConfig, label: string) { - const search = new URLSearchParams({ - state: 'all', - labels: label, - per_page: '100', - }); - const payload = await this.requestJson(config, `/issues?${search.toString()}`, { - method: 'GET', - code: 'github_issues_search_failed', - }) as unknown; - const issue = Array.isArray(payload) ? payload.map(record).find((candidate) => !candidate.pull_request) : null; - return issue ? normalizeIssue(config, issue) : null; - } - - private async createIssue(config: GitHubIssuesExecutionProviderConfig, input: ExecutionProviderInvocation, label: string) { - const payload = await this.requestJson(config, '/issues', { - method: 'POST', - body: { - title: input.workPackage.title, - body: issueBodyFromWorkPackage(input), - labels: [...new Set([...config.labels, label, 'treeseed-assignment'])], - }, - code: 'github_issue_create_failed', - }); - return normalizeIssue(config, payload); - } - - private async fetchIssue(config: GitHubIssuesExecutionProviderConfig, issueNumber: string | number) { - const issue = normalizeIssue(config, await this.requestJson(config, `/issues/${encodeURIComponent(String(issueNumber))}`, { - method: 'GET', - code: 'github_issue_fetch_failed', - })); - const commentsPayload = await this.requestJson(config, `/issues/${encodeURIComponent(String(issueNumber))}/comments?per_page=100`, { - method: 'GET', - code: 'github_issue_comments_fetch_failed', - }).catch(() => []); - return { - ...issue, - comments: Array.isArray(commentsPayload) ? commentsPayload.map(record) : [], - }; - } - - private snapshotFromError(error: unknown, input: ExecutionRunRef): ExecutionRunSnapshot { - if (error instanceof GitHubIssuesProviderError) { - const returned = error.code === 'github_issue_missing'; - return { - status: returned ? 'returned' : error.retryable ? 'waiting' : 'failed', - summary: error.message, - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: error.retryable, - code: error.code, - metadata: { provider: 'github_issues', status: error.status }, - }; - } - return { - status: 'failed', - summary: 'GitHub Issues provider failed with an unexpected error.', - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: false, - code: 'github_issues_unexpected_error', - metadata: { provider: 'github_issues' }, - }; - } -} diff --git a/src/agents/adapters/reconciliation/execution-jira-adapter.ts b/src/agents/adapters/reconciliation/execution-jira-adapter.ts deleted file mode 100644 index 3c3f4745..00000000 --- a/src/agents/adapters/reconciliation/execution-jira-adapter.ts +++ /dev/null @@ -1,382 +0,0 @@ -import type { ExecutionArtifactRef, ExecutionProviderObservation, ExecutionProviderObserveInput, ExecutionRunRef, ExecutionRunSnapshot, ExecutionUsageActual } from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; -import { JIRA_ASSIGNMENT_PROPERTY, JiraExecutionProviderAdapterOptions, JiraExecutionProviderConfig, JiraProviderError, arrayOfRecords, assignmentLabel, authHeader, descriptor, issueSnapshot, issueUrl, jiraDocFromWorkPackage, mapHttpError, normalizeIssue, record, resolveJiraExecutionProviderConfig, safeAttachmentRefs, safeCommentBodies, safeLinkRefs, stringValue, textDoc } from '../integrations/execution-jira.ts'; - -export class JiraExecutionProviderAdapter implements ExecutionProviderAdapter { - constructor(private readonly options: JiraExecutionProviderAdapterOptions = {}) {} - - private config() { - return this.options.config === undefined ? resolveJiraExecutionProviderConfig() : this.options.config; - } - - private fetchImpl() { - return this.options.fetchImpl ?? fetch; - } - - async describe() { - return descriptor(); - } - - async observe(_input: ExecutionProviderObserveInput): Promise { - const config = this.config(); - const provider = descriptor(); - if (!config) { - return { - descriptor: provider, - available: false, - pressure: 'exhausted', - blockedReason: 'Jira execution provider is not configured.', - metadata: { configured: false }, - }; - } - return { - descriptor: provider, - available: true, - pressure: 'normal', - activeAssignmentCount: 0, - metadata: { - projectKey: config.projectKey, - baseUrl: config.baseUrl, - configured: true, - }, - }; - } - - async prepare() { - const config = this.config(); - if (!config) { - return { - accepted: false, - summary: 'Jira execution provider is not configured.', - retryable: false, - code: 'jira_provider_not_configured', - }; - } - return { - accepted: true, - summary: `Jira execution provider is configured for ${config.projectKey}.`, - metadata: { - projectKey: config.projectKey, - baseUrl: config.baseUrl, - }, - }; - } - - async start(input: ExecutionProviderInvocation): Promise { - const config = this.requireConfig(); - const label = assignmentLabel(input.assignment.id); - const existing = await this.findIssueByAssignmentLabel(config, label); - if (existing) { - return { - status: 'waiting', - summary: `Jira issue ${existing.key} is waiting for human execution.`, - runId: existing.key, - externalRef: existing.key, - externalUrl: existing.url, - outputs: { - issueKey: existing.key, - issueUrl: existing.url, - jiraStatus: existing.status, - projectKey: config.projectKey, - }, - metadata: { - provider: 'jira', - assignmentId: input.assignment.id, - issueKey: existing.key, - issueStatus: existing.status, - reused: true, - }, - }; - } - const created = await this.createIssue(config, input, label); - await this.setAssignmentProperty(config, created.key, input); - return { - status: 'waiting', - summary: `Jira issue ${created.key} is waiting for human execution.`, - runId: created.key, - externalRef: created.key, - externalUrl: created.url, - outputs: { - issueKey: created.key, - issueUrl: created.url, - jiraStatus: created.status, - projectKey: config.projectKey, - }, - metadata: { - provider: 'jira', - assignmentId: input.assignment.id, - issueKey: created.key, - issueStatus: created.status, - reused: false, - }, - }; - } - - async poll(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - try { - const issue = await this.fetchIssue(config, input.externalRef ?? input.runId); - return issueSnapshot(config, issue); - } catch (error) { - return this.snapshotFromError(error, input); - } - } - - resume(input: ExecutionRunRef): Promise { - return this.poll(input); - } - - async cancel(input: ExecutionRunRef & { reason: string }): Promise { - const config = this.requireConfig(); - const issueKey = input.externalRef ?? input.runId; - let transitionApplied = false; - const transitions = await this.requestJson(config, `/rest/api/3/issue/${encodeURIComponent(issueKey)}/transitions`, { - method: 'GET', - code: 'jira_transition_lookup_failed', - }).catch(() => ({ transitions: [] })); - const transition = arrayOfRecords(record(transitions).transitions).find((candidate) => { - const name = `${stringValue(candidate.name) ?? ''} ${stringValue(record(record(candidate.to).statusCategory).name) ?? ''} ${stringValue(record(candidate.to).name) ?? ''}`.toLowerCase(); - return ['cancel', "won't do", 'wont do', 'decline', 'close'].some((needle) => name.includes(needle)); - }); - if (transition && stringValue(transition.id)) { - await this.requestJson(config, `/rest/api/3/issue/${encodeURIComponent(issueKey)}/transitions`, { - method: 'POST', - body: { transition: { id: stringValue(transition.id) } }, - code: 'jira_transition_failed', - }); - transitionApplied = true; - } - await this.requestJson(config, `/rest/api/3/issue/${encodeURIComponent(issueKey)}/comment`, { - method: 'POST', - body: { body: textDoc(`Treeseed cancelled this assignment: ${input.reason}`) }, - code: 'jira_cancel_comment_failed', - }).catch(() => null); - return { - status: 'cancelled', - summary: `Jira issue ${issueKey} was cancelled for Treeseed assignment execution.`, - runId: input.runId, - externalRef: issueKey, - externalUrl: issueUrl(config, issueKey), - code: 'jira_issue_cancelled', - retryable: false, - metadata: { - provider: 'jira', - issueKey, - transitionApplied, - }, - }; - } - - async collectUsage(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - const issue = await this.fetchIssue(config, input.externalRef ?? input.runId); - const usage: ExecutionUsageActual[] = []; - const seconds = typeof issue.timeTracking?.timeSpentSeconds === 'number' ? issue.timeTracking.timeSpentSeconds : 0; - if (seconds > 0) { - usage.push({ - kind: 'jira_time_spent', - unit: 'second', - amount: seconds, - source: 'jira', - partial: false, - }); - } - if (config.storyPointsField) { - const value = issue.fields?.[config.storyPointsField]; - const amount = typeof value === 'number' ? value : Number.NaN; - if (Number.isFinite(amount) && amount > 0) { - usage.push({ - kind: 'jira_story_points', - unit: 'story_point', - amount, - source: 'jira', - partial: true, - }); - } - } - return usage; - } - - async collectArtifacts(input: ExecutionRunRef): Promise { - const config = this.requireConfig(); - const issue = await this.fetchIssue(config, input.externalRef ?? input.runId); - const artifacts: ExecutionArtifactRef[] = [{ - kind: 'external_issue', - name: issue.key, - externalUrl: issue.url, - metadata: { - provider: 'jira', - status: issue.status, - }, - }]; - for (const attachment of safeAttachmentRefs(issue)) { - artifacts.push({ - kind: 'jira_attachment', - name: attachment.filename ?? attachment.id ?? null, - externalUrl: attachment.content ?? null, - mediaType: attachment.mimeType ?? null, - metadata: { - provider: 'jira', - issueKey: issue.key, - attachmentId: attachment.id ?? null, - size: attachment.size ?? null, - }, - }); - } - for (const comment of safeCommentBodies(issue)) { - artifacts.push({ - kind: 'jira_comment', - name: comment.id ? `Comment ${comment.id}` : null, - externalUrl: issue.url, - metadata: { - provider: 'jira', - issueKey: issue.key, - commentId: comment.id, - }, - }); - } - for (const link of safeLinkRefs(issue)) { - artifacts.push({ - kind: 'jira_link', - name: link.type ?? link.id, - externalUrl: null, - metadata: { - provider: 'jira', - issueKey: issue.key, - ...link, - }, - }); - } - return artifacts; - } - - private requireConfig() { - const config = this.config(); - if (!config) { - throw new JiraProviderError('jira_provider_not_configured', 'Jira execution provider is not configured.', false); - } - return config; - } - - private async requestJson(config: JiraExecutionProviderConfig, path: string, input: { - method: string; - body?: unknown; - code: string; - }): Promise> { - const response = await this.fetchImpl()(`${config.baseUrl}${path}`, { - method: input.method, - headers: { - accept: 'application/json', - 'content-type': 'application/json', - authorization: authHeader(config), - }, - body: input.body === undefined ? undefined : JSON.stringify(input.body), - }); - if (!response.ok) { - throw mapHttpError(response.status, input.code); - } - if (response.status === 204) return {}; - return response.json().catch(() => ({})) as Promise>; - } - - private async findIssueByAssignmentLabel(config: JiraExecutionProviderConfig, label: string) { - const jql = `project = ${config.projectKey} AND labels = "${label}"`; - const search = new URLSearchParams({ - jql, - fields: 'summary,status,assignee,timetracking,labels', - }); - const payload = await this.requestJson(config, `/rest/api/3/search?${search.toString()}`, { - method: 'GET', - code: 'jira_search_failed', - }); - const issue = arrayOfRecords(payload.issues).at(0); - return issue ? normalizeIssue(config, issue) : null; - } - - private async createIssue(config: JiraExecutionProviderConfig, input: ExecutionProviderInvocation, label: string) { - const payload = { - fields: { - project: { key: config.projectKey }, - issuetype: { name: config.issueType }, - summary: input.workPackage.title, - description: jiraDocFromWorkPackage(input), - labels: [label, 'treeseed-assignment'], - }, - }; - const issue = await this.requestJson(config, '/rest/api/3/issue', { - method: 'POST', - body: payload, - code: 'jira_issue_create_failed', - }); - const key = stringValue(issue.key); - if (!key) throw new JiraProviderError('jira_payload_invalid', 'Created Jira issue did not include an issue key.', false); - return { - key, - url: issueUrl(config, key), - status: 'To Do', - }; - } - - private async setAssignmentProperty(config: JiraExecutionProviderConfig, issueKey: string, input: ExecutionProviderInvocation) { - await this.requestJson(config, `/rest/api/3/issue/${encodeURIComponent(issueKey)}/properties/${JIRA_ASSIGNMENT_PROPERTY}`, { - method: 'PUT', - body: { - assignmentId: input.assignment.id, - capacityProviderId: input.assignment.capacityProviderId, - executionProviderId: input.assignment.executionProviderId ?? null, - runnerId: input.runnerId, - mode: input.assignment.mode, - workPackageKind: input.workPackage.kind, - createdBy: 'treeseed', - }, - code: 'jira_assignment_property_failed', - }); - } - - private async fetchIssue(config: JiraExecutionProviderConfig, issueKey: string) { - const fields = [ - 'summary', - 'status', - 'assignee', - 'comment', - 'attachment', - 'issuelinks', - 'timetracking', - 'labels', - config.storyPointsField, - ].filter(Boolean).join(','); - const payload = await this.requestJson(config, `/rest/api/3/issue/${encodeURIComponent(issueKey)}?fields=${encodeURIComponent(fields)}`, { - method: 'GET', - code: 'jira_issue_fetch_failed', - }); - return normalizeIssue(config, payload); - } - - private snapshotFromError(error: unknown, input: ExecutionRunRef): ExecutionRunSnapshot { - if (error instanceof JiraProviderError) { - return { - status: error.code === 'jira_issue_missing' ? 'returned' : error.retryable ? 'waiting' : 'failed', - summary: error.message, - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: error.retryable, - code: error.code, - metadata: { - provider: 'jira', - status: error.status ?? null, - }, - }; - } - return { - status: 'waiting', - summary: 'Jira provider network request failed.', - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: true, - code: 'jira_network_error', - metadata: { provider: 'jira' }, - }; - } -} diff --git a/src/agents/adapters/reconciliation/execution-timeout.ts b/src/agents/adapters/reconciliation/execution-timeout.ts deleted file mode 100644 index 678a8be2..00000000 --- a/src/agents/adapters/reconciliation/execution-timeout.ts +++ /dev/null @@ -1,92 +0,0 @@ -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export function capacityExecutionTiming(envelopeValue: unknown, nowMs = Date.now()) { - const envelope = record(envelopeValue); - const budget = record(envelope.budget); - const time = record(budget.time); - const deadlineAt = typeof time.authorityDeadlineAt === 'string'?time.authorityDeadlineAt:typeof time.hardDeadlineAt === 'string' ? time.hardDeadlineAt - : typeof budget.deadline === 'string' ? budget.deadline : null; - const closeoutWarningSeconds = Number(time.closeoutWarningSeconds); - return deadlineAt ? { - ...time, - deadlineAt, - allocatedSeconds: Number(envelope.reservedSeconds ?? envelope.requestedSeconds) || null, - remainingSeconds: Math.max(0, Math.floor((Date.parse(deadlineAt) - nowMs) / 1_000)), - closeoutWarningSeconds: Number.isInteger(closeoutWarningSeconds) && closeoutWarningSeconds > 0 ? closeoutWarningSeconds : 180, - } : null; -} - -export function assignmentExecutionTiming( - timingValue: unknown, - assignment: { claimedAt?: string | null; assignedAt?: string | null }, -) { - if (!timingValue || typeof timingValue !== 'object' || Array.isArray(timingValue)) return timingValue; - const timing = timingValue as Record; - return { - ...timing, - startedAt: typeof timing.startedAt === 'string' ? timing.startedAt : assignment.claimedAt ?? assignment.assignedAt, - }; -} - -export function codexExecutionTimeoutMs(providerTimeoutMs: number, activityTimeoutSeconds: number | null | undefined) { - const activityTimeoutMs = Number(activityTimeoutSeconds) * 1_000; - return Number.isFinite(activityTimeoutMs) && activityTimeoutMs > 0 - ? Math.min(providerTimeoutMs, activityTimeoutMs) - : providerTimeoutMs; -} - -export function deadlineBoundExecutionTimeoutMs( - configuredTimeoutMs: number, - timing: unknown, - nowMs = Date.now(), -) { - const timingRecord = record(timing); - const deadlineAt = Date.parse(String(timingRecord.deadlineAt ?? '')); - const remainingMs = deadlineAt - nowMs; - const authorityDeadline=Date.parse(String(timingRecord.authorityDeadlineAt??'')); - if(Number.isFinite(authorityDeadline)) return Math.max(1,Math.min(configuredTimeoutMs,authorityDeadline-nowMs-30_000)); - const configuredCloseoutSeconds = Number(timingRecord.closeoutWarningSeconds); - const closeoutMs = Number.isInteger(configuredCloseoutSeconds) && configuredCloseoutSeconds > 0 - ? configuredCloseoutSeconds * 1_000 - : 180_000; - const completionCorrectionLeadMs = Math.min(120_000, Math.max(30_000, Math.floor(remainingMs / 4))); - const terminalizationReserveMs = Math.min(closeoutMs + completionCorrectionLeadMs, Math.max(0, remainingMs - 1)); - return Number.isFinite(deadlineAt) - ? Math.max(1, Math.min(configuredTimeoutMs, remainingMs - terminalizationReserveMs)) - : configuredTimeoutMs; -} - -export function executionCompletionDeadlineMs(configuredTimeoutMs: number, timing: unknown, nowMs = Date.now()) { - const source=record(timing); const deadlineAt = Date.parse(String(source.authorityDeadlineAt??source.closeoutDeadlineAt??source.deadlineAt ?? '')); - const configuredDeadline = nowMs + configuredTimeoutMs; - // Agent time ends before assignment authority so the provider can close the - // TreeDX workspace and report the durable terminal state while its lease is - // still valid. This reserve belongs to provider finalization, not model work. - const providerTerminalizationDeadline = deadlineAt - 30_000; - return Number.isFinite(deadlineAt) - ? Math.max(nowMs + 1, Math.min(configuredDeadline, providerTerminalizationDeadline)) - : configuredDeadline; -} - -export function assignmentCloseoutBegun(timing: unknown, nowMs = Date.now()) { - const timingRecord = record(timing); - const executionDeadlineAt=Date.parse(String(timingRecord.executionDeadlineAt??'')); - if(Number.isFinite(executionDeadlineAt)) return nowMs>=executionDeadlineAt; - const deadlineAt = Date.parse(String(timingRecord.deadlineAt ?? '')); - const warningMs = Number(timingRecord.closeoutWarningSeconds ?? 180) * 1_000; - return Number.isFinite(deadlineAt) && nowMs >= deadlineAt - warningMs; -} - -export function completionCorrectionBoundaryInstruction(timing: unknown) { - return assignmentCloseoutBegun(timing) - ? 'The assignment is now in closeout. Do not explore or expand scope; preserve useful work, write the terminal status and summary, validate, and commit before responding.' - : 'This is the bounded completion-correction window before mandatory closeout. Do not explore or expand scope. Create or repair only the already-required artifact first, then validate, commit, and write terminal status and summary.'; -} - -export function assignmentDeadlineExpired(timing: unknown, nowMs = Date.now()): boolean { - if (!timing || typeof timing !== 'object' || Array.isArray(timing)) return false; - const source=timing as Record; const deadlineAt = Date.parse(String(source.closeoutDeadlineAt??source.deadlineAt ?? '')); - return Number.isFinite(deadlineAt) && deadlineAt <= nowMs; -} diff --git a/src/agents/adapters/repositories/execution-github-issues.ts b/src/agents/adapters/repositories/execution-github-issues.ts deleted file mode 100644 index 059f0594..00000000 --- a/src/agents/adapters/repositories/execution-github-issues.ts +++ /dev/null @@ -1,358 +0,0 @@ -import type { - ExecutionArtifactRef, - ExecutionProviderDescriptor, - ExecutionProviderObserveInput, - ExecutionProviderObservation, - ExecutionRunRef, - ExecutionRunSnapshot, - ExecutionUsageActual, -} from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderAdapter, ExecutionProviderInvocation } from '../../runtime/runtime-types.ts'; - -export interface GitHubIssuesExecutionProviderConfig { - token: string; - repository: string; - labels: string[]; - inProgressLabels: string[]; - blockedLabels: string[]; - cancelledLabels: string[]; -} - -export interface GitHubIssueSnapshot { - number: number; - id?: number | null; - state: string; - title?: string | null; - body?: string | null; - htmlUrl: string; - labels: string[]; - assignee?: string | null; - comments?: Array>; - createdAt?: string | null; - updatedAt?: string | null; - closedAt?: string | null; -} - -export interface GitHubIssuesExecutionProviderAdapterOptions { - config?: GitHubIssuesExecutionProviderConfig | null; - fetchImpl?: typeof fetch; -} - -export class GitHubIssuesProviderError extends Error { - constructor( - readonly code: string, - message: string, - readonly retryable = true, - readonly status?: number, - ) { - super(message); - this.name = 'GitHubIssuesProviderError'; - } -} - -export const DEFAULT_LABELS = ['treeseed']; -export const DEFAULT_IN_PROGRESS_LABELS = ['treeseed-in-progress']; -export const DEFAULT_BLOCKED_LABELS = ['treeseed-blocked']; -export const DEFAULT_CANCELLED_LABELS = ['treeseed-cancelled']; - -export function envValue(env: NodeJS.ProcessEnv, name: string) { - return env[name]?.trim() || ''; -} - -export function parseList(value: string, fallback: string[]) { - const values = value.split(',').map((entry) => entry.trim()).filter(Boolean); - return values.length ? values : fallback; -} - -export function resolveGitHubIssuesExecutionProviderConfig(env: NodeJS.ProcessEnv = process.env): GitHubIssuesExecutionProviderConfig | null { - const token = envValue(env, 'TREESEED_GITHUB_ISSUES_TOKEN'); - const repository = envValue(env, 'TREESEED_GITHUB_ISSUES_REPOSITORY'); - if (!token || !repository) return null; - return { - token, - repository, - labels: parseList(envValue(env, 'TREESEED_GITHUB_ISSUES_LABELS'), DEFAULT_LABELS), - inProgressLabels: parseList(envValue(env, 'TREESEED_GITHUB_ISSUES_IN_PROGRESS_LABELS'), DEFAULT_IN_PROGRESS_LABELS), - blockedLabels: parseList(envValue(env, 'TREESEED_GITHUB_ISSUES_BLOCKED_LABELS'), DEFAULT_BLOCKED_LABELS), - cancelledLabels: parseList(envValue(env, 'TREESEED_GITHUB_ISSUES_CANCELLED_LABELS'), DEFAULT_CANCELLED_LABELS), - }; -} - -export function descriptor(): ExecutionProviderDescriptor { - return { - id: 'github_issues', - kind: 'human_issue_queue', - capabilities: [ - 'human_review', - 'manual_execution', - 'qa_validation', - 'project_management', - 'issue_queue', - 'github_issue_queue', - 'planning', - 'implementation', - 'review', - 'test', - ], - capabilityAliases: ['github_issues', 'github_issue_queue', 'issue_queue'], - nativeUnit: 'issue_activity', - quotaVisibility: 'partial', - maxConcurrentAssignments: 1, - supportsAsync: true, - supportsCancel: true, - supportsResume: true, - supportsUsage: true, - supportsArtifacts: true, - }; -} - -export function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export function arrayOfRecords(value: unknown): Array> { - return Array.isArray(value) ? value.map(record).filter((entry) => Object.keys(entry).length > 0) : []; -} - -export function stringValue(...values: unknown[]) { - for (const value of values) { - if (typeof value === 'string' && value.trim()) return value.trim(); - if (typeof value === 'number' && Number.isFinite(value)) return String(value); - } - return null; -} - -export function sanitizeLabel(value: string) { - return value.toLowerCase().replace(/[^a-z0-9_.-]+/gu, '-').replace(/^-+|-+$/gu, '').slice(0, 120) || 'assignment'; -} - -export function assignmentLabel(assignmentId: string) { - return `treeseed-assignment-${sanitizeLabel(assignmentId)}`; -} - -export function repositoryParts(repository: string) { - const [owner, repo] = repository.split('/'); - if (!owner || !repo || repository.split('/').length !== 2) { - throw new GitHubIssuesProviderError('github_issues_repository_invalid', 'GitHub Issues provider repository must be in owner/repo form.', false); - } - return { owner, repo }; -} - -export function issueUrl(config: GitHubIssuesExecutionProviderConfig, issueNumber: number) { - return `https://github.com/${config.repository}/issues/${issueNumber}`; -} - -export function redactSensitive(value: unknown, key = ''): unknown { - if (key && /(?:token|authorization|password|credential|api[_-]?key|private[_-]?key)/iu.test(key)) return ''; - if (typeof value === 'string') { - if (/(?:gh[psuor]_[A-Za-z0-9_]+|secret_should_not_leak|github_secret)/u.test(value)) return ''; - return value; - } - if (Array.isArray(value)) return value.map((entry) => redactSensitive(entry)); - if (value && typeof value === 'object') { - return Object.fromEntries(Object.entries(value as Record).map(([entryKey, entryValue]) => [ - entryKey, - redactSensitive(entryValue, entryKey), - ])); - } - return value; -} - -export function fieldSummary(value: unknown) { - try { - return JSON.stringify(redactSensitive(value), null, 2); - } catch { - return String(value ?? ''); - } -} - -export function agentToolInstructions(input: ExecutionProviderInvocation) { - const tools = (input.tools ?? []).filter((tool) => tool.kind === 'agent_tool'); - if (tools.length === 0) return null; - return [ - '## Available TreeSeed tools', - 'Use only these assignment-scoped tools when tool use is needed. Do not request or paste raw credentials in GitHub.', - '', - 'Available tools:', - tools.map((tool) => [ - `### ${tool.name}`, - `Tool id: ${tool.id}`, - `Execution target: ${tool.executionTarget}`, - `Mutability: ${tool.mutability}`, - `Input schema:\n\`\`\`json\n${fieldSummary(tool.inputSchema)}\n\`\`\``, - ].join('\n\n')).join('\n\n'), - 'Expected completion artifact format:', - '```json', - fieldSummary({ - treeDxWorkspaceId: '', - treeDxCommitId: '', - changedPaths: [''], - summary: '', - verificationEvidence: '', - }), - '```', - ].join('\n\n'); -} - -export function issueBodyFromWorkPackage(input: ExecutionProviderInvocation) { - const handles = record(input.assignment.capabilityHandles); - const workspace = record(input.assignment.workspaceContext); - const safeAssignment = { - id: input.assignment.id, - projectId: input.assignment.projectId, - teamId: input.assignment.teamId, - capacityProviderId: input.assignment.capacityProviderId, - executionProviderId: input.assignment.executionProviderId ?? null, - mode: input.assignment.mode, - capabilityHandleKinds: Object.keys(handles), - workspaceContextKeys: Object.keys(workspace).filter((key) => !/token|secret|credential|handle/iu.test(key)), - }; - return [ - `## Summary\n${input.workPackage.summary}`, - `## Instructions\n${input.workPackage.instructions}`, - `## Expected outputs\n\`\`\`json\n${fieldSummary(input.workPackage.expectedOutputs)}\n\`\`\``, - `## Constraints\n\`\`\`json\n${fieldSummary(input.workPackage.constraints)}\n\`\`\``, - `## Assignment\n\`\`\`json\n${fieldSummary(safeAssignment)}\n\`\`\``, - `## Capacity envelope\n\`\`\`json\n${fieldSummary(input.capacityEnvelope)}\n\`\`\``, - `## Decision input\n\`\`\`json\n${fieldSummary(input.decisionInput.input)}\n\`\`\``, - `## Allowed paths\n${(input.workPackage.constraints.allowedPaths ?? input.agent.execution.allowedPaths ?? []).join('\n') || ''}`, - `## Forbidden paths\n${(input.workPackage.constraints.forbiddenPaths ?? input.agent.execution.forbiddenPaths ?? []).join('\n') || ''}`, - agentToolInstructions(input), - '', - ].filter(Boolean).join('\n\n'); -} - -export function authHeaders(config: GitHubIssuesExecutionProviderConfig) { - return { - accept: 'application/vnd.github+json', - authorization: `Bearer ${config.token}`, - 'content-type': 'application/json', - 'user-agent': 'treeseed-agent-provider', - 'x-github-api-version': '2022-11-28', - }; -} - -export function safeErrorMessage(status: number, code: string) { - if (status === 401 || status === 403) return 'GitHub Issues authentication failed.'; - if (status === 404) return 'GitHub issue was not found.'; - if (status === 410) return 'GitHub Issues are disabled for the repository.'; - if (status === 429 || status >= 500) return 'GitHub Issues provider is temporarily unavailable.'; - return `GitHub Issues request failed with status ${status} (${code}).`; -} - -export function mapHttpError(status: number, fallbackCode: string) { - if (status === 401 || status === 403) return new GitHubIssuesProviderError('github_issues_auth_failed', safeErrorMessage(status, fallbackCode), false, status); - if (status === 404) return new GitHubIssuesProviderError('github_issue_missing', safeErrorMessage(status, fallbackCode), true, status); - if (status === 410) return new GitHubIssuesProviderError('github_issues_disabled', safeErrorMessage(status, fallbackCode), false, status); - if (status === 429 || status >= 500) return new GitHubIssuesProviderError('github_issues_provider_unavailable', safeErrorMessage(status, fallbackCode), true, status); - return new GitHubIssuesProviderError(fallbackCode, safeErrorMessage(status, fallbackCode), false, status); -} - -export function normalizeLabels(value: unknown) { - return Array.isArray(value) - ? value.map((entry) => typeof entry === 'string' ? entry : stringValue(record(entry).name)).filter((entry): entry is string => Boolean(entry)) - : []; -} - -export function normalizeIssue(config: GitHubIssuesExecutionProviderConfig, value: unknown): GitHubIssueSnapshot { - const issue = record(value); - const number = typeof issue.number === 'number' ? issue.number : Number.NaN; - if (!Number.isFinite(number)) { - throw new GitHubIssuesProviderError('github_issues_payload_invalid', 'GitHub issue payload did not include an issue number.', false); - } - const assignee = record(issue.assignee); - return { - number, - id: typeof issue.id === 'number' ? issue.id : null, - state: stringValue(issue.state) ?? 'open', - title: stringValue(issue.title), - body: stringValue(issue.body), - htmlUrl: stringValue(issue.html_url, issue.htmlUrl) ?? issueUrl(config, number), - labels: normalizeLabels(issue.labels), - assignee: stringValue(assignee.login, assignee.name), - comments: arrayOfRecords(issue.comments), - createdAt: stringValue(issue.created_at, issue.createdAt), - updatedAt: stringValue(issue.updated_at, issue.updatedAt), - closedAt: stringValue(issue.closed_at, issue.closedAt), - }; -} - -export function hasAnyLabel(issue: GitHubIssueSnapshot, labels: string[]) { - const issueLabels = new Set(issue.labels.map((label) => label.toLowerCase())); - return labels.some((label) => issueLabels.has(label.toLowerCase())); -} - -export function issueSnapshot(config: GitHubIssuesExecutionProviderConfig, issue: GitHubIssueSnapshot): ExecutionRunSnapshot { - let status: ExecutionRunSnapshot['status'] = 'waiting'; - let retryable: boolean | undefined; - let code: string | undefined; - if (hasAnyLabel(issue, config.cancelledLabels)) { - status = 'failed'; - retryable = false; - code = 'github_issue_cancelled'; - } else if (hasAnyLabel(issue, config.blockedLabels)) { - status = 'blocked'; - retryable = true; - code = 'github_issue_blocked'; - } else if (issue.state === 'closed') { - status = 'completed'; - } else if (hasAnyLabel(issue, config.inProgressLabels)) { - status = 'running'; - } - return { - status, - summary: status === 'completed' - ? `GitHub issue #${issue.number} is complete.` - : `GitHub issue #${issue.number} is ${status}.`, - runId: String(issue.number), - externalRef: String(issue.number), - externalUrl: issue.htmlUrl, - outputs: { - issueNumber: issue.number, - issueUrl: issue.htmlUrl, - githubState: issue.state, - labels: issue.labels, - assignee: issue.assignee ?? null, - commentCount: issue.comments?.length ?? null, - blockerReason: status === 'blocked' ? issue.title ?? 'GitHub issue blocked.' : null, - completionSummary: status === 'completed' ? issue.title ?? `GitHub issue #${issue.number} completed.` : null, - }, - retryable, - code, - metadata: { - provider: 'github_issues', - repository: config.repository, - issueNumber: issue.number, - labels: issue.labels, - }, - }; -} - -export function commentArtifacts(issue: GitHubIssueSnapshot): ExecutionArtifactRef[] { - return (issue.comments ?? []).map((comment, index) => ({ - kind: 'github_issue_comment', - name: `Comment ${stringValue(comment.id) ?? index + 1}`, - externalUrl: stringValue(comment.html_url, comment.htmlUrl) ?? issue.htmlUrl, - metadata: { - provider: 'github_issues', - issueNumber: issue.number, - commentId: stringValue(comment.id), - author: stringValue(record(comment.user).login), - createdAt: stringValue(comment.created_at), - updatedAt: stringValue(comment.updated_at), - }, - })); -} - -export function linkedRefsFromText(text: string | null | undefined) { - if (!text) return []; - const refs = new Set(); - const pattern = /(?:^|\s)(?:#\d+|https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/(?:issues|pull)\/\d+)/gu; - for (const match of text.matchAll(pattern)) { - refs.add(match[0].trim()); - } - return [...refs]; -} - -export { GitHubIssueExecutionProviderAdapter } from '../reconciliation/execution-github-issues-adapter.ts'; - diff --git a/src/agents/adapters/repositories/repository.ts b/src/agents/adapters/repositories/repository.ts deleted file mode 100644 index dd62bb2d..00000000 --- a/src/agents/adapters/repositories/repository.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; -import type { AgentRepositoryInspectionAdapter } from '../../runtime/runtime-types.ts'; -import { getAgentProviderSelections } from '@treeseed/sdk/platform/deploy-runtime'; - -const execFileAsync = promisify(execFile); - -export class GitRepositoryInspectionAdapter implements AgentRepositoryInspectionAdapter { - async inspectBranch(input: { repoRoot: string; branchName: string | null }) { - if (!input.branchName) { - return { - branchName: null, - changedPaths: [], - commitSha: null, - summary: 'No branch to inspect.', - }; - } - - try { - const { stdout: changedStdout } = await execFileAsync( - 'git', - ['diff', '--name-only', 'HEAD~1..HEAD'], - { cwd: input.repoRoot, env: process.env }, - ); - const { stdout: shaStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { - cwd: input.repoRoot, - env: process.env, - }); - const changedPaths = changedStdout - .split('\n') - .map((entry) => entry.trim()) - .filter(Boolean); - return { - branchName: input.branchName, - changedPaths, - commitSha: shaStdout.trim() || null, - summary: `Inspected ${changedPaths.length} changed path(s) on ${input.branchName}.`, - }; - } catch { - return { - branchName: input.branchName, - changedPaths: [], - commitSha: null, - summary: `Unable to inspect branch ${input.branchName}.`, - }; - } - } -} - -export function createRepositoryInspectionAdapter() { - const provider = String( - process.env.TREESEED_AGENT_REPOSITORY_PROVIDER ?? getAgentProviderSelections().repository, - ).toLowerCase(); - if (provider !== 'git') { - throw new Error(`Unsupported agent repository provider "${provider}". Configure TREESEED_AGENT_REPOSITORY_PROVIDER=git.`); - } - return new GitRepositoryInspectionAdapter(); -} diff --git a/src/agents/adapters/runtime/codex-runtime-home.ts b/src/agents/adapters/runtime/codex-runtime-home.ts deleted file mode 100644 index 250a960c..00000000 --- a/src/agents/adapters/runtime/codex-runtime-home.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { existsSync } from 'node:fs'; -import { copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { dirname, join } from 'node:path'; -import { resolveCodexAuthFile } from '../accounts/codex-auth.ts'; - -export interface CodexRuntimeHome { - codexHome: string; - authFile: string; - cleanup(): Promise; -} - -export async function createIsolatedCodexRuntimeHome(options: { - sourceAuthFile?: string; - serviceTier?: 'fast'; - model?: string; - env?: NodeJS.ProcessEnv; -} = {}): Promise { - const env = options.env ?? process.env; - const sourceAuthFile = options.sourceAuthFile ?? resolveCodexAuthFile(env); - const apiKey = env.TREESEED_CODEX_API_KEY?.trim() || env.OPENAI_API_KEY?.trim(); - if (!existsSync(sourceAuthFile) && !apiKey) { - throw new Error(`Codex auth file was not found at ${sourceAuthFile}. Run Codex login or set TREESEED_CODEX_AUTH_FILE.`); - } - const codexHome = await mkdtemp(join(tmpdir(), 'treeseed-live-codex-')); - const authFile = join(codexHome, 'auth.json'); - await mkdir(dirname(authFile), { recursive: true, mode: 0o700 }); - if (existsSync(sourceAuthFile)) await copyFile(sourceAuthFile, authFile); - const serviceTier = options.serviceTier === 'fast' ? 'fast' : null; - const model = options.model ?? (process.env.TREESEED_CODEX_DEFAULT_MODEL?.trim() || 'gpt-5.5'); - const configLines = [ - 'approval_policy = "never"', - 'sandbox_mode = "workspace-write"', - `model = ${JSON.stringify(model)}`, - env.TREESEED_CODEX_MODEL_PROVIDER ? `model_provider = ${JSON.stringify(env.TREESEED_CODEX_MODEL_PROVIDER)}` : null, - serviceTier ? `service_tier = ${JSON.stringify(serviceTier)}` : null, - '', - '[sandbox_workspace_write]', - 'network_access = true', - '', - env.TREESEED_CODEX_MODEL_PROVIDER && env.TREESEED_CODEX_BASE_URL ? `[model_providers.${env.TREESEED_CODEX_MODEL_PROVIDER}]` : null, - env.TREESEED_CODEX_MODEL_PROVIDER && env.TREESEED_CODEX_BASE_URL ? `name = ${JSON.stringify('TreeSeed AI')}` : null, - env.TREESEED_CODEX_MODEL_PROVIDER && env.TREESEED_CODEX_BASE_URL ? `base_url = ${JSON.stringify(env.TREESEED_CODEX_BASE_URL)}` : null, - env.TREESEED_CODEX_MODEL_PROVIDER && env.TREESEED_CODEX_BASE_URL ? 'wire_api = "responses"' : null, - env.TREESEED_CODEX_MODEL_PROVIDER && env.TREESEED_CODEX_BASE_URL ? 'env_key = "OPENAI_API_KEY"' : null, - env.TREESEED_CODEX_MODEL_PROVIDER && env.TREESEED_CODEX_BASE_URL ? '' : null, - ].filter((line): line is string => line !== null); - await writeFile(join(codexHome, 'config.toml'), configLines.join('\n'), { encoding: 'utf8', mode: 0o600 }); - return { - codexHome, - authFile, - cleanup: () => rm(codexHome, { recursive: true, force: true }), - }; -} diff --git a/src/agents/adapters/tools/mutations.ts b/src/agents/adapters/tools/mutations.ts deleted file mode 100644 index ce0bbfe8..00000000 --- a/src/agents/adapters/tools/mutations.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { execFile } from 'node:child_process'; -import { mkdir, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import { promisify } from 'node:util'; -import { GitRuntime } from '@treeseed/sdk/git-runtime'; -import type { AgentMutationAdapter } from '../../runtime/runtime-types.ts'; - -const execFileAsync = promisify(execFile); - -function isNothingToCommit(error: unknown) { - if (!error || typeof error !== 'object') return false; - const stdout = 'stdout' in error ? String((error as { stdout?: string }).stdout ?? '') : ''; - const stderr = 'stderr' in error ? String((error as { stderr?: string }).stderr ?? '') : ''; - return `${stdout}\n${stderr}`.includes('nothing to commit'); -} - -export class LocalBranchMutationAdapter implements AgentMutationAdapter { - private readonly git: GitRuntime; - - constructor(repoRoot: string) { - this.git = new GitRuntime( - repoRoot, - process.env.TREESEED_AGENT_DISABLE_GIT === 'true', - ); - } - - async writeArtifact(input: { - runId: string; - agent: { execution: { branchPrefix: string } }; - relativePath: string; - content: string; - commitMessage: string; - }) { - const normalizedPath = input.relativePath.replaceAll('\\', '/').replace(/^\.\//u, ''); - if (normalizedPath.startsWith('src/content/') || normalizedPath.startsWith('docs/src/content/')) { - throw new Error('Knowledge Hub content mutations require an assignment-scoped TreeDX tool receipt; local branch writes are forbidden.'); - } - const branchName = `${input.agent.execution.branchPrefix}/${input.runId}`; - const worktreePath = await this.git.ensureWorktree(branchName); - const filePath = path.join(worktreePath, input.relativePath); - await mkdir(path.dirname(filePath), { recursive: true }); - await writeFile(filePath, input.content, 'utf8'); - let git; - try { - git = await this.git.commitFileChange(filePath, branchName, input.commitMessage); - } catch (error) { - if (!isNothingToCommit(error)) { - throw error; - } - const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: worktreePath }); - git = { - branchName, - commitMessage: input.commitMessage, - worktreePath, - commitSha: stdout.trim() || null, - changedPaths: [filePath], - }; - } - return { - branchName: git.branchName, - commitMessage: git.commitMessage, - worktreePath: git.worktreePath, - commitSha: git.commitSha, - changedPaths: git.changedPaths, - }; - } -} diff --git a/src/agents/adapters/tools/research.ts b/src/agents/adapters/tools/research.ts deleted file mode 100644 index 8deecce6..00000000 --- a/src/agents/adapters/tools/research.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { AgentResearchAdapter, AgentTreeDxAdapter } from '../../runtime/runtime-types.ts'; - -function records(value: unknown) { - if (!value || typeof value !== 'object' || Array.isArray(value)) return []; - const source = value as Record; - for (const candidate of [source.items, source.results, source.entries, source.matches, source.context]) { - if (Array.isArray(candidate)) return candidate.filter((item): item is Record => Boolean(item) && typeof item === 'object' && !Array.isArray(item)); - } - return []; -} - -export class TreeDxProjectGraphResearchAdapter implements AgentResearchAdapter { - constructor(private readonly treeDx: AgentTreeDxAdapter) {} - - async research(input: { questionId: string; reason: string | null; runId: string }) { - const result = await this.treeDx.buildContext({ - repoId: '', - query: input.questionId, - body: { limit: 10, purpose: 'agent_research', reason: input.reason, runId: input.runId }, - }); - const items = records(result); - return { - status: 'completed' as const, - summary: `TreeDX-backed research context prepared for ${input.questionId}.`, - markdown: [ - '# Research Context', - '', - `Question: ${input.questionId}`, - `Reason: ${input.reason ?? 'not provided'}`, - `Run: ${input.runId}`, - '', - items.length ? 'Relevant TreeDX context:' : 'No relevant TreeDX context was returned; external evidence is required before synthesis.', - ...items.map((item) => `- ${String(item.title ?? item.path ?? item.id ?? 'context')}`), - ].join('\n'), - sources: items.map((item) => String(item.id ?? item.path ?? item.title ?? '')).filter(Boolean), - }; - } -} - -class UnavailableResearchAdapter implements AgentResearchAdapter { - async research() { - return { - status: 'waiting' as const, - summary: 'Research requires an assignment-scoped TreeDX adapter; direct local content access is disabled.', - markdown: '', - sources: [], - }; - } -} - -export function createResearchAdapter(treeDx?: AgentTreeDxAdapter | null) { - return treeDx ? new TreeDxProjectGraphResearchAdapter(treeDx) : new UnavailableResearchAdapter(); -} diff --git a/src/agents/adapters/tools/verification.ts b/src/agents/adapters/tools/verification.ts deleted file mode 100644 index 11004468..00000000 --- a/src/agents/adapters/tools/verification.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; -import type { AgentVerificationAdapter } from '../../runtime/runtime-types.ts'; -import { getAgentProviderSelections } from '@treeseed/sdk/platform/deploy-runtime'; - -const execFileAsync = promisify(execFile); - -export class LocalVerificationAdapter implements AgentVerificationAdapter { - async runChecks(input: { commands: string[]; cwd?: string }) { - if (!input.commands.length) { - return { - status: 'waiting' as const, - summary: 'No verification commands configured.', - stdout: '', - stderr: '', - }; - } - - const stdoutChunks: string[] = []; - const stderrChunks: string[] = []; - for (const command of input.commands) { - try { - const { stdout, stderr } = await execFileAsync('/bin/bash', ['-lc', command], { - cwd: input.cwd ?? process.cwd(), - env: process.env, - maxBuffer: 10 * 1024 * 1024, - }); - stdoutChunks.push(stdout); - stderrChunks.push(stderr); - } catch (error) { - return { - status: 'failed' as const, - summary: `Verification command failed: ${command}`, - stdout: stdoutChunks.join('\n'), - stderr: - error && typeof error === 'object' && 'stderr' in error - ? String((error as { stderr?: string }).stderr ?? '') - : String(error), - errorCategory: 'execution_error' as const, - }; - } - } - return { - status: 'completed' as const, - summary: `Verification completed for ${input.commands.length} command(s).`, - stdout: stdoutChunks.join('\n'), - stderr: stderrChunks.join('\n'), - }; - } -} - -export function createVerificationAdapter() { - const provider = String( - process.env.TREESEED_AGENT_VERIFICATION_PROVIDER ?? getAgentProviderSelections().verification, - ).toLowerCase(); - if (provider !== 'local') { - throw new Error(`Unsupported agent verification provider "${provider}". Configure TREESEED_AGENT_VERIFICATION_PROVIDER=local.`); - } - return new LocalVerificationAdapter(); -} diff --git a/src/agents/cli-tools.ts b/src/agents/cli-tools.ts deleted file mode 100644 index 878fc0f3..00000000 --- a/src/agents/cli-tools.ts +++ /dev/null @@ -1 +0,0 @@ -export { normalizeAgentCliOptions, buildCopilotAllowToolArgs } from '@treeseed/sdk/cli-tools'; diff --git a/src/agents/content/content-artifacts.ts b/src/agents/content/content-artifacts.ts deleted file mode 100644 index c49c3553..00000000 --- a/src/agents/content/content-artifacts.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { existsSync } from 'node:fs'; -import { relative, resolve, sep } from 'node:path'; - -export interface ContentArtifactRef { - contentPath: string; - model: string; - subjectId: string | null; - subjectField?: string | null; - artifactKind: string; - sourceAssignmentId: string | null; - producedByAgent: string; - commitSha?: string | null; - ref?: string | null; -} - -export function resolveProjectContentRoot(repoRoot: string) { - const docsContent = resolve(repoRoot, 'docs/src/content'); - if (existsSync(docsContent)) return 'docs/src/content'; - return 'src/content'; -} - -export function assertRelativeContentPath(repoRoot: string, relativePath: string) { - const root = resolve(repoRoot); - const target = resolve(root, relativePath); - const rel = relative(root, target); - if (!rel || rel.startsWith('..') || rel.split(sep).includes('..')) { - throw new Error(`Content artifact path escapes the repository: ${relativePath}`); - } - const normalized = relativePath.replaceAll('\\', '/'); - if (!normalized.startsWith('src/content/') && !normalized.startsWith('docs/src/content/')) { - throw new Error(`Content artifact path must be inside a Knowledge Hub content root: ${relativePath}`); - } - return target; -} diff --git a/src/agents/context/code-context-packs.ts b/src/agents/context/code-context-packs.ts deleted file mode 100644 index b97989e1..00000000 --- a/src/agents/context/code-context-packs.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { DeclarativeContextQuery, HandlerContextPackSource, ResolvedHandlerContextPack } from '@treeseed/sdk/graph/context-query-contracts'; -import type { SdkContextPack, SdkGraphNode } from '@treeseed/sdk'; - -export type CodeContextPackKind = 'package_surface' | 'module_surface' | 'flow'; - -export interface DocsCoverageLink { - path: string; - kind?: string; - evidence?: string; - matched?: string[]; -} - -export interface ModuleSurfaceInventory { - path: string; - packageName: string; - responsibility: string; - fileCount?: number; - importantFiles: string[]; - exportedSymbols: string[]; - imports: string[]; - tests: string[]; - relatedDocs: DocsCoverageLink[]; - warnings: string[]; -} - -export interface PackageSurfaceInventory { - name: string; - purpose: string; - root: string; - entrypoints: string[]; - publicExports: string[]; - commands?: string[]; - runtimeServices: string[]; - moduleCount?: number; - fileCount?: number; - tests: string[]; - relatedDocs: DocsCoverageLink[]; - knownGaps?: string[]; - modules: ModuleSurfaceInventory[]; - warnings: string[]; -} - -export interface CodebaseInventoryArtifact { - id: string; - kind: 'codebase_inventory'; - title?: string; - repoRef?: string; - packages: PackageSurfaceInventory[]; - modules: ModuleSurfaceInventory[]; - warnings?: string[]; -} - -export interface CodeContextPackBuilderInput { - query: DeclarativeContextQuery; - inventory: CodebaseInventoryArtifact; - source: HandlerContextPackSource; - sourceRef?: string; -} - -function normalize(value: string) { - return value.trim().replace(/\\/gu, '/').replace(/^\.\/+/u, '').replace(/^\/+/u, '').toLowerCase(); -} - -function slug(value: string) { - return normalize(value).replace(/[^a-z0-9]+/gu, '-').replace(/^-+|-+$/gu, '') || 'code'; -} - -function tokenSet(value: string) { - return new Set(normalize(value).split(/[^a-z0-9]+/u).filter((entry) => entry.length >= 3)); -} - -function scoreTokens(scope: string, text: string) { - const expected = tokenSet(scope); - if (!expected.size) return 0; - const actual = tokenSet(text); - return [...expected].filter((token) => actual.has(token)).length; -} - -function scopeMatchesPath(scope: string, path: string) { - const normalizedScope = normalize(scope); - const normalizedPath = normalize(path); - return normalizedPath === normalizedScope - || normalizedPath.startsWith(`${normalizedScope}/`) - || normalizedScope.startsWith(`${normalizedPath}/`) - || normalizedPath.includes(normalizedScope); -} - -function scopeMatchesPackage(scope: string, pkg: PackageSurfaceInventory) { - const normalizedScope = normalize(scope); - return normalizedScope === pkg.name - || scopeMatchesPath(normalizedScope, pkg.root === '.' ? 'market' : pkg.root) - || scoreTokens(scope, `${pkg.name} ${pkg.purpose} ${pkg.root}`) >= 2; -} - -function scopeMatchesModule(scope: string, module: ModuleSurfaceInventory) { - return scopeMatchesPath(scope, module.path) - || module.importantFiles.some((file) => scopeMatchesPath(scope, file)) - || scoreTokens(scope, `${module.path} ${module.responsibility} ${module.exportedSymbols.join(' ')}`) >= 2; -} - -function sourceFilesForPackage(pkg: PackageSurfaceInventory) { - return [...new Set([ - ...pkg.entrypoints, - ...pkg.modules.flatMap((module) => module.importantFiles.slice(0, 4)), - ])].sort().slice(0, 16); -} - -function node( - id: string, - title: string, - relativePath: string, - text: string, - data: Record, -): SdkContextPack['nodes'][number] { - const graphNode: SdkGraphNode = { - id, - nodeType: 'File', - sourceModel: 'codebase_inventory', - title, - text, - data: { - relativePath, - ...data, - }, - }; - return { - node: graphNode, - score: 1, - depth: 0, - text, - tokenEstimate: Math.max(1, Math.ceil(text.length / 4)), - reasons: ['codebase_inventory'], - provenance: { - seedIds: [id], - viaEdgeTypes: [], - }, - }; -} - -function packageText(pkg: PackageSurfaceInventory) { - return [ - `Package: ${pkg.name}`, - `Root: ${pkg.root}`, - `Purpose: ${pkg.purpose}`, - `Entrypoints: ${pkg.entrypoints.join(', ') || 'none recorded'}`, - `Public exports: ${pkg.publicExports.join(', ') || 'none recorded'}`, - `Runtime services: ${pkg.runtimeServices.join(', ') || 'none recorded'}`, - `Tests: ${pkg.tests.join(', ') || 'none recorded'}`, - `Related docs: ${pkg.relatedDocs.map((doc) => doc.path).join(', ') || 'none recorded'}`, - `Warnings: ${pkg.warnings.join(' ') || 'none'}`, - ].join('\n'); -} - -function moduleText(module: ModuleSurfaceInventory) { - return [ - `Module: ${module.path}`, - `Package: ${module.packageName}`, - `Responsibility: ${module.responsibility}`, - `Important files: ${module.importantFiles.join(', ') || 'none recorded'}`, - `Exports: ${module.exportedSymbols.join(', ') || 'none recorded'}`, - `Imports: ${module.imports.join(', ') || 'none recorded'}`, - `Tests: ${module.tests.join(', ') || 'none recorded'}`, - `Related docs: ${module.relatedDocs.map((doc) => doc.path).join(', ') || 'none recorded'}`, - `Warnings: ${module.warnings.join(' ') || 'none'}`, - ].join('\n'); -} - -function createPack(input: CodeContextPackBuilderInput, kind: CodeContextPackKind, idSuffix: string, nodes: SdkContextPack['nodes'], warnings: string[]): ResolvedHandlerContextPack { - const pack: SdkContextPack = { - seedIds: nodes.map((entry) => entry.node.id), - totalTokenEstimate: nodes.reduce((total, entry) => total + entry.tokenEstimate, 0), - includedNodeIds: nodes.map((entry) => entry.node.id), - nodes, - edges: [], - }; - return { - id: `${input.query.id}:code:${idSuffix}`, - purpose: input.query.purpose, - source: input.source, - sourceRef: input.sourceRef, - query: input.query, - request: { - query: input.query.query, - stage: 'research', - view: 'full', - options: { depth: 0, limit: nodes.length, maxNodes: nodes.length }, - }, - pack, - warnings: [ - `code_context_kind:${kind}`, - `codebase_inventory:${input.inventory.id}`, - `repo_ref:${input.inventory.repoRef}`, - ...warnings, - ], - }; -} - -function packagePack(input: CodeContextPackBuilderInput, pkg: PackageSurfaceInventory) { - const files = sourceFilesForPackage(pkg); - return createPack(input, 'package_surface', `package-${pkg.name}`, [ - node(`code-package:${pkg.name}`, `${pkg.name} package surface`, pkg.root === '.' ? 'market' : pkg.root, packageText(pkg), { - codeContextKind: 'package_surface', - sourceFiles: files, - sourceSymbolsOrSections: pkg.publicExports, - repoRef: input.inventory.repoRef, - }), - ...files.slice(0, 6).map((file) => node(`code-file:${file}`, file, file, `Important file for ${pkg.name}: ${file}`, { - codeContextKind: 'module_surface', - sourceFiles: [file], - sourceSymbolsOrSections: [], - repoRef: input.inventory.repoRef, - })), - ], pkg.warnings); -} - -function modulePack(input: CodeContextPackBuilderInput, module: ModuleSurfaceInventory) { - return createPack(input, 'module_surface', `module-${slug(module.path)}`, [ - node(`code-module:${module.path}`, `${module.path} module surface`, module.path, moduleText(module), { - codeContextKind: 'module_surface', - sourceFiles: module.importantFiles, - sourceSymbolsOrSections: module.exportedSymbols, - repoRef: input.inventory.repoRef, - }), - ...module.importantFiles.slice(0, 6).map((file) => node(`code-file:${file}`, file, file, `Important file for ${module.path}: ${file}`, { - codeContextKind: 'module_surface', - sourceFiles: [file], - sourceSymbolsOrSections: module.exportedSymbols, - repoRef: input.inventory.repoRef, - })), - ], module.warnings); -} - -function flowPack(input: CodeContextPackBuilderInput, scope: string) { - const flowName = scope.replace(/^flow[:/]/iu, '').trim() || input.query.query; - const rankedModules = input.inventory.modules - .map((module) => ({ module, score: scoreTokens(`${flowName} ${input.query.query}`, `${module.path} ${module.responsibility} ${module.exportedSymbols.join(' ')} ${module.imports.join(' ')}`) })) - .filter((entry) => entry.score > 0) - .sort((left, right) => right.score - left.score || left.module.path.localeCompare(right.module.path)) - .slice(0, 6) - .map((entry) => entry.module); - if (!rankedModules.length) return null; - const files = [...new Set(rankedModules.flatMap((module) => module.importantFiles.slice(0, 4)))].sort(); - return createPack(input, 'flow', `flow-${slug(flowName)}`, [ - node(`code-flow:${slug(flowName)}`, `${flowName} flow surface`, `flow/${slug(flowName)}`, [ - `Flow: ${flowName}`, - `Matched modules: ${rankedModules.map((module) => module.path).join(', ')}`, - `Source files: ${files.join(', ')}`, - ].join('\n'), { - codeContextKind: 'flow', - sourceFiles: files, - sourceSymbolsOrSections: rankedModules.flatMap((module) => module.exportedSymbols).slice(0, 24), - repoRef: input.inventory.repoRef, - }), - ...rankedModules.map((module) => node(`code-module:${module.path}`, `${module.path} module surface`, module.path, moduleText(module), { - codeContextKind: 'module_surface', - sourceFiles: module.importantFiles, - sourceSymbolsOrSections: module.exportedSymbols, - repoRef: input.inventory.repoRef, - })), - ], []); -} - -export function buildCodeContextPacksForQuery(input: CodeContextPackBuilderInput): ResolvedHandlerContextPack[] { - const scopes = input.query.codeScopes ?? []; - if (!scopes.length) return []; - const packs: ResolvedHandlerContextPack[] = []; - const seen = new Set(); - for (const scope of scopes) { - const normalizedScope = normalize(scope); - const flow = /^flow[:/]/iu.test(scope) ? flowPack(input, scope) : null; - if (flow && !seen.has(flow.id)) { - packs.push(flow); - seen.add(flow.id); - continue; - } - for (const pkg of input.inventory.packages.filter((entry) => scopeMatchesPackage(normalizedScope, entry))) { - const pack = packagePack(input, pkg); - if (!seen.has(pack.id)) { - packs.push(pack); - seen.add(pack.id); - } - } - for (const module of input.inventory.modules.filter((entry) => scopeMatchesModule(normalizedScope, entry))) { - const pack = modulePack(input, module); - if (!seen.has(pack.id)) { - packs.push(pack); - seen.add(pack.id); - } - } - } - return packs.sort((left, right) => left.id.localeCompare(right.id)); -} diff --git a/src/agents/context/context-processor.ts b/src/agents/context/context-processor.ts deleted file mode 100644 index 4fe0c20e..00000000 --- a/src/agents/context/context-processor.ts +++ /dev/null @@ -1,216 +0,0 @@ -import type { SdkContextPack } from '@treeseed/sdk'; -import type { ScopedAgentSdk } from '@treeseed/sdk/sdk'; -import { - compileDeclarativeContextQuery, - type DeclarativeContextQuery, - type HandlerContextPackSource, - type ResolvedHandlerContextPack, -} from '@treeseed/sdk/graph/context-query-contracts'; -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; -import { buildCodeContextPacksForQuery, type CodebaseInventoryArtifact } from './code-context-packs.ts'; - -interface QueryCandidate { - query: DeclarativeContextQuery; - source: HandlerContextPackSource; - sourceRef?: string; - priority: number; - index: number; -} - -export interface HandlerContextRecord { - ref?: string; - context?: { - queries?: DeclarativeContextQuery[]; - }; - frontmatter?: { - context?: { - queries?: DeclarativeContextQuery[]; - }; - }; -} - -export interface ResolveHandlerContextPacksInput { - sdk: Pick; - agent?: (AgentRuntimeSpec & { context?: { queries?: DeclarativeContextQuery[] } }) | null; - taskPayload?: Record | null; - workPackage?: Record | null; - contentRecords?: HandlerContextRecord[]; - defaultRoleContext?: DeclarativeContextQuery[]; -} - -export interface HandlerContextPackCollection { - all(): ResolvedHandlerContextPack[]; - get(id: string): ResolvedHandlerContextPack | undefined; - byPurpose(purpose: string): ResolvedHandlerContextPack[]; - warnings(): string[]; -} - -export interface ResolveHandlerContextPacksResult { - contextPacks: HandlerContextPackCollection; - warnings: string[]; -} - -const SOURCE_PRIORITIES: Record = { - default_role_context: 10, - agent_spec: 20, - content_frontmatter: 30, - work_package: 40, - task_payload: 50, -}; - -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' && !Array.isArray(value); -} - -function codebaseInventoryFromPayload(value: unknown): CodebaseInventoryArtifact | null { - if (!isRecord(value)) return null; - const inventory = isRecord(value.codebaseInventory) ? value.codebaseInventory : null; - return inventory?.kind === 'codebase_inventory' ? inventory as unknown as CodebaseInventoryArtifact : null; -} - -function contextQueriesFromRecord(value: unknown): DeclarativeContextQuery[] { - if (!isRecord(value)) return []; - const context = isRecord(value.context) ? value.context : null; - return Array.isArray(context?.queries) - ? context.queries.filter(isRecord) as unknown as DeclarativeContextQuery[] - : []; -} - -function collectFromRecord( - candidates: QueryCandidate[], - source: HandlerContextPackSource, - record: unknown, - sourceRef: string | undefined, - indexRef: { value: number }, -) { - for (const query of contextQueriesFromRecord(record)) { - candidates.push({ - query, - source, - sourceRef, - priority: SOURCE_PRIORITIES[source], - index: indexRef.value++, - }); - } -} - -function collectCandidates(input: ResolveHandlerContextPacksInput) { - const candidates: QueryCandidate[] = []; - const indexRef = { value: 0 }; - - for (const query of input.defaultRoleContext ?? []) { - candidates.push({ - query, - source: 'default_role_context', - priority: SOURCE_PRIORITIES.default_role_context, - index: indexRef.value++, - }); - } - - if (input.agent?.context?.queries?.length) { - for (const query of input.agent.context.queries) { - candidates.push({ - query, - source: 'agent_spec', - sourceRef: input.agent.slug, - priority: SOURCE_PRIORITIES.agent_spec, - index: indexRef.value++, - }); - } - } - - for (const record of input.contentRecords ?? []) { - const frontmatterSource = record.frontmatter ? { context: record.frontmatter.context } : record; - collectFromRecord(candidates, 'content_frontmatter', frontmatterSource, record.ref, indexRef); - } - - collectFromRecord(candidates, 'work_package', input.workPackage, undefined, indexRef); - collectFromRecord(candidates, 'task_payload', input.taskPayload, undefined, indexRef); - - return candidates; -} - -function mergeCandidates(candidates: QueryCandidate[]) { - const byId = new Map(); - for (const candidate of candidates) { - const id = typeof candidate.query.id === 'string' ? candidate.query.id.trim() : ''; - if (!id) { - continue; - } - const current = byId.get(id); - if (!current || candidate.priority > current.priority || (candidate.priority === current.priority && candidate.index > current.index)) { - byId.set(id, candidate); - } - } - return [...byId.values()].sort((left, right) => left.index - right.index); -} - -function createCollection(packs: ResolvedHandlerContextPack[], warnings: string[]): HandlerContextPackCollection { - const byId = new Map(packs.map((pack) => [pack.id, pack])); - return { - all: () => [...packs], - get: (id) => byId.get(id), - byPurpose: (purpose) => packs.filter((pack) => pack.purpose === purpose), - warnings: () => [...warnings], - }; -} - -function contextQueryErrorMessage(error: unknown) { - return error instanceof Error ? error.message : String(error); -} - -export async function resolveHandlerContextPacks( - input: ResolveHandlerContextPacksInput, -): Promise { - const warnings: string[] = []; - const packs: ResolvedHandlerContextPack[] = []; - - for (const candidate of mergeCandidates(collectCandidates(input))) { - const compiled = compileDeclarativeContextQuery(candidate.query); - warnings.push(...compiled.warnings.map((warning) => `${candidate.query.id}: ${warning}`)); - if (!compiled.ok || !compiled.compiled) { - const detail = compiled.errors.join(' '); - if (candidate.query.required) { - throw new Error(`Required context query "${candidate.query.id}" failed validation: ${detail}`); - } - warnings.push(`Skipped context query "${candidate.query.id}": ${detail}`); - continue; - } - - let pack: SdkContextPack; - try { - pack = await input.sdk.buildContextPack(compiled.compiled.request) as SdkContextPack; - } catch (error) { - const detail = contextQueryErrorMessage(error); - if (candidate.query.required) { - throw new Error(`Required context query "${candidate.query.id}" failed during TreeDX context resolution: ${detail}`); - } - warnings.push(`Skipped context query "${candidate.query.id}" after TreeDX context resolution failed: ${detail}`); - continue; - } - packs.push({ - id: compiled.compiled.query.id, - purpose: compiled.compiled.query.purpose, - source: candidate.source, - sourceRef: candidate.sourceRef, - query: compiled.compiled.query, - request: compiled.compiled.request, - pack, - warnings: compiled.compiled.warnings, - }); - const inventory = codebaseInventoryFromPayload(input.taskPayload); - if (inventory) { - packs.push(...buildCodeContextPacksForQuery({ - query: compiled.compiled.query, - inventory, - source: candidate.source, - sourceRef: candidate.sourceRef, - })); - } - } - - return { - contextPacks: createCollection(packs, warnings), - warnings, - }; -} diff --git a/src/agents/contracts/knowledge.ts b/src/agents/contracts/knowledge.ts deleted file mode 100644 index 6bcdc5f7..00000000 --- a/src/agents/contracts/knowledge.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { ResearchSourceMapEntry } from './research.ts'; - -export type KnowledgeReviewState = - | 'pending_review' - | 'verified_for_staging' - | 'staged' - | 'release_pending_human_approval'; - -export interface KnowledgeDraftFrontmatter { - title: string; - summary: string; - type: 'guide' | 'architecture' | 'reference' | 'operations' | 'governance' | 'api' | 'cli' | 'ui'; - status: 'draft' | 'pending_review' | 'canonical' | 'deprecated'; - generated_by: 'treeseed-agent'; - agent_role: 'knowledge_generator' | string; - source_question: string; - source_research: string[]; - review_state: KnowledgeReviewState; - book_target: string; - section_target: string; - confidence: 'low' | 'medium' | 'high'; - source_map: ResearchSourceMapEntry[]; - updated: string; - related: { - objectives: string[]; - questions: string[]; - proposals: string[]; - decisions: string[]; - }; -} - -export interface KnowledgeDraft { - id: string; - kind: 'knowledge_draft'; - title: string; - book: string; - section: string; - targetPath: string; - state: 'draft' | 'feature_branch' | 'staged' | 'released'; - sourceQuestionId: string; - sourceResearchIds: string[]; - frontmatter: KnowledgeDraftFrontmatter; - body: string; - reviewState: KnowledgeReviewState; - createdAt: string; - updatedAt: string; -} - -export interface KnowledgeOptimizationScore { - factual_grounding: number; - book_fit: number; - structure: number; - future_agent_usefulness: number; - human_reviewability: number; - link_quality: number; - uncertainty_visibility: number; -} - -export interface OptimizationReport { - id: string; - kind: 'knowledge_optimization_report'; - draftId: string; - score: KnowledgeOptimizationScore; - totalScore: number; - recommendation: 'promote' | 'revise' | 'defer' | 'reject'; - remainingIssues: string[]; - criticalIssues: string[]; - createdAt: string; -} - -const SCORE_FIELDS: Array = [ - 'factual_grounding', - 'book_fit', - 'structure', - 'future_agent_usefulness', - 'human_reviewability', - 'link_quality', - 'uncertainty_visibility', -]; - -export function sumKnowledgeOptimizationScore(score: KnowledgeOptimizationScore) { - return SCORE_FIELDS.reduce((total, field) => total + score[field], 0); -} - -export function validateKnowledgeDraft( - draft: KnowledgeDraft, - options: { allowedStatuses?: KnowledgeDraft['frontmatter']['status'][] } = {}, -) { - const errors: string[] = []; - const allowedStatuses = options.allowedStatuses ?? ['pending_review']; - if (!draft.id) errors.push('Knowledge draft id is required.'); - if (draft.kind !== 'knowledge_draft') errors.push('Knowledge draft kind must be knowledge_draft.'); - if (!draft.targetPath.startsWith('src/content/knowledge/')) errors.push('Knowledge draft targetPath must be a canonical knowledge path.'); - if (!draft.sourceQuestionId) errors.push('Knowledge draft sourceQuestionId is required.'); - if (!draft.sourceResearchIds.length) errors.push('Knowledge draft sourceResearchIds is required.'); - if (!draft.frontmatter.type) errors.push('Knowledge draft frontmatter type is required.'); - if (!allowedStatuses.includes(draft.frontmatter.status)) { - errors.push(`Knowledge draft frontmatter status must be ${allowedStatuses.join(' or ')}.`); - } - if (!draft.frontmatter.source_map.length) errors.push('Knowledge draft frontmatter source_map is required.'); - if (!Array.isArray(draft.frontmatter.related.decisions)) { - errors.push('Knowledge draft frontmatter related.decisions must be an array.'); - } - for (const section of [ - '## What this explains', - '## Current implementation', - '## Main flow', - '## Important files', - '## Source map', - '## Governance and safety boundaries', - '## Open questions', - '## Verification notes', - ]) { - if (!draft.body.includes(section)) errors.push(`Knowledge draft body must include ${section}.`); - } - return { - ok: errors.length === 0, - errors, - }; -} - -export function validateOptimizationReport(report: OptimizationReport) { - const errors = SCORE_FIELDS.flatMap((field) => { - const value = report.score[field]; - return Number.isInteger(value) && value >= 0 && value <= 5 - ? [] - : [`Optimization score ${field} must be an integer from 0 to 5.`]; - }); - if (report.totalScore !== sumKnowledgeOptimizationScore(report.score)) { - errors.push('Optimization report totalScore must equal the sum of score fields.'); - } - if (!['promote', 'revise', 'defer', 'reject'].includes(report.recommendation)) { - errors.push('Optimization report recommendation is invalid.'); - } - if (!Array.isArray(report.criticalIssues)) { - errors.push('Optimization report criticalIssues must be an array.'); - } - return { - ok: errors.length === 0, - errors, - }; -} diff --git a/src/agents/contracts/messages.ts b/src/agents/contracts/messages.ts deleted file mode 100644 index bc37926d..00000000 --- a/src/agents/contracts/messages.ts +++ /dev/null @@ -1,384 +0,0 @@ -export interface QuestionPriorityUpdatedMessage { - questionId: string; - reason: string; - plannerRunId: string; -} - -export interface ObjectivePriorityUpdatedMessage { - objectiveId: string; - reason: string; - plannerRunId: string; -} - -export interface ArchitectureUpdatedMessage { - objectiveId: string; - knowledgeId: string; - architectRunId: string; -} - -export interface SubscriberNotifiedMessage { - email: string; - itemCount: number; - notifierRunId: string; -} - -export interface ResearchStartedMessage { - questionId: string; - researcherRunId: string; -} - -export interface ResearchCompletedMessage { - questionId: string; - knowledgeId: string | null; - researcherRunId: string; -} - -export interface KnowledgeDraftCreatedMessage { - draftId: string; - targetPath: string; - sourceQuestionId: string; - sourceResearchIds: string[]; - generatorRunId: string; -} - -export interface KnowledgeOptimizationCompletedMessage { - reportId: string; - draftId: string; - recommendation: string; - totalScore: number; - optimizerRunId: string; -} - -export interface TaskCompleteMessage { - branchName: string | null; - changedTargets: string[]; - engineerRunId: string; -} - -export interface TaskWaitingMessage { - blockingReason: string; - engineerRunId: string; -} - -export interface TaskFailedMessage { - failureSummary: string; - engineerRunId: string; -} - -export interface TaskVerifiedMessage { - branchName: string | null; - reviewerRunId: string; -} - -export interface ReviewFailedMessage { - failureSummary: string; - reviewerRunId: string; -} - -export interface ReviewWaitingMessage { - blockingReason: string; - reviewerRunId: string; -} - -export interface ReleaseStartedMessage { - taskRunId: string | null; - releaserRunId: string; -} - -export interface ReleaseCompletedMessage { - releaseSummary: string; - releaserRunId: string; -} - -export interface ReleaseFailedMessage { - failureSummary: string; - releaserRunId: string; -} - -export interface DocumentationAutomationEventMessage extends Record { - summary?: string; - agentRunId?: string; -} - -export interface AgentMessageContracts { - question_priority_updated: QuestionPriorityUpdatedMessage; - objective_priority_updated: ObjectivePriorityUpdatedMessage; - architecture_updated: ArchitectureUpdatedMessage; - subscriber_notified: SubscriberNotifiedMessage; - research_started: ResearchStartedMessage; - research_completed: ResearchCompletedMessage; - knowledge_draft_created: KnowledgeDraftCreatedMessage; - knowledge_optimization_completed: KnowledgeOptimizationCompletedMessage; - task_complete: TaskCompleteMessage; - task_waiting: TaskWaitingMessage; - task_failed: TaskFailedMessage; - task_verified: TaskVerifiedMessage; - review_failed: ReviewFailedMessage; - review_waiting: ReviewWaitingMessage; - release_started: ReleaseStartedMessage; - release_completed: ReleaseCompletedMessage; - release_failed: ReleaseFailedMessage; - documentation_gap_detected: DocumentationAutomationEventMessage; - research_task_requested: DocumentationAutomationEventMessage; - documentation_plan_updated: DocumentationAutomationEventMessage; - codebase_inventory_completed: DocumentationAutomationEventMessage; - research_note_created: DocumentationAutomationEventMessage; - source_map_created: DocumentationAutomationEventMessage; - knowledge_gap_detected: DocumentationAutomationEventMessage; - draft_requires_context: DocumentationAutomationEventMessage; - promotion_request_created: DocumentationAutomationEventMessage; - revision_requested: DocumentationAutomationEventMessage; - draft_rejected: DocumentationAutomationEventMessage; - docs_mutation_completed: DocumentationAutomationEventMessage; - docs_mutation_waiting_for_review: DocumentationAutomationEventMessage; - docs_mutation_failed: DocumentationAutomationEventMessage; - repair_task_created: DocumentationAutomationEventMessage; - review_passed: DocumentationAutomationEventMessage; - human_approval_recommended: DocumentationAutomationEventMessage; - approval_request_created: DocumentationAutomationEventMessage; - governance_item_created: DocumentationAutomationEventMessage; - policy_violation_detected: DocumentationAutomationEventMessage; - approval_ready_for_human: DocumentationAutomationEventMessage; - workday_report_created: DocumentationAutomationEventMessage; - project_summary_updated: DocumentationAutomationEventMessage; - team_inbox_item_created: DocumentationAutomationEventMessage; - release_candidate_created: DocumentationAutomationEventMessage; - release_waiting_for_approval: DocumentationAutomationEventMessage; - knowledge_generated: DocumentationAutomationEventMessage; - knowledge_optimized: DocumentationAutomationEventMessage; - report_created: DocumentationAutomationEventMessage; - distribution_digest_ready: DocumentationAutomationEventMessage; -} - -export type AgentMessageType = keyof AgentMessageContracts; -export type AgentMessagePayload = AgentMessageContracts[TType]; -export const AGENT_MESSAGE_TYPES = [ - 'question_priority_updated', - 'objective_priority_updated', - 'architecture_updated', - 'subscriber_notified', - 'research_started', - 'research_completed', - 'knowledge_draft_created', - 'knowledge_optimization_completed', - 'task_complete', - 'task_waiting', - 'task_failed', - 'task_verified', - 'review_failed', - 'review_waiting', - 'release_started', - 'release_completed', - 'release_failed', - 'documentation_gap_detected', - 'research_task_requested', - 'documentation_plan_updated', - 'codebase_inventory_completed', - 'research_note_created', - 'source_map_created', - 'knowledge_gap_detected', - 'draft_requires_context', - 'promotion_request_created', - 'revision_requested', - 'draft_rejected', - 'docs_mutation_completed', - 'docs_mutation_waiting_for_review', - 'docs_mutation_failed', - 'repair_task_created', - 'review_passed', - 'human_approval_recommended', - 'approval_request_created', - 'governance_item_created', - 'policy_violation_detected', - 'approval_ready_for_human', - 'workday_report_created', - 'project_summary_updated', - 'team_inbox_item_created', - 'release_candidate_created', - 'release_waiting_for_approval', - 'knowledge_generated', - 'knowledge_optimized', - 'report_created', - 'distribution_digest_ready', -] as const satisfies readonly AgentMessageType[]; - -const DOCUMENTATION_AUTOMATION_MESSAGE_TYPES = new Set([ - 'documentation_gap_detected', - 'research_task_requested', - 'documentation_plan_updated', - 'codebase_inventory_completed', - 'research_note_created', - 'source_map_created', - 'knowledge_gap_detected', - 'draft_requires_context', - 'promotion_request_created', - 'revision_requested', - 'draft_rejected', - 'docs_mutation_completed', - 'docs_mutation_waiting_for_review', - 'docs_mutation_failed', - 'repair_task_created', - 'review_passed', - 'human_approval_recommended', - 'approval_request_created', - 'governance_item_created', - 'policy_violation_detected', - 'approval_ready_for_human', - 'workday_report_created', - 'project_summary_updated', - 'team_inbox_item_created', - 'release_candidate_created', - 'release_waiting_for_approval', - 'knowledge_generated', - 'knowledge_optimized', - 'report_created', - 'distribution_digest_ready', -]); - -function ensureString(value: unknown, label: string) { - if (typeof value !== 'string' || value.trim().length === 0) { - throw new Error(`Invalid ${label}: expected non-empty string.`); - } - return value; -} - -function ensureOptionalString(value: unknown, label: string) { - if (value === null || value === undefined) { - return null; - } - return ensureString(value, label); -} - -function ensureStringArray(value: unknown, label: string) { - if (!Array.isArray(value)) { - throw new Error(`Invalid ${label}: expected array.`); - } - return value.map((entry, index) => ensureString(entry, `${label}[${index}]`)); -} - -function ensureNumber(value: unknown, label: string) { - if (typeof value !== 'number' || Number.isNaN(value)) { - throw new Error(`Invalid ${label}: expected number.`); - } - return value; -} - -export function parseAgentMessagePayload( - type: TType, - payloadJson: string, -): AgentMessagePayload { - const parsed = JSON.parse(payloadJson) as Record; - - switch (type) { - case 'question_priority_updated': - return { - questionId: ensureString(parsed.questionId, 'questionId'), - reason: ensureString(parsed.reason, 'reason'), - plannerRunId: ensureString(parsed.plannerRunId, 'plannerRunId'), - } as AgentMessagePayload; - case 'objective_priority_updated': - return { - objectiveId: ensureString(parsed.objectiveId, 'objectiveId'), - reason: ensureString(parsed.reason, 'reason'), - plannerRunId: ensureString(parsed.plannerRunId, 'plannerRunId'), - } as AgentMessagePayload; - case 'architecture_updated': - return { - objectiveId: ensureString(parsed.objectiveId, 'objectiveId'), - knowledgeId: ensureString(parsed.knowledgeId, 'knowledgeId'), - architectRunId: ensureString(parsed.architectRunId, 'architectRunId'), - } as AgentMessagePayload; - case 'subscriber_notified': - return { - email: ensureString(parsed.email, 'email'), - itemCount: ensureNumber(parsed.itemCount, 'itemCount'), - notifierRunId: ensureString(parsed.notifierRunId, 'notifierRunId'), - } as AgentMessagePayload; - case 'research_started': - return { - questionId: ensureString(parsed.questionId, 'questionId'), - researcherRunId: ensureString(parsed.researcherRunId, 'researcherRunId'), - } as AgentMessagePayload; - case 'research_completed': - return { - questionId: ensureString(parsed.questionId, 'questionId'), - knowledgeId: ensureOptionalString(parsed.knowledgeId, 'knowledgeId'), - researcherRunId: ensureString(parsed.researcherRunId, 'researcherRunId'), - } as AgentMessagePayload; - case 'knowledge_draft_created': - return { - draftId: ensureString(parsed.draftId, 'draftId'), - targetPath: ensureString(parsed.targetPath, 'targetPath'), - sourceQuestionId: ensureString(parsed.sourceQuestionId, 'sourceQuestionId'), - sourceResearchIds: ensureStringArray(parsed.sourceResearchIds, 'sourceResearchIds'), - generatorRunId: ensureString(parsed.generatorRunId, 'generatorRunId'), - } as AgentMessagePayload; - case 'knowledge_optimization_completed': - return { - reportId: ensureString(parsed.reportId, 'reportId'), - draftId: ensureString(parsed.draftId, 'draftId'), - recommendation: ensureString(parsed.recommendation, 'recommendation'), - totalScore: ensureNumber(parsed.totalScore, 'totalScore'), - optimizerRunId: ensureString(parsed.optimizerRunId, 'optimizerRunId'), - } as AgentMessagePayload; - case 'task_complete': - return { - branchName: ensureOptionalString(parsed.branchName, 'branchName'), - changedTargets: ensureStringArray(parsed.changedTargets, 'changedTargets'), - engineerRunId: ensureString(parsed.engineerRunId, 'engineerRunId'), - } as AgentMessagePayload; - case 'task_waiting': - return { - blockingReason: ensureString(parsed.blockingReason, 'blockingReason'), - engineerRunId: ensureString(parsed.engineerRunId, 'engineerRunId'), - } as AgentMessagePayload; - case 'task_failed': - return { - failureSummary: ensureString(parsed.failureSummary, 'failureSummary'), - engineerRunId: ensureString(parsed.engineerRunId, 'engineerRunId'), - } as AgentMessagePayload; - case 'task_verified': - return { - branchName: ensureOptionalString(parsed.branchName, 'branchName'), - reviewerRunId: ensureString(parsed.reviewerRunId, 'reviewerRunId'), - } as AgentMessagePayload; - case 'review_failed': - return { - failureSummary: ensureString(parsed.failureSummary, 'failureSummary'), - reviewerRunId: ensureString(parsed.reviewerRunId, 'reviewerRunId'), - } as AgentMessagePayload; - case 'review_waiting': - return { - blockingReason: ensureString(parsed.blockingReason, 'blockingReason'), - reviewerRunId: ensureString(parsed.reviewerRunId, 'reviewerRunId'), - } as AgentMessagePayload; - case 'release_started': - return { - taskRunId: ensureOptionalString(parsed.taskRunId, 'taskRunId'), - releaserRunId: ensureString(parsed.releaserRunId, 'releaserRunId'), - } as AgentMessagePayload; - case 'release_completed': - return { - releaseSummary: ensureString(parsed.releaseSummary, 'releaseSummary'), - releaserRunId: ensureString(parsed.releaserRunId, 'releaserRunId'), - } as AgentMessagePayload; - case 'release_failed': - return { - failureSummary: ensureString(parsed.failureSummary, 'failureSummary'), - releaserRunId: ensureString(parsed.releaserRunId, 'releaserRunId'), - } as AgentMessagePayload; - default: - if (DOCUMENTATION_AUTOMATION_MESSAGE_TYPES.has(type)) { - return parsed as AgentMessagePayload; - } - throw new Error(`Unsupported message type "${type}".`); - } -} - -export function serializeAgentMessagePayload( - type: TType, - payload: AgentMessagePayload, -) { - parseAgentMessagePayload(type, JSON.stringify(payload)); - return payload as unknown as Record; -} diff --git a/src/agents/contracts/research.ts b/src/agents/contracts/research.ts deleted file mode 100644 index 8a3f1d15..00000000 --- a/src/agents/contracts/research.ts +++ /dev/null @@ -1,72 +0,0 @@ -export interface ResearchContextQueryProvenance { - id: string; - purpose: string; - source: string; - sourceRef?: string; - includedNodeIds: string[]; - warnings: string[]; -} - -export interface ResearchSourceRef { - ref: string; - kind: 'path' | 'content_id' | 'graph_node'; - title?: string; -} - -export interface ResearchSourceMapEntry { - claim: string; - sourceFiles: string[]; - sourceSymbolsOrSections: string[]; - evidenceStrength: 'direct' | 'supporting' | 'inferred'; - uncertainty: string; - lastObservedRef: string; -} - -export interface ResearchInference { - statement: string; - sourceRefs: string[]; - confidence: 'low' | 'medium' | 'high'; -} - -export interface ResearchUncertainty { - statement: string; - impact: 'low' | 'medium' | 'high'; - nextStep?: string; -} - -export interface ResearchNote { - id: string; - kind: 'research_note'; - questionId: string; - state: 'draft' | 'reviewed'; - contextQueries: ResearchContextQueryProvenance[]; - contextPackSummary: string; - sourceRefs: ResearchSourceRef[]; - sourceMap: ResearchSourceMapEntry[]; - observedFacts: string[]; - inferences: ResearchInference[]; - uncertainties: ResearchUncertainty[]; - recommendedKnowledgeArtifacts: string[]; - recommendedImplementationProposal?: string | null; - createdAt: string; -} - -export function validateResearchNote(note: ResearchNote) { - const errors: string[] = []; - if (!note.id) errors.push('Research note id is required.'); - if (note.kind !== 'research_note') errors.push('Research note kind must be research_note.'); - if (!note.questionId) errors.push('Research note questionId is required.'); - if (!note.contextQueries.length) errors.push('Research note must record context query provenance.'); - if (!note.sourceRefs.length) errors.push('Research note must include source refs.'); - if (!note.sourceMap.length) errors.push('Research note must include source map entries.'); - for (const [index, entry] of note.sourceMap.entries()) { - if (!entry.claim) errors.push(`Research note sourceMap[${index}] claim is required.`); - if (!entry.sourceFiles.length) errors.push(`Research note sourceMap[${index}] must include source files.`); - if (!['direct', 'supporting', 'inferred'].includes(entry.evidenceStrength)) errors.push(`Research note sourceMap[${index}] has invalid evidence strength.`); - } - if (!note.observedFacts.length) errors.push('Research note must include observed facts.'); - return { - ok: errors.length === 0, - errors, - }; -} diff --git a/src/agents/contracts/run.ts b/src/agents/contracts/run.ts deleted file mode 100644 index e0466e45..00000000 --- a/src/agents/contracts/run.ts +++ /dev/null @@ -1,30 +0,0 @@ -export type AgentErrorCategory = - | 'trigger_resolution_error' - | 'permission_error' - | 'message_claim_error' - | 'lease_error' - | 'execution_error' - | 'mutation_error' - | 'sdk_error'; - -export interface AgentRunTrace { - [key: string]: unknown; - runId: string; - agentSlug: string; - handlerKind: string; - triggerKind: string; - triggerSource: string; - claimedMessageId: number | null; - selectedItemKey: string | null; - branchName: string | null; - commitSha: string | null; - changedPaths: string[]; - summary: string | null; - error: string | null; - errorCategory: AgentErrorCategory | null; - startedAt: string; - finishedAt: string | null; - status: string; -} - -export {}; diff --git a/src/agents/handlers/actor.ts b/src/agents/handlers/actor.ts deleted file mode 100644 index 702e9af3..00000000 --- a/src/agents/handlers/actor.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AgentHandler } from '../runtime/runtime-types.ts'; -import { createExecutionContentHandler } from './execution-content.ts'; - -const executionActorHandler = createExecutionContentHandler({ - kind: 'actor', - defaultWorkPackageKind: 'actor', - defaultArtifactKind: 'implementation_report', - executionAccess: 'configured', -}); - -export const actorHandler: AgentHandler = { - ...executionActorHandler, - kind: 'actor', -}; diff --git a/src/agents/handlers/context/shared-context-queries.ts b/src/agents/handlers/context/shared-context-queries.ts deleted file mode 100644 index d2530d4b..00000000 --- a/src/agents/handlers/context/shared-context-queries.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { createHash } from 'node:crypto'; -import { compileDeclarativeContextQuery,contextQueryResultFacts,type ContextQuerySetDefinition,type DeclarativeContextQuery } from '@treeseed/sdk'; -import { validateContentFrontmatter } from '@treeseed/sdk/content-validation'; -import { parseFrontmatterDocument } from '@treeseed/sdk/frontmatter'; -import type { AgentContext } from '../../runtime/runtime-types.ts'; -import { readRecord,type HandlerPayload } from '../shared.ts'; - -type QueryRef={kind:'query'|'query-set';id:string;revision:number}; - -function text(value:unknown) { return typeof value==='string'&&value.trim()?value.trim():null; } -function digest(value:unknown) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); } -function references(payload:HandlerPayload):QueryRef[] { - return (Array.isArray(payload.contextQueryRefs)?payload.contextQueryRefs:[]).flatMap((value)=>{ - const row=readRecord(value); const kind=row?.kind; const id=text(row?.id); const revision=Number(row?.revision); - return (kind==='query'||kind==='query-set')&&id&&Number.isInteger(revision)&&revision>0?[{kind,id,revision}]:[]; - }); -} - -function responseFile(response:Record) { - const payload=readRecord(response.payload)??response; - const values=[payload.files,readRecord(payload.data)?.files,readRecord(payload.result)?.files]; - for(const value of values) if(Array.isArray(value)&&value.length) return readRecord(value[0]); - return null; -} - -export function assignmentContextRef(context:AgentContext,payload:HandlerPayload) { - const definitionRef=text(payload.contextDefinitionRef)??text(readRecord(payload.agentDefinition)?.immutableRef); - if(definitionRef) return definitionRef; - const assignment=readRecord(context.capacity?.assignment); - const handle=readRecord(context.capacity?.treedxProxyHandle)??readRecord(assignment?.treedxProxyHandle)??readRecord(readRecord(assignment?.workspaceContext)?.treedxProxyHandle); - return text(handle?.baseCommitSha)??text(handle?.baseRef)??text(payload.contentBaseRef); -} - -export function assignmentRuntimeContextRef(context:AgentContext,payload:HandlerPayload) { - const runtimeRef=text(payload.contextRuntimeRef)??text(payload.contentBaseRef); - if(runtimeRef) return runtimeRef; - const assignment=readRecord(context.capacity?.assignment); - const handle=readRecord(context.capacity?.treedxProxyHandle)??readRecord(assignment?.treedxProxyHandle)??readRecord(readRecord(assignment?.workspaceContext)?.treedxProxyHandle); - return text(handle?.baseCommitSha)??text(handle?.baseRef); -} - -async function frontmatter(context:AgentContext,repoId:string,path:string,ref:string|null) { - const response=await context.treeDx!.readRepositoryFiles({repoId,paths:[path],...(ref?{ref}:{}),body:{source:'assignment_context_query_definition',assignmentId:context.capacity?.assignmentId}}); - const file=responseFile(response); const source=[file?.content,file?.text,file?.body].find((value):value is string=>typeof value==='string'&&value.length>0)??null; - if(!source) throw new Error(`Shared context definition ${path} was not readable through the assignment TreeDX proxy.`); - return parseFrontmatterDocument(source).frontmatter; -} - -function compiledRequest(query:DeclarativeContextQuery) { - const compiled=compileDeclarativeContextQuery(query); - if(!compiled.ok||!compiled.compiled) throw new Error(`Shared context query ${query.id} is invalid: ${compiled.errors.join('; ')}`); - return compiled.compiled.request as unknown as Record; -} - -async function execute(context:AgentContext,repoId:string,query:DeclarativeContextQuery,reference:QueryRef,check:Record|null,definitionRef:string|null,runtimeRef:string|null) { - if(query.id!==reference.id||query.revision!==reference.revision) throw new Error(`Shared context query ${reference.id} revision does not match assignment revision ${reference.revision}.`); - const request=compiledRequest(query); const paths=Array.isArray(request.scopePaths)?request.scopePaths.map(String):[]; - const pack=await context.treeDx!.buildContext({repoId,...(runtimeRef?{ref:runtimeRef}:{}),query:text(request.query),paths,body:{ - ...request,source:'assignment_realtime_context_query',assignmentId:context.capacity?.assignmentId, - agentId:context.agent.slug,queryRef:reference, - }}); - const stats=contextQueryResultFacts(pack); - return {id:`treedx-realtime-context-query:${reference.id}`,purpose:'Real-time shared agent context query.',source:'treedx_realtime_context_query', - sourceRef:{queryRef:reference,definitionRef,requestedRuntimeRef:runtimeRef,resolvedRef:text(pack.resolvedRef)??text(readRecord(pack.payload)?.resolvedRef),checkId:text(check?.id),testRef:text(check?.testRef),resultDigest:digest(pack),stats},pack}; -} - -export async function collectRealtimeSharedContext(input:{context:AgentContext;payload:HandlerPayload;contentRoot:string;repoId:string}):Promise { - const refs=references(input.payload); if(!refs.length) return []; - const definitionRef=assignmentContextRef(input.context,input.payload); - const runtimeRef=assignmentRuntimeContextRef(input.context,input.payload); - const checks=Array.isArray(input.payload.contextQueryChecks)?input.payload.contextQueryChecks.map(readRecord).filter(Boolean):[]; - const queryCache=new Map(); - const loadQuery=async(reference:{id:string;revision:number})=>{ - const key=`${reference.id}@${reference.revision}`; const cached=queryCache.get(key); if(cached) return cached; - const raw=await frontmatter(input.context,input.repoId,`${input.contentRoot}/agent-context-queries/${reference.id}.mdx`,definitionRef); - const validation=validateContentFrontmatter('agent_context_query',raw); - if(!validation.ok||!validation.data) throw new Error(`Shared context query ${key} failed runtime validation: ${validation.diagnostics.map((entry)=>`${entry.field}: ${entry.message}`).join('; ')}`); - const query=validation.data as DeclarativeContextQuery; - if(query.revision!==reference.revision) throw new Error(`Shared context query ${key} no longer resolves to its assigned revision.`); - queryCache.set(key,query); return query; - }; - const evidence=[]; - for(const reference of refs) { - const check=checks.find((candidate)=>readRecord(candidate?.definition)?.kind===reference.kind - &&text(readRecord(candidate?.definition)?.id)===reference.id&&Number(readRecord(candidate?.definition)?.revision)===reference.revision)??null; - if(!check) throw new Error(`Shared context query ${reference.id}@${reference.revision} has no admitted passing check provenance.`); - if(reference.kind==='query') { - evidence.push(await execute(input.context,input.repoId,await loadQuery(reference),reference,check,definitionRef,runtimeRef)); - continue; - } - const raw=await frontmatter(input.context,input.repoId,`${input.contentRoot}/agent-context-query-sets/${reference.id}.mdx`,definitionRef); - const validation=validateContentFrontmatter('agent_context_query_set',raw); - if(!validation.ok||!validation.data) throw new Error(`Shared context query set ${reference.id}@${reference.revision} failed runtime validation.`); - const set=validation.data as ContextQuerySetDefinition; - if(set.revision!==reference.revision) throw new Error(`Shared context query set ${reference.id} no longer resolves to assigned revision ${reference.revision}.`); - for(const member of set.queryRefs) evidence.push(await execute(input.context,input.repoId,await loadQuery(member),{kind:'query',...member},check,definitionRef,runtimeRef)); - } - return evidence; -} diff --git a/src/agents/handlers/estimate.ts b/src/agents/handlers/estimate.ts deleted file mode 100644 index 6f2172f1..00000000 --- a/src/agents/handlers/estimate.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { AgentHandler } from "../runtime/runtime-types.ts"; -import type { - ExecutionContentInputs, - ExecutionContentResult, -} from "./execution-content.ts"; -import { createExecutionContentHandler } from "./execution-content.ts"; -import { - validateStructuredAgentEstimate, - type StructuredAgentEstimate, -} from "@treeseed/sdk/agent-capacity"; -import { readRecord, readString } from "./shared.ts"; - -function exactRevision(value:unknown):StructuredAgentEstimate['proposalRevision']{ - const item=readRecord(value);const id=readString(item?.id);const version=Number(item?.version);const digest=readString(item?.digest);return id&&Number.isInteger(version)&&version>0&&digest?{id,version,digest}:undefined; -} -function definitionRevision(value:unknown):StructuredAgentEstimate['agentDefinitionRevision']{ - const item=readRecord(value);const id=readString(item?.id);const revision=Number(item?.revision);const digest=readString(item?.digest);return id&&Number.isInteger(revision)&&revision>0&&digest?{id,revision,digest}:undefined; -} -function membershipSnapshot(value:unknown):StructuredAgentEstimate['groupSnapshot']{ - const item=readRecord(value);const projectId=readString(item?.projectId);const graphRevision=readString(item?.graphRevision);const immutableRef=readString(item?.immutableRef);const digest=readString(item?.digest);const capturedAt=readString(item?.capturedAt);if(!projectId||!graphRevision||!immutableRef||!digest||!capturedAt)return undefined;return {projectId,graphRevision,immutableRef,digest,capturedAt,directGroupIds:stringArray(item?.directGroupIds),effectiveGroupIds:stringArray(item?.effectiveGroupIds),provenance:Array.isArray(item?.provenance)?item.provenance as NonNullable['provenance']:[]}; -} - -const executionEstimateHandler = createExecutionContentHandler({ - kind: "estimate", - defaultWorkPackageKind: "estimate", - defaultArtifactKind: "agent_estimate", - requireContentArtifact: false, -}); - -function stringArray(value: unknown): string[] { - return Array.isArray(value) - ? value.map((entry) => String(entry ?? "").trim()).filter(Boolean) - : []; -} - -function firstString(...values: unknown[]): string | null { - for (const value of values) { - const string = readString(value); - if (string) return string; - } - return null; -} - -function outputRequirements( - value: unknown, -): StructuredAgentEstimate["expectedOutputs"] { - if (!Array.isArray(value)) - return [{ outputType: "agent_estimate", required: true }]; - return value.map((entry) => { - const record = readRecord(entry); - return { - id: readString(record?.id) ?? undefined, - outputType: - firstString(record?.outputType, record?.type) ?? "agent_estimate", - description: readString(record?.description) ?? undefined, - required: record?.required === false ? false : true, - metadata: readRecord(record?.metadata) ?? undefined, - }; - }); -} - -function dependencySpecs( - value: unknown, -): StructuredAgentEstimate["dependencies"] { - if (!Array.isArray(value)) return []; - return value.map((entry, index) => { - const record = readRecord(entry) ?? {}; - return { - id: readString(record.id) ?? `dependency-${index + 1}`, - type: (readString(record.type) ?? - "capability") as StructuredAgentEstimate["dependencies"][number]["type"], - requiredBefore: (readString(record.requiredBefore) ?? - "start") as StructuredAgentEstimate["dependencies"][number]["requiredBefore"], - optional: record.optional === true, - deliverableType: readString(record.deliverableType) ?? undefined, - capability: readString(record.capability) ?? undefined, - agentClass: readString(record.agentClass) ?? undefined, - contentRefs: stringArray(record.contentRefs), - humanInputPolicy: readRecord( - record.humanInputPolicy, - ) as StructuredAgentEstimate["dependencies"][number]["humanInputPolicy"], - summary: readString(record.summary) ?? undefined, - }; - }); -} - -function buildStructuredEstimate( - context: Parameters[0], - result: ExecutionContentResult, -): StructuredAgentEstimate { - const assignment = readRecord(context.capacity?.assignment); - const decisionInput = readRecord(context.capacity?.decisionInput); - const input = readRecord(decisionInput?.input) ?? {}; - const metadata = - readRecord(result.snapshot.outputs?.structuredEstimate) ?? - readRecord(result.snapshot.outputs?.estimate) ?? - readRecord(result.snapshot.outputs?.metadata) ?? - {}; - const expectedSeconds = Number( - metadata.expectedSeconds ?? - context.capacity?.envelope?.reservedSeconds ?? - 900, - ); - const minSeconds = Number( - metadata.minSeconds ?? Math.max(0, expectedSeconds), - ); - const maxSeconds = Number( - metadata.maxSeconds ?? Math.max(minSeconds, expectedSeconds), - ); - const breakdownInput=readRecord(metadata.workBreakdown)??{}; - const weighted=[10,35,15,10,10,5,5,5]; - const allocated=weighted.map((weight)=>Math.floor(expectedSeconds*weight/100)); - for(const index of [3,6,7]) allocated[index]=Math.max(1,allocated[index]??0); - for(const index of [1,2,0,4,5]) { - const excess=Math.max(0,allocated.reduce((sum,value)=>sum+value,0)-expectedSeconds); - allocated[index]=Math.max(0,(allocated[index]??0)-excess); - } - const reserveSeconds=Math.max(0,expectedSeconds-allocated.reduce((sum,value)=>sum+value,0)); - const workBreakdown={ - preparationSeconds:Number(breakdownInput.preparationSeconds??allocated[0]),implementationSeconds:Number(breakdownInput.implementationSeconds??allocated[1]), - verificationSeconds:Number(breakdownInput.verificationSeconds??allocated[2]),independentReviewSeconds:Number(breakdownInput.independentReviewSeconds??allocated[3]), - revisionSeconds:Number(breakdownInput.revisionSeconds??allocated[4]),revisionVerificationSeconds:Number(breakdownInput.revisionVerificationSeconds??allocated[5]), - finalReviewSeconds:Number(breakdownInput.finalReviewSeconds??allocated[6]),reportingSeconds:Number(breakdownInput.reportingSeconds??allocated[7]), - reserveSeconds:Number(breakdownInput.reserveSeconds??reserveSeconds),expectedRevisionCycles:Number(breakdownInput.expectedRevisionCycles??1), - }; - const proposalRevision=exactRevision(metadata.proposalRevision)??exactRevision(input.proposalRevision); - const decisionRevision=exactRevision(metadata.decisionRevision)??exactRevision(input.decisionRevision); - const agentDefinitionRevision=definitionRevision(metadata.agentDefinitionRevision)??definitionRevision(readRecord(assignment?.metadata)?.agentDefinitionRevision); - const groupSnapshot=membershipSnapshot(metadata.groupSnapshot)??membershipSnapshot(input.groupSnapshot); - const v3=Boolean(proposalRevision&&decisionRevision&&agentDefinitionRevision&&groupSnapshot); - return { - schemaVersion:v3 ? 3 : 2, - id: readString(metadata.id) ?? `estimate-${context.runId}`, - teamId: - firstString(assignment?.teamId, context.capacity?.envelope?.teamId) ?? - "unknown-team", - projectId: - firstString( - assignment?.projectId, - context.capacity?.envelope?.projectId, - ) ?? "unknown-project", - decisionId: - firstString( - input.decisionId, - assignment?.decisionId, - decisionInput?.decisionId, - ) ?? null, - proposalId: firstString(input.proposalId, assignment?.proposalId) ?? null, - workUnitId: firstString(input.workUnitId, assignment?.workUnitId) ?? null, - agentClass: - firstString( - metadata.agentClass, - context.agent.projectAgentClassSlug, - assignment?.projectAgentClassId, - ) ?? context.agent.slug, - agentId: context.agent.slug, - minSeconds, - expectedSeconds, - maxSeconds, - confidence: (readString(metadata.confidence) ?? - "medium") as StructuredAgentEstimate["confidence"], - riskLevel: (readString(metadata.riskLevel) ?? - "medium") as StructuredAgentEstimate["riskLevel"], - assumptions: stringArray(metadata.assumptions), - blockers: stringArray(metadata.blockers), - dependencies: dependencySpecs(metadata.dependencies), - expectedOutputs: outputRequirements(metadata.expectedOutputs), - acceptanceCriteria: stringArray(metadata.acceptanceCriteria), - completionEvidence: stringArray(metadata.completionEvidence), - workBreakdown, - ...(v3 ? { - proposalRevision: proposalRevision!, - decisionRevision: decisionRevision!, - groupSnapshot: groupSnapshot!, - agentDefinitionRevision: agentDefinitionRevision!, - requiredProviderCapabilities: stringArray(metadata.requiredProviderCapabilities).length - ? stringArray(metadata.requiredProviderCapabilities) : stringArray(readRecord(context.agent.execution)?.requiredCapabilities), - acceptableProviderClasses: stringArray(metadata.acceptableProviderClasses).length - ? stringArray(metadata.acceptableProviderClasses) : ['ai_model'], - providerNativeRanges: Array.isArray(metadata.providerNativeRanges) - ? metadata.providerNativeRanges as StructuredAgentEstimate['providerNativeRanges'] : [], - } : {}), - createdAt: new Date().toISOString(), - metadata: { - source: "estimate_handler", - assignmentId: context.capacity?.assignmentId ?? null, - executionProviderRunId: result.snapshot.runId ?? null, - }, - }; -} - -export const estimateHandler: AgentHandler< - ExecutionContentInputs, - ExecutionContentResult -> = { - ...executionEstimateHandler, - kind: "estimate", - async emitOutputs(context, result) { - const output = await executionEstimateHandler.emitOutputs(context, result); - const structuredEstimate = buildStructuredEstimate(context, result); - const validation = validateStructuredAgentEstimate(structuredEstimate); - const status = - validation.ok && output.status === "completed" - ? output.status - : validation.ok - ? output.status - : "waiting"; - return { - ...output, - status, - summary: validation.ok - ? output.summary - : `Estimate output is waiting for valid structured estimate fields: ${validation.diagnostics.map((entry) => entry.code).join(", ")}`, - metadata: { - ...(output.metadata ?? {}), - kind: "structured_agent_estimate", - type: "structured_agent_estimate", - structuredEstimate, - estimateValidation: validation, - }, - }; - }, -}; diff --git a/src/agents/handlers/execution-content-artifacts.ts b/src/agents/handlers/execution-content-artifacts.ts deleted file mode 100644 index 4efc88ff..00000000 --- a/src/agents/handlers/execution-content-artifacts.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { ExecutionRunSnapshot } from '@treeseed/sdk/types/agents'; -import type { AgentContext } from '../runtime/runtime-types.ts'; -import type { ContentArtifactRef } from '../content/content-artifacts.ts'; -import { contentModelForArtifactKind } from '../tools/agent-tool-completion.ts'; -import { readRecord } from './shared.ts'; - -function firstString(...values: unknown[]) { - for (const value of values) if (typeof value === 'string' && value.trim()) return value.trim(); - return null; -} - -export function normalizedContentModel(model: string) { - return model.trim().toLowerCase().replace(/s$/u, ''); -} - -export function contentModelSupportsArtifactKind(model: string, artifactKind: string) { - return normalizedContentModel(model) === contentModelForArtifactKind(artifactKind); -} - -function artifactKindFromContentModel(model: string) { - const normalizedModel = normalizedContentModel(model); - if (normalizedModel === 'question') return 'planning_question'; - if (normalizedModel === 'proposal') return 'planning_proposal'; - if (normalizedModel === 'knowledge') return 'knowledge_page'; - return 'planning_note'; -} - -export function collectExecutionContentArtifactReceipts( - context: AgentContext, - snapshot: ExecutionRunSnapshot, - artifactKind: string, -): ContentArtifactRef[] { - const references = new Map(); - for (const artifact of snapshot.artifacts ?? []) { - if (artifact.kind !== 'treedx_content_receipt') continue; - const ref = readRecord((readRecord(artifact.metadata) ?? {}).contentRef); - if (!ref) continue; - const contentPath = firstString(ref.contentPath, ref.path, artifact.uri?.replace(/^treedx:\/\//u, '')); - const model = firstString(ref.model); - if (!contentPath || !model) continue; - const current = references.get(contentPath); - const next: ContentArtifactRef = { - contentPath, - model, - subjectId: firstString(ref.subjectId, current?.subjectId), - subjectField: firstString(ref.subjectField, current?.subjectField), - artifactKind: contentModelSupportsArtifactKind(model, artifactKind) ? artifactKind : artifactKindFromContentModel(model), - sourceAssignmentId: context.capacity?.assignmentId ?? null, - producedByAgent: context.agent.slug, - commitSha: firstString(ref.commitSha, current?.commitSha), - ref: firstString(ref.ref, ref.branchRef, current?.ref), - }; - references.set(contentPath, next); - } - return [...references.values()]; -} diff --git a/src/agents/handlers/execution-content-context.ts b/src/agents/handlers/execution-content-context.ts deleted file mode 100644 index fb43eb41..00000000 --- a/src/agents/handlers/execution-content-context.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { createHash } from 'node:crypto'; -import { compileEditorialContext, type EditorialContextLayer } from '@treeseed/sdk/knowledge'; -import { parseFrontmatterDocument } from '@treeseed/sdk/frontmatter'; -import type { AgentContext } from '../runtime/runtime-types.ts'; -import { resolveProjectContentRoot } from '../content/content-artifacts.ts'; -import { readRecord, type HandlerPayload } from './shared.ts'; -import { collectRealtimeSharedContext } from './context/shared-context-queries.ts'; - -export interface ExecutionContentSubject { - model: string | null; - id: string | null; - title: string | null; - path?: string | null; - ref?: string | null; -} - -function text(...values: unknown[]) { - for (const value of values) if (typeof value === 'string' && value.trim()) return value.trim(); - return null; -} - -function strings(value: unknown) { - return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string' && Boolean(entry.trim())) : []; -} - -function unique(values: Array) { - return [...new Set(values.filter((value): value is string => typeof value === 'string' && Boolean(value.trim())))]; -} - -function canonicalValue(value: unknown): unknown { - if (Array.isArray(value)) return value.map(canonicalValue); - const record = readRecord(value); - if (!record) return value; - return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonicalValue(record[key])])); -} - -function serialized(value: unknown) { - try { return JSON.stringify(canonicalValue(value), null, 2); } - catch { return String(value); } -} - -function revisionFor(content: string) { - return createHash('sha256').update(content).digest('hex'); -} - -function treeDxHandle(context: AgentContext) { - const explicit = readRecord(context.capacity?.treedxProxyHandle); - if (explicit && Object.keys(explicit).length) return explicit; - const assignment = readRecord(context.capacity?.assignment); - return readRecord(assignment?.treedxProxyHandle) ?? readRecord(readRecord(assignment?.workspaceContext)?.treedxProxyHandle); -} - -export function executionContentRoot(context: AgentContext, payload: HandlerPayload) { - const assignment = readRecord(context.capacity?.assignment); - const assignmentMetadata = readRecord(assignment?.metadata); - const workspaceProject = readRecord(readRecord(assignment?.workspaceContext)?.project); - const architecture = readRecord(workspaceProject?.architecture); - return text(payload.contentRoot, assignmentMetadata?.contentRoot, architecture?.contentPath) - ?? resolveProjectContentRoot(context.repoRoot); -} - -function repositoryFiles(response: Record) { - const payload = readRecord(response.payload) ?? response; - const candidates = [payload.files, readRecord(payload.data)?.files, readRecord(payload.result)?.files]; - for (const candidate of candidates) { - if (Array.isArray(candidate)) { - return candidate.flatMap((item) => { - const file = readRecord(item); - if (!file) return []; - const path = text(file.path, file.filePath, file.name); - const content = [file.text,file.content,file.body].find((entry):entry is string=>typeof entry==='string'&&Boolean(entry)); - const revision = text(file.sha, file.revision, file.contentHash); - return path || content ? [{ path, text: content ?? null, revision }] : []; - }); - } - const files = readRecord(candidate); - if (files) return Object.entries(files).flatMap(([path, value]) => { - const item=readRecord(value); - const content = typeof value === 'string' ? value : [item?.text,item?.content,item?.body].find((entry):entry is string=>typeof entry==='string'&&Boolean(entry)); - const revision = text(readRecord(value)?.sha, readRecord(value)?.revision, readRecord(value)?.contentHash); - return path || content ? [{ path, text: content ?? null, revision }] : []; - }); - } - return []; -} - -function repositoryPaths(response: Record) { - const payload = readRecord(response.payload) ?? response; - const candidates = [payload.entries, payload.paths, readRecord(payload.data)?.entries, readRecord(payload.result)?.entries]; - for (const candidate of candidates) { - if (!Array.isArray(candidate)) continue; - return unique(candidate.flatMap((entry) => typeof entry === 'string' - ? [entry] - : [text(readRecord(entry)?.path, readRecord(entry)?.filePath)])); - } - return []; -} - -function configuredQueries(context: AgentContext) { - return (context.agent.context?.queries ?? []).map((query) => ({ - id: text(query.id), purpose: text(query.purpose), query: text(query.query), scope: text(query.scope), - codeScopes: strings(query.codeScopes), relations: strings(query.relations), - depth: typeof query.depth === 'number' ? query.depth : null, - budget: typeof query.budget === 'number' ? query.budget : null, - format: text(query.format), - })); -} - -function exactRevisionRefs(value:unknown) { - return Array.isArray(value) ? value.flatMap((entry)=>{ - const item=readRecord(entry); const id=text(item?.id); const revision=Number(item?.revision); - return id&&Number.isInteger(revision)&&revision>0?[{id,revision}]:[]; - }) : []; -} - -function instructionTemplateGroups(payload:HandlerPayload,contentRoot:string) { - return exactRevisionRefs(payload.instructionTemplateRefs).map((reference)=>({ - id:`instruction template ${reference.id}@${reference.revision}`,kind:'instruction-template',required:true,templateRef:reference, - paths:[`${contentRoot}/agent-instruction-templates/${reference.id}.mdx`,`${contentRoot}/agent-instruction-templates/${reference.id}.md`], - })); -} - -function queryPaths(query: ReturnType[number], contentRoot: string) { - return unique((query.codeScopes.length ? query.codeScopes : [query.scope ?? contentRoot]).map((scope) => { - const normalized = scope.replace(/^\/+|\/+$/gu, ''); - if (!normalized || normalized === '.' || normalized === 'src/content' || normalized === contentRoot) return `${contentRoot}/**`; - if (normalized.endsWith('/**') || normalized.includes('*') || /\/[^/]+\.[a-z0-9]+$/iu.test(normalized)) return normalized; - return `${normalized}/**`; - })); -} - -function relatedArtifacts(payload: HandlerPayload) { - const candidates = [readRecord(payload.relatedArtifact), ...(Array.isArray(payload.relatedArtifacts) ? payload.relatedArtifacts.map(readRecord) : [])] - .filter((entry): entry is Record => Boolean(entry)); - const seen = new Set(); - return candidates.filter((artifact) => { - const path = text(artifact.contentPath, artifact.path); - const ref = text(artifact.commitSha, artifact.ref); - const key = `${path}:${ref ?? ''}`; - if (!path || seen.has(key)) return false; - seen.add(key); - return true; - }); -} - -function assignedObjectivePaths(payload: HandlerPayload, subject: ExecutionContentSubject, contentRoot: string) { - const objectiveId = text( - payload.objectiveId, - readRecord(payload.objective)?.id, - readRecord(payload.objective)?.slug, - subject.model === 'objective' ? subject.id : null, - ); - if (!objectiveId) return []; - return unique([ - subject.model === 'objective' ? subject.path : null, - text(payload.subjectPath), - `${contentRoot}/objectives/${objectiveId}.mdx`, - `${contentRoot}/objectives/${objectiveId}.md`, - ]); -} - -function assignedAgentPaths(context: AgentContext, contentRoot: string) { - const assignment = readRecord(context.capacity?.assignment); - const metadata = readRecord(assignment?.metadata); - return unique([ - text(metadata?.agentContentPath), - `${contentRoot}/agents/${context.agent.slug}.mdx`, - `${contentRoot}/agents/editorial/${context.agent.slug}.mdx`, - ]); -} - -const editorialAgentSlugs = new Set([ - 'guide-steward', 'knowledge-cartographer', 'evidence-researcher', 'guide-writer', - 'technical-verifier', 'audience-reviewer', 'publication-steward', 'workday-reporter', -]); - -function guideChapter(payload: HandlerPayload, subject: ExecutionContentSubject) { - const explicit = text(payload.chapter, payload.chapterSlug); - if (explicit) return explicit; - const path = text(subject.path, payload.targetPath, payload.contentPath) ?? ''; - return path.match(/treeseed-guide\/(foundation|deployment|security|content|work|governance|market)(?:\/|\.|$)/u)?.[1] ?? null; -} - -function editorialPathGroups(context: AgentContext, payload: HandlerPayload, subject: ExecutionContentSubject, contentRoot: string) { - const editorial = editorialAgentSlugs.has(context.agent.slug); - const chapter = guideChapter(payload, subject); - if (!editorial) return []; - if (!chapter && ['guide-writer', 'technical-verifier', 'audience-reviewer', 'publication-steward'].includes(context.agent.slug) - && ['acting', 'reviewing'].includes(String(context.agent.activityType)) - && ['knowledge', 'page'].includes(String(subject.model ?? ''))) { - throw new Error(`Editorial agent "${context.agent.slug}" requires an explicit TreeSeed Guide chapter scope.`); - } - return [ - { kind: 'core-objective', id: 'objective:core', required: true, - paths: unique([subject.model === 'objective' && subject.id === 'core' ? text(subject.path, payload.subjectPath) : null, - `${contentRoot}/objectives/core.mdx`, `${contentRoot}/objectives/core.md`]) }, - { kind: 'project-core', id: 'note:market:editorial:core', required: true, - paths: [`${contentRoot}/notes/editorial/core.mdx`, `${contentRoot}/notes/editorial/core.md`] }, - { kind: 'book-core', id: 'note:market:editorial:treeseed-guide:core', required: true, - paths: [`${contentRoot}/notes/editorial/books/treeseed-guide/core.mdx`, `${contentRoot}/notes/editorial/books/treeseed-guide/core.md`] }, - ...(chapter ? [{ kind: 'chapter-brief', id: `note:market:editorial:treeseed-guide:chapter:${chapter}`, - required: ['acting', 'reviewing', 'reporting'].includes(String(context.agent.activityType)), - paths: [`${contentRoot}/notes/editorial/books/treeseed-guide/chapters/${chapter}/brief.mdx`, - `${contentRoot}/notes/editorial/books/treeseed-guide/chapters/${chapter}/brief.md`] }] : []), - ]; -} - -async function collectEvidence(context: AgentContext, subject: ExecutionContentSubject, payload: HandlerPayload) { - if (!context.treeDx) return []; - const handle = treeDxHandle(context); - const repoId = text(handle?.repositoryId, handle?.repoId); - const contentRoot = executionContentRoot(context, payload); - const evidence: unknown[] = []; - const warnings: string[] = []; - const queries = configuredQueries(context); - if (repoId) { - evidence.push(...await collectRealtimeSharedContext({context,payload,contentRoot,repoId})); - const objectivePaths = assignedObjectivePaths(payload, subject, contentRoot); - const editorialGroups = editorialPathGroups(context, payload, subject, contentRoot); - const groups = [ - ...editorialGroups, - ...instructionTemplateGroups(payload,contentRoot), - ...(objectivePaths.length && !editorialGroups.some((group) => group.paths.some((path) => objectivePaths.includes(path))) - ? [{ id: 'assigned objective', paths: objectivePaths }] : []), - { id: 'agent configuration', paths: assignedAgentPaths(context, contentRoot) }, - ]; - const files: Array<{ path: string | null; text: string | null; revision?: string | null }> = []; - const resolvedPaths: string[] = []; - const responses: unknown[] = []; - let indexedPaths: Set | null = null; - if (typeof context.treeDx.listRepositoryPaths === 'function') { - try { - const listed = await context.treeDx.listRepositoryPaths({ repoId, path: contentRoot, body: { - paths: unique(groups.flatMap((group) => group.paths)), extensions: ['.md', '.mdx'], limit: 500, - source: 'agent_execution_content_handler', assignmentId: context.capacity?.assignmentId, - } }); - indexedPaths = new Set(repositoryPaths(listed)); - } catch (error) { - warnings.push(`TreeDX repository path discovery failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - for (const group of groups) { - const failures: string[] = []; - const discoveredPaths = indexedPaths ? group.paths.filter((path) => indexedPaths!.has(path)) : group.paths; - // Required exact-revision inputs must be resolved by an authoritative file - // read. Repository path discovery is an optimization and may lag newly - // published portable content models, so an empty discovery result cannot - // turn a valid exact reference into a false missing-context failure. - const confirmedPaths = discoveredPaths.length || !('required' in group && group.required) - ? discoveredPaths - : group.paths; - for (const path of confirmedPaths) { - try { - const response = await context.treeDx.readRepositoryFiles({ repoId, paths: [path] }); - const resolved = repositoryFiles(response); - if (!resolved.length) { - failures.push(`${path}: no file returned`); - responses.push({ path, response }); - continue; - } - files.push(...resolved); - resolvedPaths.push(...unique(resolved.map((file) => file.path ?? path))); - break; - } catch (error) { - failures.push(`${path}: ${error instanceof Error ? error.message : String(error)}`); - } - } - const unresolved = !confirmedPaths.length || failures.length === confirmedPaths.length; - if (unresolved) warnings.push(`TreeDX repository ${group.id} evidence failed: ${failures.join('; ') || 'no matching path exists at the immutable repository ref'}`); - if ('required' in group && group.required && unresolved) { - throw new Error(`Required editorial context "${group.kind}" could not be resolved through TreeDX.`); - } - } - evidence.push({ - id: 'treedx-repository-files', - purpose: 'Assignment core objective and agent configuration.', - source: 'treedx_proxy', - sourceRef: { repoId, paths: unique(resolvedPaths), requestedPathGroups: groups }, - files, - response: files.length ? undefined : responses, - }); - const reads = new Map(); - for (const artifact of [...(subject.path ? [{ contentPath: subject.path, commitSha: subject.ref }] : []), ...relatedArtifacts(payload)]) { - const path = text(artifact.contentPath, artifact.path); - const ref = text(artifact.commitSha, artifact.ref); - if (path) reads.set(`${path}:${ref ?? ''}`, { path, ref }); - } - for (const artifact of reads.values()) { - try { - const response = await context.treeDx.readRepositoryFiles({ repoId, paths: [artifact.path], ref: artifact.ref, body: { source: 'agent_execution_content_handler', assignmentId: context.capacity?.assignmentId, agentId: context.agent.slug, subject, artifact } }); - const files = repositoryFiles(response); - evidence.push({ id: 'treedx-subject-artifact', purpose: 'Exact upstream assignment handoff artifact.', source: 'treedx_proxy', sourceRef: { repoId, ...artifact }, files, response: files.length ? undefined : response }); - } catch (error) { - warnings.push(`TreeDX subject artifact read failed for ${artifact.path}: ${error instanceof Error ? error.message : String(error)}`); - } - } - evidence.push({ id: 'treedx-agent-context-query-contracts', purpose: 'Configured assignment context queries.', source: 'agent_spec', sourceRef: { agentId: context.agent.slug, queryCount: queries.length }, queries }); - for (const query of queries) { - if (!query.query) continue; - try { - const paths = queryPaths(query, contentRoot); - const pack = await context.treeDx.buildContext({ repoId, query: unique([query.query, subject.title, subject.id, subject.model, context.agent.slug]).join(' '), paths, body: { source: 'agent_execution_content_handler', assignmentId: context.capacity?.assignmentId, agentId: context.agent.slug, configuredQuery: query, limit: query.budget ?? 12, depth: query.depth ?? 1, format: query.format ?? 'summary' } }); - evidence.push({ id: `treedx-context-query:${query.id ?? 'query'}`, purpose: query.purpose ?? 'Configured assignment context query.', source: 'treedx_proxy', sourceRef: { repoId, queryId: query.id, query: query.query, paths }, pack }); - } catch (error) { - warnings.push(`TreeDX configured context query ${query.id ?? query.query} failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - } else warnings.push('No TreeDX repository id was available on the assignment proxy handle.'); - if (warnings.length) evidence.push({ id: 'treedx-evidence-warnings', purpose: 'TreeDX evidence collection diagnostics.', source: 'treedx_proxy', warnings }); - return evidence; -} - -function resolvedInstructionTemplates(evidence:unknown[],payload:HandlerPayload,contentRoot:string) { - const refs=exactRevisionRefs(payload.instructionTemplateRefs); - if(!refs.length) return []; - const repositoryEvidence=evidence.map(readRecord).find((entry)=>entry?.id==='treedx-repository-files'); - const files=Array.isArray(repositoryEvidence?.files)?repositoryEvidence.files.map(readRecord).filter(Boolean):[]; - return refs.map((reference)=>{ - const expectedPath=`${contentRoot}/agent-instruction-templates/${reference.id}`; - const file=files.find((candidate)=>[`${expectedPath}.mdx`,`${expectedPath}.md`].includes(text(candidate?.path)??'')); - const source=typeof file?.text==='string'?file.text:null; if(!source) throw new Error(`Required instruction template ${reference.id}@${reference.revision} was not read through TreeDX.`); - const frontmatter=readRecord(parseFrontmatterDocument(source).frontmatter)??{}; - if(text(frontmatter.id)!==reference.id||Number(frontmatter.revision)!==reference.revision) throw new Error(`Instruction template ${reference.id}@${reference.revision} did not match TreeDX content ${text(frontmatter.id)??'missing'}@${String(frontmatter.revision??'missing')}.`); - return { id:reference.id,revision:reference.revision,kind:text(frontmatter.kind),instructions:text(frontmatter.instructions),outputSkeleton:text(frontmatter.outputSkeleton),variables:strings(frontmatter.variables) }; - }); -} - -function editorialContext(evidence: unknown[], context: AgentContext, payload: HandlerPayload, subject: ExecutionContentSubject, contentRoot: string) { - if (!editorialAgentSlugs.has(context.agent.slug)) return null; - const repositoryEvidence = evidence.map(readRecord).find((entry) => entry?.id === 'treedx-repository-files'); - const files = Array.isArray(repositoryEvidence?.files) ? repositoryEvidence.files.map(readRecord).filter(Boolean) : []; - const layers: EditorialContextLayer[] = []; - for (const group of editorialPathGroups(context, payload, subject, contentRoot)) { - const file = files.find((candidate) => group.paths.includes(text(candidate?.path) ?? '')); - const content = text(file?.text); - if (!content) continue; - layers.push({ - kind: group.kind as EditorialContextLayer['kind'], id: group.id, path: text(file?.path) ?? undefined, - revision: text(file?.revision) ?? revisionFor(content), content, - reason: `Required ${group.kind} for ${context.agent.slug}.`, - }); - } - const targetEvidence = evidence.map(readRecord).find((entry) => entry?.id === 'treedx-subject-artifact'); - const targetFile = Array.isArray(targetEvidence?.files) ? targetEvidence.files.map(readRecord).find(Boolean) : null; - const targetContent = text(targetFile?.text); - if (targetContent) layers.push({ kind: 'target-page', id: subject.id ?? subject.path ?? 'assigned-target', - path: text(targetFile?.path, subject.path) ?? undefined, - revision: text(targetFile?.revision, subject.ref) ?? revisionFor(targetContent), content: targetContent, - reason: 'Exact Guide page assigned for drafting or review.' }); - const targetRequired = Boolean(subject.path) && ['guide-writer', 'technical-verifier', 'audience-reviewer', 'publication-steward'].includes(context.agent.slug) - && ['acting', 'reviewing'].includes(String(context.agent.activityType)); - if (targetRequired && !targetContent) throw new Error('The exact Guide target page could not be resolved through TreeDX.'); - const graphEvidence = evidence.map(readRecord).find((entry) => entry?.id === 'treedx-guide-editorial-graph'); - if (graphEvidence?.pack) { - const content = serialized(graphEvidence.pack); - layers.push({ kind: 'evidence', id: `${subject.id ?? 'guide-assignment'}:graph-evidence`, revision: revisionFor(content), content, - reason: text(readRecord(graphEvidence.sourceRef)?.reason) ?? 'Guide graph, guarantees, and editorial evidence.' }); - } - for (const source of evidence.map(readRecord).filter((entry) => /^treedx-(?:realtime-|verified-)?context-query:/u.test(text(entry?.id) ?? ''))) { - if (!source?.pack) continue; - const content = serialized(source.pack); - layers.push({ kind: 'source', id: text(source.id)!, revision: revisionFor(content), content, - reason: text(source.purpose) ?? 'Role-specific TreeDX source context.' }); - } - const assignment = JSON.stringify({ - agent: context.agent.slug, activityType: context.agent.activityType, assignmentId: context.capacity?.assignmentId ?? null, - chapter: guideChapter(payload, subject), subject: { model: subject.model, id: subject.id, title: subject.title, path: subject.path, ref: subject.ref }, - output: text(payload.outputContract, payload.artifactKind, payload.expectedOutput), - }, null, 2); - layers.push({ kind: 'assignment', id: String(context.capacity?.assignmentId ?? `${context.agent.slug}:assignment`), - revision: revisionFor(assignment), content: assignment, reason: 'Exact bounded assignment identity and output scope.' }); - const optionalUniqueKinds = (['chapter-brief', 'target-page'] as const) - .filter((kind) => layers.some((layer) => layer.kind === kind)); - return compileEditorialContext(layers, { - requiredKinds: ['core-objective', 'project-core', 'book-core'], - requireUniqueKinds: ['core-objective', 'project-core', 'book-core', ...optionalUniqueKinds, 'assignment'], - }); -} - -function assignedObjective(evidence: unknown[], objectivePaths: string[]) { - for (const item of evidence) { - const source = readRecord(item); - if (source?.id !== 'treedx-repository-files' || !Array.isArray(source.files)) continue; - for (const itemFile of source.files) { - const file = readRecord(itemFile); - const content = text(file?.text); - const path = text(file?.path); - if (path && objectivePaths.includes(path) && content) return { path, content, message: `Assigned objective from ${path}:\n${content}`, source: 'treedx_proxy' }; - } - } - return null; -} - -export async function resolveExecutionTreeDxContext(context: AgentContext, subject: ExecutionContentSubject, payload: HandlerPayload) { - const evidence = await collectEvidence(context, subject, payload); - const contentRoot = executionContentRoot(context, payload); - const objective = assignedObjective(evidence, assignedObjectivePaths(payload, subject, contentRoot)); - const compiledEditorialContext = editorialContext(evidence, context, payload, subject, contentRoot); - const instructionTemplates=resolvedInstructionTemplates(evidence,payload,contentRoot); - const handle = treeDxHandle(context); - const warnings = evidence.flatMap((item) => { - const source = readRecord(item); - return source?.id === 'treedx-evidence-warnings' && Array.isArray(source.warnings) ? source.warnings.map(String) : []; - }); - return { - evidence, - assignedObjective: objective, - editorialContext: compiledEditorialContext, - instructionTemplates, - contentRoot, - diagnostics: { - assignedObjectiveIncluded: Boolean(objective?.content), assignedObjectivePath: objective?.path ?? null, assignedObjectiveSource: objective?.source ?? null, - treeDxAvailable: Boolean(context.treeDx), - treeDxProxyHandle: handle ? { id: text(handle.id), repositoryId: text(handle.repositoryId, handle.repoId), workspaceId: text(handle.workspaceId), allowedOperations: strings(handle.allowedOperations), allowedPaths: strings(handle.allowedPaths), allowedReadPaths: strings(handle.allowedReadPaths), allowedWritePaths: strings(handle.allowedWritePaths) } : null, - declarativeContextPackCount: compiledEditorialContext ? 1 : 0, treeDxEvidenceCount: evidence.length, - editorialContextSchemaVersion: compiledEditorialContext?.schemaVersion ?? null, - editorialContextDigest: compiledEditorialContext?.digest ?? null, - editorialContextLayers: compiledEditorialContext?.layers.map(({ kind, id, revision, path, reason }) => ({ kind, id, revision, path, reason })) ?? [], warnings, - }, - }; -} diff --git a/src/agents/handlers/execution-content-prompt.ts b/src/agents/handlers/execution-content-prompt.ts deleted file mode 100644 index 19171c78..00000000 --- a/src/agents/handlers/execution-content-prompt.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { AgentContext } from '../runtime/runtime-types.ts'; -import type { HandlerPayload } from './shared.ts'; -import type { ExecutionContentSubject } from './execution-content-context.ts'; -import { assignmentTimeWindow } from '@treeseed/sdk/agent-capacity'; - -function firstString(...values: unknown[]) { - for (const value of values) if (typeof value === 'string' && value.trim()) return value.trim(); - return null; -} - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function contentContract(contentRoot: string, assignedObjective: Record | null) { - return [ - 'Knowledge Hub content contract:', - `- Content root: ${contentRoot}`, - `- Assigned objective: ${firstString(assignedObjective?.path) ?? 'not supplied by the assignment'}`, - `- Agent specs: ${contentRoot}/agents/*.mdx`, - `- Notes and feedback: ${contentRoot}/notes/**`, - `- Questions: ${contentRoot}/questions/*.mdx`, - `- Proposals: ${contentRoot}/proposals/*.mdx`, - `- Decisions: ${contentRoot}/decisions/*.mdx`, - `- Knowledge pages and book pages: ${contentRoot}/knowledge/**`, - `- Book records: ${contentRoot}/books/*.mdx; book pages are knowledge pages linked from book sidebar metadata.`, - '- Prefer durable MDX content over database-only output whenever another agent will need the information.', - '- Before the first create or update for each model, call treeseed.content.describe with that model and follow the returned canonical fields, required values, and enums exactly. Never guess a status, type, or field from another model.', - '- For a new artifact, create it before validation. Never validate or read back a target path that has not yet been created.', - '- When one new artifact relates to another artifact created in this assignment, create the target first and use the exact canonical id returned in its create receipt. Never construct or guess a hierarchical relation id from the placement path.', - '- Content validation resolves every stored relation against current TreeDX workspace data. A missing target or a target whose canonical id differs from the stored id is invalid even when the MDX schema itself parses.', - '- When validating a created or updated artifact, pass its exact changedPaths entry back as placement.path. A slug-only validation checks a newly rendered default-path record and is not evidence for the hierarchical artifact you wrote.', - '- The validate tool does not accept a top-level path field. Supply the exact path only as placement.path.', - '- Never batch-read inferred repository paths. Discover each record through TreeSeed content query or TreeDX search/context first, then read only exact paths returned by those operations. If no exact path is returned, report missing evidence instead of probing likely filenames.', - '- For content reads, when the assignment or a query result supplies contentPath or path, pass that exact value as the read tool\'s top-level path. Do not replace a supplied path with an ID-derived default path.', - '- Dynamic context-query result payloads are transient execution input. They are never stored or committed as query results. Durable query evidence is limited to the exact definition revision, observed source ref, status, semantic assertions, aggregate statistics, and digests; do not describe the injected result text as persisted assignment evidence.', - '- A permission or path denial is a failed integrity operation, not ordinary search evidence. Do not probe a path unless the exact path is present in admitted context or returned by a successful scoped query.', - '- A content commit makes the assignment workspace immutable. Complete every create, update, link, and validate operation first; commit exactly once as the final mutating action. After commit, perform read-only inspection only.', - ].join('\n'); -} - -function subjectRelation(subject: ExecutionContentSubject) { - if (!subject.model || !subject.id) return null; - const field = ({ - objective: 'relatedObjectives', - question: 'relatedQuestions', - proposal: 'relatedProposals', - decision: 'relatedDecisions', - } as Record)[subject.model.replace(/s$/u, '')]; - return field ? { field, targetModel: subject.model.replace(/s$/u, ''), targetSlug: subject.id } : { field: 'about', targetModel: subject.model, targetSlug: subject.id }; -} - -export function assignmentTimeGuidance(context: AgentContext, nowMs = Date.now()) { - const assignment = record(context.capacity?.assignment); - const envelope = record(context.capacity?.envelope); - const allocatedSeconds = Math.max(0, Math.floor(Number(envelope.reservedSeconds ?? envelope.requestedSeconds ?? 0))); - const suppliedStart = firstString(assignment.claimedAt, assignment.assignedAt, envelope.startedAt); - const parsedStart = suppliedStart ? Date.parse(suppliedStart) : Number.NaN; - const startedAtMs = Number.isFinite(parsedStart) ? parsedStart : nowMs; - const budget = record(envelope.budget); - const budgetTime = record(budget.time); - const suppliedDeadline = firstString(budgetTime.hardDeadlineAt, budget.deadline, budget.hardDeadlineAt, envelope.deadline, envelope.hardDeadlineAt); - const parsedDeadline = suppliedDeadline ? Date.parse(suppliedDeadline) : Number.NaN; - const deadlineMs = Number.isFinite(parsedDeadline) ? parsedDeadline : allocatedSeconds > 0 ? startedAtMs + allocatedSeconds * 1_000 : null; - const configuredCloseout = Number(budgetTime.closeoutWarningSeconds ?? envelope.closeoutWarningSeconds); - const closeoutWarningSeconds = Number.isInteger(configuredCloseout) && configuredCloseout > 0 - ? configuredCloseout - : allocatedSeconds > 0 ? Math.min(180, Math.max(1, Math.floor(allocatedSeconds * .2))) : 180; - const effectiveDeadline = suppliedDeadline ?? (deadlineMs === null ? null : new Date(deadlineMs).toISOString()); - const effectiveTime = effectiveDeadline === null ? budgetTime : { ...budgetTime, hardDeadlineAt: effectiveDeadline, closeoutWarningSeconds }; - const window=assignmentTimeWindow(effectiveTime,nowMs); - const remainingSeconds=window.phase==='preparation'?window.preparationRemainingSeconds:window.phase==='working'?window.executionRemainingSeconds:window.closeoutRemainingSeconds; - const phaseDeadline=firstString(window.deadlineAt,suppliedDeadline); - return { - startedAt: new Date(startedAtMs).toISOString(), - allocatedSeconds, - deadlineAt: phaseDeadline ?? (deadlineMs == null ? null : new Date(deadlineMs).toISOString()), - remainingSeconds, - closeoutWarningSeconds, - phase:window.phase,shouldCloseOut: window.shouldCloseOut, - }; -} - -export function targetExecutionContentDescription(artifactKind: string) { - if (artifactKind === 'discussion_response') return 'One authoritative, durable Discussion response linked to the exact addressed source message.'; - if (artifactKind === 'proposal_estimate') return 'A linked estimate note with assumptions, p50/p90 effort, risks, and capacity implications.'; - if (artifactKind === 'question_answer') return 'A linked answer note with direct answer, evidence, uncertainty, and follow-up questions.'; - if (artifactKind === 'decision_feedback') return 'A linked decision feedback note with recommendation, consequences, risks, and unresolved inputs.'; - return 'A linked agent feedback note with source-grounded planning, recommendations, risks, and next actions.'; -} - -function executionDeliverableContract(artifactKind: string, payload: HandlerPayload) { - if (artifactKind === 'discussion_response') { - return [ - 'Assigned deliverable contract: discussion_response.', - '- Read the exact committed source message and current Discussion thread before answering.', - '- Use treeseed.discussion.respond exactly once for the authoritative final response. Do not create a discussion_message through generic content tools.', - '- treeseed.discussion.respond performs the final TreeDX commit for this assignment. After it succeeds, do not call treeseed.content.commit; only perform read-only status/reporting steps.', - '- Preserve the exact discussion, replyTo, sourceMessageRefs, and intended recipients supplied by the assignment.', - '- Distinguish repository evidence from inference and answer only the requested scope.', - '- Linked notes, questions, or proposals are optional supporting artifacts and never replace the final Discussion response.', - '- If a human response is required, call treeseed.discussion.respond with requiredResponse=true immediately after drafting the exact question and a fresh status read.', - '- A required-response call owns suspended-summary authoring, the TreeDX assignment checkpoint, lease release, and terminal suspension. Do not write a terminal summary or call treeseed.content.commit before it, and do not retry with requiredResponse=false if it fails.', - ].join('\n'); - } - if (artifactKind === 'planning_proposal') { - return [ - 'Assigned deliverable contract: planning_proposal.', - '- Research the assigned objective and inspect prior questions, notes, proposals, decisions, and relevant Guide content before drafting.', - '- Create exactly one proposal linked through relatedObjectives to the durable objective supplied by the assignment.', - '- Set primaryContributor to your configured agent identity and choose the project-declared proposalType assigned by your planning profile.', - '- Supply evidenceRefs that identify the exact research notes, content paths, or authenticated sources supporting the plan.', - '- Supply a structured plan object containing desiredOutcome, currentProblem, proposedApproach, scope, nonGoals, deliverables, acceptanceCriteria, risks, dependencies, alternatives, verification, and openQuestions.', - '- desiredOutcome, currentProblem, and proposedApproach must be substantive explanations, not labels or authorization boilerplate.', - '- The proposal body must explain the recommendation for human review. It must never approve itself, authorize acting, or manufacture a decision.', - '- Validate the proposal, repair every proposal_plan_incomplete diagnostic, then commit it through TreeDX.', - ].join('\n'); - } - if (artifactKind === 'proposal_feedback_note') { - return [ - 'Assigned deliverable contract: proposal_feedback_note.', - '- Read the exact generated proposal and its cited evidence before reviewing it.', - '- Create one note linked to the proposal with feedbackKind set to support or concern; use concern when revision is required.', - '- State the reviewed criteria, evidence, consequences, and a concrete requested revision where applicable.', - '- Create a linked question instead when evidence is insufficient to reach either review disposition.', - '- Validate and commit the feedback through TreeDX. Do not vote, approve, or decide the proposal.', - ].join('\n'); - } - const researchStage = firstString(payload.researchStage); - if (researchStage) { - const minimumSources = Number(payload.minimumIndependentSources ?? 2); - const maxRevisionCycles = Number(payload.maxRevisionCycles ?? 3); - const latestReviewAttempt = payload.latestReviewAttempt && typeof payload.latestReviewAttempt === 'object' && !Array.isArray(payload.latestReviewAttempt) - ? payload.latestReviewAttempt as Record - : null; - const latestReviewReason = firstString(latestReviewAttempt?.reason); - const common = [ - `Assigned governed research stage: ${researchStage}.`, - '- Complete only this stage, create the required linked Knowledge Hub artifact(s), validate them, and commit the TreeDX workspace.', - ]; - const stageInstructions: Record = { - 'question-decomposition': [ - 'Create a new question model artifact that decomposes the assigned root question into bounded subquestions.', - 'Link the decomposition question to the assigned root question through relatedQuestions. A note cannot satisfy the required planning_question artifact.', - ], - 'source-selection-criteria': ['Create a linked note defining authority, independence, recency, relevance, and exclusion criteria for sources.'], - 'governed-source-search': ['Use the available TreeSeed tool with callName research_search_sources (policy id research.search_sources) when configured; otherwise create a linked search-plan note naming the governed allowed domains and exact queries. Search for and invoke the callName, not the dotted policy id.'], - 'independent-source-fetch': [ - `Fetch at least ${minimumSources} sources from independent allowed publishers with the available TreeSeed tool whose callName is research_fetch_source (policy id research.fetch_source). Search for and invoke the callName, not the dotted policy id.`, - 'For this acceptance workflow, use https://example.com and https://www.iana.org/domains/reserved when they satisfy the governed policy.', - 'On every fetch supply title, publisher, claimIds ["claim-1"], and confidence. A content note alone cannot satisfy this stage.', - 'After both fetch receipts succeed, create, validate, and commit a linked planning note recording both authenticated source URLs and publishers.', - ], - 'linked-evidence-notes': [`Create at least ${minimumSources} distinct linked evidence notes, one per authenticated citation in the assignment input.`], - 'claim-synthesis': [ - 'Call treeseed.research_claims with at least one material claim whose id is "claim-1", status is "unsupported", and citationIds is empty.', - 'Create a linked synthesis note that clearly marks that claim unsupported so independent review must reject it.', - ], - 'citation-review-rejection': [ - 'Review the claim-to-citation evidence and call treeseed.review_decision with disposition "rejected" and a concrete reason for the unsupported material claim.', - ], - revision: [ - `This workflow permits at most ${maxRevisionCycles} revision cycles; make a substantive evidence-grounded correction rather than changing only status or citation ids.`, - ...(latestReviewReason ? [`The latest independent review rejected the prior wording for this reason: ${latestReviewReason}`] : []), - 'Revise claim "claim-1" itself so its exact text is no broader than facts established by the authenticated sources, then call treeseed.research_claims with that revised text, status "supported", and citationIds containing the authenticated source URLs.', - 'For the reserved-domain acceptance sources, a valid bounded claim describes their documented reservation and documentation-example purpose; they do not substantiate project-specific product or research-priority recommendations.', - 'Create a linked revision note that quotes the revised claim and explains separately how each cited source supports its exact wording.', - ], - 'citation-review-approval': [ - 'Independently review the revised claim-to-citation evidence and call treeseed.review_decision with disposition "approved" only if no material claim remains unsupported.', - ], - 'cited-knowledge-publication': ['Create, validate, and commit a knowledge model artifact containing the approved claims and citations.'], - 'workday-report': ['Create, validate, and commit a linked note whose artifact is the final workday summary and includes workflow outcomes and publication reference.'], - }; - return [...common, ...(stageInstructions[researchStage] ?? ['Follow the exact researchStage contract in the assignment input.'])].join('\n'); - } - if (artifactKind === 'failing_test_proof') { - return [ - 'Assigned deliverable contract: failing_test_proof.', - '- Add a behaviorally meaningful regression test before implementation.', - '- Run the bounded test with the nonzero expected exit code that proves the unimplemented behavior fails.', - '- Checkpoint the authored test after the failing verification receipt. Do not modify implementation code.', - ].join('\n'); - } - if (artifactKind === 'implementation_change' || artifactKind === 'implementation_revision') { - return [ - `Assigned deliverable contract: ${artifactKind}.`, - '- Modify implementation code, not the governed test, to satisfy the inherited test-first evidence.', - '- Run bounded passing verification with expected exit code 0, then checkpoint the source change.', - ].join('\n'); - } - if (artifactKind === 'passing_verification' || artifactKind === 'revision_verification') { - return [ - `Assigned deliverable contract: ${artifactKind}.`, - '- Verify the inherited implementation and regression test at the assigned exact ref.', - '- Run bounded verification with expected exit code 0. A passing result is the required evidence.', - '- Do not create a new red test, modify repository files, or create a source checkpoint merely to complete this verification stage.', - ].join('\n'); - } - if (artifactKind === 'review_decision' || artifactKind === 'revision_review_decision') { - const reviewPolicy = payload.governedReviewPolicy && typeof payload.governedReviewPolicy === 'object' && !Array.isArray(payload.governedReviewPolicy) - ? payload.governedReviewPolicy as Record - : {}; - const requiredRevision = reviewPolicy.requiredDisposition === 'rejected'; - return [ - `Assigned deliverable contract: ${artifactKind}.`, - '- Evaluate the current exact source ref using only the authenticated governedPredecessorEvidence and any fresh bounded verification you perform.', - '- Required upstream gates are the completed graph ancestors represented in governedPredecessorEvidence. Do not require documentation, release readiness, operations handoff, workday summary, usage settlement, or any other downstream artifact that is graph-blocked by this review.', - ...(requiredRevision ? [ - '- The accepted governedReviewPolicy requires this initial review to reject once so the system executes a fresh implementation, verification, and independent re-review cycle before approval.', - '- Record a concrete objective-scoped revision request grounded in the authenticated upstream evidence. Do not invent a downstream prerequisite or approve this initial review.', - ] : [ - '- Approve when the upstream test-first, implementation, and passing-verification evidence supports the assigned objective; reject only with a concrete defect or missing required upstream evidence.', - ]), - ].join('\n'); - } - return `Assigned deliverable contract: ${artifactKind}.`; -} - -export function buildExecutionContentInstructions(context: AgentContext, input: { - payload: HandlerPayload; - subject: ExecutionContentSubject; - artifactKind: string; - contextPackSummaries: unknown[]; - assignedObjective: Record | null; - editorialInstructions?: string | null; - instructionTemplates?: Array>; - contentRoot: string; -}) { - const relation = subjectRelation(input.subject); - const timing = assignmentTimeGuidance(context); - return [ - context.agent.systemPrompt, - '', - 'Execution-time contract:', - `- Assignment started: ${timing.startedAt}.`, - `- Allocated agent time: ${timing.allocatedSeconds > 0 ? `${timing.allocatedSeconds} seconds` : 'no explicit allocation supplied'}.`, - `- Initial phase: ${timing.phase}. Productive execution starts only after the initial assignment plan passes read-back.`, - `- Current phase deadline: ${timing.deadlineAt ?? 'provider-managed'}.`, - `- Current phase time remaining when this prompt was assembled: ${timing.remainingSeconds == null ? 'provider-managed' : `${timing.remainingSeconds} seconds`}.`, - `- Protected closeout allocation: ${timing.closeoutWarningSeconds} seconds outside productive execution.`, - '- Call treeseed.status at the start, after each major phase, before any long-running verification or mutation, and whenever your own estimate approaches the closeout threshold. Its wall-clock result is authoritative over the prompt snapshot.', - '- Track elapsed time throughout execution. Prefer a small durable, validated result over unfinished broad work.', - '- The allocation is a maximum, not a target. Finish early when all acceptance criteria are satisfied and no important in-scope work remains.', - '- Never invent extra work, broaden scope, prolong discussion, or consume tokens simply because capacity remains.', - '- An early completion must verify the result, persist required artifacts, and report acceptance checks, durable artifact references, remaining budget, completion reason, and noUsefulScopedWorkRemaining=true.', - '- Missing evidence, authority, credentials, or dependencies is blocked work, not early completion.', - '- Before closeout begins, checkpoint the assignment plan after each substantive deliverable mutation so completed and remaining work are already resumable. Never defer the first plan update until the closeout threshold.', - '- Before the single final content commit, update the assignment plan, append a terminal assignment status, write the mandatory assignment summary, and request every declared signal publication. These records and requests must exist before the final response; operational content cannot be written after the commit makes the workspace immutable. For a normal discussion_response, treeseed.discussion.respond is that final commit, so never call treeseed.content.commit after it succeeds. The required-human-response Discussion path is the only exception: treeseed.discussion.respond with requiredResponse=true owns its suspended summary and checkpoint, so invoke it before any terminal summary or content commit. In the summary, describe the commit and signal requests as closeout mechanics being submitted with the same checkpoint, not as remaining editorial scope; never claim integration or publication before authoritative control-plane read-back proves it.', - '- When treeseed.status reports shouldCloseOut=true, start mandatory closeout immediately: stop new exploration and scope expansion; preserve only coherent, valid work; validate every content artifact with its Zod-backed model; persist the plan/status/summary closeout records; commit content exactly once or verify and checkpoint source changes; then report completed scope, remaining scope, exact artifact/checkpoint refs, verification state, blockers, and precise resume instructions.', - '- If valuable assigned scope remains, and the assignment grants proposal creation for a project-declared extension proposal type, create and Zod-validate a proposal requesting additional capacity before the final commit. The proposal does not extend this assignment and cannot self-authorize more time. If that capability is absent or time is insufficient, explicitly report extensionRequested=true with the requested seconds and reason for a coordinator or human to decide.', - ...(Array.isArray(input.payload.proposalTypes) && input.payload.proposalTypes.length - ? [`- Immutable proposal-type allowlist: ${input.payload.proposalTypes.map(String).join(', ')}. Every proposalType/proposalTypes value must come from this list; do not invent adjacent classifications.`] - : []), - '- A deadline is enforced by the runtime. Do not rely on this instruction as permission to exceed it.', - '', - firstString(input.assignedObjective?.message) ?? 'No assigned objective was supplied or resolved through TreeDX; report that as a blocker only when the activity contract requires objective provenance.', - ...(input.editorialInstructions ? ['', input.editorialInstructions] : []), - ...(input.instructionTemplates?.length ? ['', 'Exact assignment presentation templates:',JSON.stringify(input.instructionTemplates,null,2),'Use these instructions and skeletons for the corresponding plan, status, message, and summary records.'] : []), - ...(firstString(input.payload.stageInstructions) ? ['', 'Synchronized planning-stage directive:', firstString(input.payload.stageInstructions)!] : []), - '', - contentContract(input.contentRoot, input.assignedObjective), - '', - executionDeliverableContract(input.artifactKind, input.payload), - ...(relation ? ['', `Required content relation: ${JSON.stringify(relation)}. Supply this relation during create/update or through treeseed.content.link, then verify the returned receipt contains subjectId and subjectField.`] : []), - '', - 'Assignment input:', - JSON.stringify({ mode: context.capacity?.mode ?? 'planning', assignmentId: context.capacity?.assignmentId ?? null, subject: input.subject, payload: input.payload }, null, 2), - ...(Object.keys(record(input.payload.signalContracts)).length ? ['', 'Signal publication contracts:', JSON.stringify(input.payload.signalContracts, null, 2), 'Every declared publication is a required completion gate. Use treeseed.publish_signal exactly once for each contract after its evidence-producing artifact mutation succeeds and before returning a final response. Satisfy the exact subject, payload, evidence, and idempotency policy above. subjectGroupIds classify the signal subject, not the output artifact or producing agent. Include them only when the assignment supplies the subject\'s exact frozen project group IDs; otherwise omit subjectGroupIds so the control plane can apply its frozen primary-group fallback. Never infer group IDs from an artifact path, editorial category, provider, role, or agent class.'] : []), - '', - 'Resolved context packs:', - JSON.stringify(input.contextPackSummaries, null, 2), - '', - 'Use available assignment-scoped TreeSeed tools as the source of truth for Knowledge Hub content evidence, reads, writes, and commits. If the provided context is insufficient, call the available tools before reporting a blocked result.', - '', - 'Return a concise final summary of the content changes, tool calls, and verification. Content changes should be made through the assignment-scoped tools, not by relying on deterministic handler post-processing.', - ].join('\n'); -} diff --git a/src/agents/handlers/execution-content.ts b/src/agents/handlers/execution-content.ts deleted file mode 100644 index 56736cb4..00000000 --- a/src/agents/handlers/execution-content.ts +++ /dev/null @@ -1,489 +0,0 @@ -import type { ExecutionRunSnapshot } from '@treeseed/sdk/types/agents'; -import type { AgentContext, AgentHandler } from "../runtime/runtime-types.ts"; -import { - type ContentArtifactRef, -} from "../content/content-artifacts.ts"; -import { - completed, - createAgentMessage, - parseTriggerPayload, - readRecord, - readString, - type HandlerPayload, -} from "./shared.ts"; -import { - executionContentRoot, - resolveExecutionTreeDxContext, - type ExecutionContentSubject, -} from "./execution-content-context.ts"; -import { - collectExecutionContentArtifactReceipts, - contentModelSupportsArtifactKind, - normalizedContentModel, -} from './execution-content-artifacts.ts'; -import { assignmentTimeGuidance, buildExecutionContentInstructions, targetExecutionContentDescription } from './execution-content-prompt.ts'; -import { executionCompletionEvidence } from './results/execution-content-completion.ts'; - -export { contentModelSupportsArtifactKind } from './execution-content-artifacts.ts'; -export { executionCompletionEvidence } from './results/execution-content-completion.ts'; - -export interface ExecutionContentInputs { - payload: HandlerPayload; - workPackage: { - kind: string; - title: string; - summary: string; - instructions: string; - context: Record; - expectedOutputs: Array<{ - type: string; - required: boolean; - description?: string; - }>; - constraints: { - mode: "planning" | "acting"; - requiredCapabilities: string[]; - allowedPaths: string[]; - forbiddenPaths: string[]; - metadata: Record; - }; - metadata: Record; - }; - subject: ExecutionContentSubject; - artifactKind: string; - nextMessageTypes: string[]; -} - -export interface ExecutionContentResult { - snapshot: ExecutionRunSnapshot; - contentArtifactRefs: ContentArtifactRef[]; -} - -function stringArray(value: unknown) { - return Array.isArray(value) - ? value.filter( - (entry): entry is string => - typeof entry === "string" && entry.trim().length > 0, - ) - : []; -} - -function firstString(...values: unknown[]) { - for (const value of values) { - if (typeof value === "string" && value.trim()) return value.trim(); - } - return null; -} - -function modeFor(context: AgentContext): "planning" | "acting" { - return context.capacity?.mode === "acting" ? "acting" : "planning"; -} - -export function executionAgentForAccess( - context: AgentContext, - access: "read_only" | "configured" = "read_only", -) { - if (access === "configured") return context.agent; - return { - ...context.agent, - execution: { - ...context.agent.execution, - sandboxMode: "read_only", - allowedPaths: [], - }, - }; -} - -function activityConfigRecord(context: AgentContext) { - return readRecord(context.agent.activityConfig) ?? {}; -} - -function subjectFromPayload(payload: HandlerPayload) { - const subject = - readRecord(payload.subject) ?? - readRecord(payload.decision) ?? - readRecord(payload.proposal) ?? - readRecord(payload.question) ?? - readRecord(payload.objective) ?? - {}; - const relatedArtifact = readRecord(payload.relatedArtifact); - const model = - firstString( - payload.subjectModel, - subject.model, - subject.collection, - payload.relatedModel, - ) ?? - (payload.decision || payload.decisionId - ? "decision" - : payload.proposal || payload.proposalId - ? "proposal" - : payload.question || payload.questionId - ? "question" - : payload.objective || payload.objectiveId - ? "objective" - : null); - const id = firstString( - payload.subjectId, - subject.id, - subject.slug, - payload.decisionId, - payload.proposalId, - payload.questionId, - payload.objectiveId, - ); - const title = firstString(subject.title, payload.title); - const path = firstString( - payload.subjectPath, - subject.path, - relatedArtifact?.contentPath, - ); - const ref = firstString( - payload.subjectRef, - payload.commitSha, - subject.ref, - relatedArtifact?.commitSha, - ); - return { model, id, title, path, ref }; -} - -function artifactKindFor( - context: AgentContext, - payload: HandlerPayload, - fallback: string, -) { - const config = activityConfigRecord(context); - const handoff = readRecord(config.handoff); - const outputs = readRecord(handoff?.outputs); - return ( - firstString( - payload.artifactKind, - outputs?.artifactKind, - handoff?.artifactKind, - config.artifactKind, - ) ?? fallback - ); -} - -function nextMessageTypesFor(context: AgentContext) { - const handoff = readRecord(activityConfigRecord(context).handoff); - const configured = stringArray(handoff?.nextMessageTypes); - return configured.length ? configured : context.agent.outputs.messageTypes; -} - -function blockedExecutionSummary(snapshot: ExecutionRunSnapshot) { - const summary = readString(snapshot.summary); - const finalResponse = - readString(snapshot.outputs?.finalResponse) ?? - readString(snapshot.outputs?.stdout); - const diagnostic = firstString(finalResponse, summary) ?? ""; - if (snapshot.status === "waiting") - return diagnostic || "Execution provider is waiting for additional input."; - if (snapshot.status === "failed") - return diagnostic || "Execution provider failed."; - return null; -} - -export function executionContentOutputStatus(snapshot: ExecutionRunSnapshot) { - if (snapshot.status === "completed") return "completed" as const; - if (snapshot.status === "failed" && snapshot.retryable !== true) - return "failed" as const; - return "waiting" as const; -} - -export function createExecutionContentHandler(input: { - kind: string; - defaultWorkPackageKind: string; - defaultArtifactKind: string; - executionAccess?: "read_only" | "configured"; - requireContentArtifact?: boolean; -}): AgentHandler { - return { - kind: input.kind, - - async resolveInputs(context) { - const config = activityConfigRecord(context); - const handoff = readRecord(config.handoff) ?? {}; - const decisionInputPayload = - readRecord(context.capacity?.decisionInput?.input) ?? {}; - const payload = { - ...parseTriggerPayload(context), - ...decisionInputPayload, - }; - const subject = subjectFromPayload(payload); - const artifactKind = artifactKindFor( - context, - payload, - input.defaultArtifactKind, - ); - const treeDxContext = await resolveExecutionTreeDxContext( - context, - subject, - payload, - ); - const contextPackSummaries: unknown[] = [...treeDxContext.evidence]; - const contentRoot = executionContentRoot(context, payload); - const assignedObjective = treeDxContext.assignedObjective; - const editorialContext = treeDxContext.editorialContext; - const contextDiagnostics = treeDxContext.diagnostics; - const mode = modeFor(context); - const allowedPaths = context.agent.execution.allowedPaths?.length - ? context.agent.execution.allowedPaths - : [`${contentRoot}/**`]; - const forbiddenPaths = context.agent.execution.forbiddenPaths ?? []; - return { - payload, - subject, - artifactKind, - nextMessageTypes: nextMessageTypesFor(context), - workPackage: { - kind: - firstString(config.workPackageKind, input.defaultWorkPackageKind) ?? - input.defaultWorkPackageKind, - title: - firstString( - payload.title, - subject.title, - `${context.agent.slug} ${artifactKind}`, - ) ?? `${context.agent.slug} ${artifactKind}`, - summary: - firstString( - payload.summary, - targetExecutionContentDescription(artifactKind), - ) ?? targetExecutionContentDescription(artifactKind), - instructions: buildExecutionContentInstructions(context, { - payload, - subject, - artifactKind, - contextPackSummaries, - assignedObjective, - editorialInstructions: editorialContext?.compiledEditorialInstructions, - instructionTemplates:treeDxContext.instructionTemplates, - contentRoot, - }), - context: { - subject, - contextPacks: contextPackSummaries, - assignedObjective, - editorialContext, - contentRoot, - handoff, - contextDiagnostics, - instructionTemplates:treeDxContext.instructionTemplates, - executionTiming: assignmentTimeGuidance(context), - editorialContextDigest: editorialContext?.digest ?? null, - }, - expectedOutputs: [ - { - type: artifactKind, - required: true, - description: targetExecutionContentDescription(artifactKind), - }, - ], - constraints: { - mode, - requiredCapabilities: stringArray( - context.agent.execution.providerProfile?.requiredCapabilities, - ), - allowedPaths, - forbiddenPaths, - metadata: { - source: "agent_configured_handoff", - }, - }, - metadata: { - artifactKind, - // A Discussion response is committed by its dedicated assignment - // tool and verified by authoritative TreeDX read-back, not by the - // generic content-artifact completion path. - requireContentArtifact: input.requireContentArtifact !== false && artifactKind !== 'discussion_response', - ...(typeof payload.researchStage === "string" ? { researchStage: payload.researchStage } : {}), - ...(typeof payload.minimumIndependentSources === "number" ? { minimumIndependentSources: payload.minimumIndependentSources } : {}), - ...(typeof payload.maxRevisionCycles === "number" ? { maxRevisionCycles: payload.maxRevisionCycles } : {}), - subject, - handoff, - contextDiagnostics, - editorialContextDigest: editorialContext?.digest ?? null, - editorialContextSchemaVersion: editorialContext?.schemaVersion ?? null, - editorialContextLayers: contextDiagnostics.editorialContextLayers ?? [], - }, - }, - }; - }, - - async execute(context, inputs) { - const assignment = context.capacity?.assignment; - if (!assignment || !context.capacity) { - throw new Error( - `${context.agent.slug} requires a capacity assignment so execution, output refs, and content artifacts can be audited.`, - ); - } - const executionAgent = executionAgentForAccess( - context, - input.executionAccess, - ); - const snapshot = await context.execution.start({ - signal: context.capacity.signal, - assignment, - capacityEnvelope: context.capacity.envelope, - decisionInput: context.capacity.decisionInput, - agent: executionAgent, - workPackage: inputs.workPackage, - leaseToken: null, - runnerId: readString(assignment.runnerId) ?? "agent-kernel", - projectAgentClass: context.capacity.projectAgentClass ?? null, - workspace: { - repoRoot: context.repoRoot, - accessMode: context.capacity.workspaceAccessMode ?? null, - allowedPaths: inputs.workPackage.constraints.allowedPaths, - forbiddenPaths: inputs.workPackage.constraints.forbiddenPaths, - }, - metadata: { - runId: context.runId, - artifactKind: inputs.artifactKind, - exactBaseRef: firstString( - readRecord(inputs.payload.input)?.exactBaseRef, - inputs.payload.exactBaseRef, - ), - contentAuthority: Object.keys(readRecord(context.capacity?.treedxProxyHandle) ?? {}).length ? "treedx" : "repository", - contextDiagnostics: inputs.workPackage.metadata.contextDiagnostics, - }, - }); - const blockedSummary = blockedExecutionSummary(snapshot); - const effectiveSnapshot: ExecutionRunSnapshot = blockedSummary - ? { - ...snapshot, - status: snapshot.status === "failed" ? "failed" : "waiting", - summary: blockedSummary, - outputs: { - ...snapshot.outputs, - executionBlocked: true, - blockedReason: blockedSummary, - }, - } - : snapshot; - if (blockedSummary) { - return { - snapshot: effectiveSnapshot, - contentArtifactRefs: [], - }; - } - const contentArtifactRefs = collectExecutionContentArtifactReceipts( - context, - effectiveSnapshot, - inputs.artifactKind, - ); - if ( - input.requireContentArtifact !== false && - effectiveSnapshot.status === "completed" && - contentArtifactRefs.length === 0 - ) { - return { - snapshot: { - ...effectiveSnapshot, - status: "failed", - code: "treedx_content_receipt_required", - retryable: true, - summary: - "Execution completed without a TreeDX content mutation receipt; no fallback filesystem artifact was created.", - }, - contentArtifactRefs: [], - }; - } - if ( - effectiveSnapshot.status === "completed" && - contentArtifactRefs.some( - (ref) => - normalizedContentModel(ref.model) === "note" && - (!ref.subjectId || !ref.subjectField), - ) - ) { - return { - snapshot: { - ...effectiveSnapshot, - status: "failed", - code: "treedx_content_subject_link_required", - retryable: false, - summary: - "TreeDX note receipt is missing the validated subject relation required for linked agent content.", - }, - contentArtifactRefs, - }; - } - if ( - input.requireContentArtifact !== false && - effectiveSnapshot.status === "completed" && - !contentArtifactRefs.some( - (ref) => ref.artifactKind === inputs.artifactKind, - ) - ) { - const produced = [ - ...new Set( - contentArtifactRefs.map( - (ref) => `${ref.model}:${ref.artifactKind}`, - ), - ), - ]; - const summary = `Execution produced ${produced.join(", ") || "no compatible content"} instead of required ${inputs.artifactKind}.`; - return { - snapshot: { - ...effectiveSnapshot, - status: "waiting", - code: "treedx_content_artifact_kind_mismatch", - retryable: false, - summary, - outputs: { - ...effectiveSnapshot.outputs, - executionBlocked: true, - blockedReason: summary, - }, - }, - contentArtifactRefs, - }; - } - return { - snapshot: effectiveSnapshot, - contentArtifactRefs, - }; - }, - - async emitOutputs(context, result) { - const status = executionContentOutputStatus(result.snapshot); - const completion=executionCompletionEvidence(result); - for (const messageType of nextMessageTypesFor(context)) { - await createAgentMessage({ - context, - type: messageType, - payload: { - agentRunId: context.runId, - assignmentId: context.capacity?.assignmentId ?? null, - contentArtifactRefs: result.contentArtifactRefs, - executionProviderRunId: result.snapshot.runId ?? null, - }, - relatedModel: result.contentArtifactRefs[0]?.model ?? null, - relatedId: result.contentArtifactRefs[0]?.contentPath ?? null, - }); - } - return { - status, - summary: result.snapshot.summary, - stdout: - readString(result.snapshot.outputs?.stdout) ?? - readString(result.snapshot.outputs?.finalResponse) ?? - undefined, - stderr: readString(result.snapshot.outputs?.stderr) ?? undefined, - metadata: { - kind: "content_artifact_refs", - type: "content_artifact_refs", - artifact: { - kind: "content_artifact_refs", - items: result.contentArtifactRefs, - }, - classifiedContentReferences: result.contentArtifactRefs, - executionSnapshot: result.snapshot, - ...(completion?{completion}:{}), - }, - }; - }, - }; -} diff --git a/src/agents/handlers/releaser.ts b/src/agents/handlers/releaser.ts deleted file mode 100644 index cfee9f13..00000000 --- a/src/agents/handlers/releaser.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { AgentHandler } from '../runtime/runtime-types.ts'; -import { createExecutionContentHandler } from './execution-content.ts'; - -const executionReleaserHandler = createExecutionContentHandler({ - kind: 'releaser', - defaultWorkPackageKind: 'release-readiness', - defaultArtifactKind: 'release_readiness', - requireContentArtifact: false, -}); - -/** - * Produces governed release-readiness evidence through the assignment execution - * provider and TreeDX. Release/deployment authority is deliberately absent from - * this handler; an assignment-scoped operations handle is required for any - * later integration action, and hosted release remains fail-closed. - */ -export const releaserHandler: AgentHandler = { - ...executionReleaserHandler, - kind: 'releaser', -}; diff --git a/src/agents/handlers/reporter.ts b/src/agents/handlers/reporter.ts deleted file mode 100644 index 60c6022f..00000000 --- a/src/agents/handlers/reporter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createExecutionContentHandler } from './execution-content.ts'; - -export const reporterHandler = createExecutionContentHandler({ - kind: 'reporter', - defaultWorkPackageKind: 'report', - defaultArtifactKind: 'workday_summary', -}); diff --git a/src/agents/handlers/results/execution-content-completion.ts b/src/agents/handlers/results/execution-content-completion.ts deleted file mode 100644 index 561eee5d..00000000 --- a/src/agents/handlers/results/execution-content-completion.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ExecutionContentResult } from '../execution-content.ts'; - -function summary(value:unknown) { - return typeof value==='string'&&value.trim()?value.trim():null; -} - -export function executionCompletionEvidence(result: ExecutionContentResult) { - if(result.snapshot.status!=='completed')return null; - const refs=[...new Set(result.contentArtifactRefs.map((entry)=>entry.contentPath).filter(Boolean))]; - const responses=result.contentArtifactRefs.filter((entry)=>entry.model==='discussion_message'&&entry.artifactKind==='discussion_response'); - return { - disposition:'completed' as const, - acceptanceChecks:[{id:'execution-provider-completed',passed:true},...result.contentArtifactRefs.map((entry)=>({id:`artifact:${entry.model}:${entry.artifactKind}`,passed:true,evidenceRefs:[entry.contentPath]}))], - durableArtifactRefs:refs,remainingBudget:{},completionReason:summary(result.snapshot.summary)??'Execution provider completed the exact assigned output contract.', - noUsefulScopedWorkRemaining:responses.length===1, - completedScope:result.contentArtifactRefs.map((entry)=>`${entry.model}:${entry.artifactKind}`),remainingScope:[], - }; -} diff --git a/src/agents/handlers/shared.ts b/src/agents/handlers/shared.ts deleted file mode 100644 index 28e38a44..00000000 --- a/src/agents/handlers/shared.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { AgentHandlerOutput, AgentContext } from '../runtime/runtime-types.ts'; - -export interface HandlerPayload { - [key: string]: unknown; -} - -export function nowIso() { - return new Date().toISOString(); -} - -export function parseTriggerPayload(context: AgentContext): HandlerPayload { - const decisionInput = context.capacity?.decisionInput && typeof context.capacity.decisionInput === 'object' - ? context.capacity.decisionInput as unknown as Record - : {}; - const assignmentInput = decisionInput.input && typeof decisionInput.input === 'object' && !Array.isArray(decisionInput.input) - ? decisionInput.input as HandlerPayload - : {}; - const raw = context.trigger.message?.payloadJson; - if (!raw) { - return assignmentInput; - } - try { - const parsed = JSON.parse(raw) as unknown; - const triggerPayload = parsed && typeof parsed === 'object' && !Array.isArray(parsed) - ? parsed as HandlerPayload - : {}; - return { ...triggerPayload, ...assignmentInput }; - } catch { - return assignmentInput; - } -} - -export function readRecord(value: unknown): Record | null { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; -} - -export function readString(value: unknown) { - return typeof value === 'string' && value.trim() ? value.trim() : null; -} - -export async function recordArtifactMessage(input: { - context: AgentContext; - payload: HandlerPayload; - kind: string; - data: Record; -}) { - await createAgentMessage({ - context: input.context, - type: `agent.${input.kind}`, - payload: { - ...input.data, - assignmentId: input.context.capacity?.assignment?.id ?? null, - mode: input.context.capacity?.mode ?? null, - agentSlug: input.context.agent.slug, - }, - relatedModel: 'provider_assignment', - relatedId: input.context.capacity?.assignment?.id ?? null, - }); -} - -export async function createAgentMessage(input: { - context: AgentContext; - type: string; - payload: Record; - relatedModel?: string | null; - relatedId?: string | null; -}) { - try { - await input.context.sdk.createMessage({ - type: input.type, - payload: input.payload, - relatedModel: input.relatedModel ?? null, - relatedId: input.relatedId ?? null, - priority: 100, - }); - } catch { - // Message permissions are agent-spec controlled; artifact metadata remains the durable handler output. - } -} - -export function waiting(summary: string): AgentHandlerOutput { - return { - status: 'waiting', - summary, - }; -} - -export function completed(summary: string, metadata: Record): AgentHandlerOutput { - return { - status: 'completed', - summary, - metadata, - }; -} diff --git a/src/agents/handlers/writer.ts b/src/agents/handlers/writer.ts deleted file mode 100644 index 9285344b..00000000 --- a/src/agents/handlers/writer.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AgentHandler } from '../runtime/runtime-types.ts'; -import { createExecutionContentHandler } from './execution-content.ts'; - -const executionWriterHandler = createExecutionContentHandler({ - kind: 'writer', - defaultWorkPackageKind: 'writer', - defaultArtifactKind: 'agent_note', - executionAccess: 'configured', -}); - -export const writerHandler: AgentHandler = { - ...executionWriterHandler, - kind: 'writer', -}; diff --git a/src/agents/index.ts b/src/agents/index.ts deleted file mode 100644 index 242c7b2e..00000000 --- a/src/agents/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { listRegisteredAgentHandlers, resolveAgentHandler } from './support/registry.ts'; diff --git a/src/agents/kernel/agents/agent-kernel.ts b/src/agents/kernel/agents/agent-kernel.ts deleted file mode 100644 index e6a47684..00000000 --- a/src/agents/kernel/agents/agent-kernel.ts +++ /dev/null @@ -1,493 +0,0 @@ -import { createExecutionProviderAdapter } from "../../adapters/execution/execution.ts"; -import { LocalBranchMutationAdapter } from "../../adapters/tools/mutations.ts"; -import { createNotificationAdapter } from "../../adapters/accounts/notification.ts"; -import { createOperationsAdapter } from "../../adapters/operations/operations.ts"; -import { createRepositoryInspectionAdapter } from "../../adapters/repositories/repository.ts"; -import { createResearchAdapter } from "../../adapters/tools/research.ts"; -import { createVerificationAdapter } from "../../adapters/tools/verification.ts"; -import type { AgentContext,ExecutionProviderAdapter,AgentMutationAdapter,AgentNotificationAdapter,AgentOperationsAdapter,AgentRepositoryInspectionAdapter,AgentResearchAdapter,AgentTreeDxAdapter,AgentTriggerInvocation,AgentVerificationAdapter } from "../../runtime/runtime-types.ts"; -import { createAgentKernelModeFallback, type AgentKernelModeExecutionResult, type AgentModeRunStatus } from "@treeseed/sdk/agent-capacity"; -import { AgentSdk } from "@treeseed/sdk/sdk"; -import { getAgentProviderSelections } from "@treeseed/sdk/platform/deploy-runtime"; -import { resolveAgentRuntimeProviders } from "../../../agent-runtime.ts"; -import { buildAgentArtifactManifest, validateAgentArtifactManifest } from "../artifacts/artifact-manifest.ts"; -import { nowIso, record, resolveExecutionRoot } from "../runtime/runtime-helpers.ts"; -import { AgentKernelOutputValidator, waitingOutputIsTerminal } from "../validation/output-validator.ts"; -import type { AgentKernelAssignmentRunOptions, AgentKernelModeRunTelemetryInput } from "../execution/run-types.ts"; -import { preflightAssignment } from "../capacity/assignments/assignment-preflight.ts"; -import { loadAssignmentActivityContext } from "../execution/context-loader.ts"; -import { dispatchAssignmentExecution } from "../execution/execution-dispatcher.ts"; -import { isRetryableAgentExecutionFailure } from "../validation/failure-classifier.ts"; -import { recordAssignmentModeRun } from "../telemetry/telemetry.ts"; -import { boundedAssignmentResult } from "../execution/execution-result.ts"; -import { inspectAgentKernel, resolveKernelAgentExecution } from '../runtime/kernel-runtime.ts'; -export type { AgentKernelAssignmentRunOptions, AgentKernelModeRunTelemetryInput } from "../execution/run-types.ts"; -export class AgentKernel { - private readonly execution; - private readonly executionOverride; - private readonly providerSelections; - private readonly mutations; - private readonly repository; - private readonly verification; - private readonly notifications; - private readonly research; - private readonly operations; - private readonly treeDx; - private readonly outputValidator; - private readonly activeRuns = new Set(); - private readonly tenantRoot; - private readonly executionRoot; - constructor( - private readonly sdk: AgentSdk, - repoRoot: string, - options?: { - executionRoot?: string; - execution?: ExecutionProviderAdapter; - mutations?: AgentMutationAdapter; - repository?: AgentRepositoryInspectionAdapter; - verification?: AgentVerificationAdapter; - notifications?: AgentNotificationAdapter; - research?: AgentResearchAdapter; - operations?: AgentOperationsAdapter; - treeDx?: AgentTreeDxAdapter | null; - }, - ) { - this.tenantRoot = repoRoot; - this.executionRoot = - options?.executionRoot ?? resolveExecutionRoot(repoRoot); - this.providerSelections = getAgentProviderSelections(); - const runtimeProviders = resolveAgentRuntimeProviders( - this.executionRoot, - this.providerSelections, - ); - this.executionOverride = options?.execution; - this.execution = - options?.execution ?? - runtimeProviders.execution ?? - createExecutionProviderAdapter(undefined, { - repoRoot: this.executionRoot, - }); - this.mutations = - options?.mutations ?? - runtimeProviders.mutations ?? - new LocalBranchMutationAdapter(this.executionRoot); - this.repository = - options?.repository ?? - runtimeProviders.repository ?? - createRepositoryInspectionAdapter(); - this.verification = - options?.verification ?? - runtimeProviders.verification ?? - createVerificationAdapter(); - this.notifications = - options?.notifications ?? - runtimeProviders.notifications ?? - createNotificationAdapter(); - this.treeDx = options?.treeDx ?? null; - this.research = - options?.research ?? - (this.treeDx - ? createResearchAdapter(this.treeDx) - : runtimeProviders.research); - this.operations = options?.operations ?? createOperationsAdapter(); - this.outputValidator = new AgentKernelOutputValidator(); - } - async doctor() { - return inspectAgentKernel(this.sdk, this.tenantRoot); - } - - async runAssignment( - options: AgentKernelAssignmentRunOptions, - ): Promise { - const { - assignment, - mode, - capacityEnvelope, - decisionInput, - treedxProxyHandle, - fallback: preflightFallback, - } = preflightAssignment(options); - if (preflightFallback) - return boundedAssignmentResult(options, preflightFallback); - - const activityContext = await loadAssignmentActivityContext({ - sdk: this.sdk, - assignment, - decisionInput, - mode, - capacityEnvelope, - }); - if (activityContext.fallback) { - return boundedAssignmentResult( - options, - activityContext.fallback, - activityContext.fallback.retryable ? "returned" : "failed", - ); - } - const agent = activityContext.agent!; - const runtimeAgent = activityContext.runtimeAgent!; - - const trigger: AgentTriggerInvocation = { - kind: "manual", - source: `capacity-provider-assignment:${mode}`, - trigger: { type: "startup" }, - }; - const startedAt = nowIso(); - await recordAssignmentModeRun(options, { - status: "running", - selectedInput: decisionInput.input, - capacityEnvelope, - startedAt, - validation: { - mode, - projectAgentClassId: assignment.projectAgentClassId, - }, - metadata: { - source: "agent_kernel_mode_runtime", - assignmentId: assignment.id, - runnerId: options.runnerId ?? null, - }, - }); - - try { - const executed = await dispatchAssignmentExecution({ - sdk: this.sdk, - agent: runtimeAgent, - trigger, - tenantRoot: this.tenantRoot, - executionRoot: this.executionRoot, - execution: resolveKernelAgentExecution({ - agent: runtimeAgent, - executionOverride: this.executionOverride, - execution: this.execution, - providerSelections: this.providerSelections, - executionRoot: this.executionRoot, - }), - mutations: this.mutations, - repository: this.repository, - verification: this.verification, - notifications: this.notifications, - research: this.research, - operations: this.operations, - treeDx: this.treeDx, - activeRuns: this.activeRuns, - capacity: { - signal: options.signal, - assignmentId: assignment.id, - providerId: assignment.capacityProviderId, - mode, - envelope: capacityEnvelope, - decisionInput, - projectAgentClass: options.projectAgentClass ?? null, - kernelProfile: - options.kernelProfile ?? - options.projectAgentClass?.kernelProfile ?? - null, - kernelPolicy: - options.kernelPolicy ?? - options.projectAgentClass?.kernelPolicy ?? - null, - assignment, - readiness: (options.readiness ?? null) as unknown as Record< - string, - unknown - > | null, - treedxProxyHandle, - capabilityHandles: (assignment.capabilityHandles ?? - assignment.workspaceContext?.capabilityHandles ?? - null) as NonNullable["capabilityHandles"], - workspaceAccessMode: (assignment.capabilityHandles - ?.workspaceAccessMode ?? - assignment.workspaceContext?.workspaceAccessMode ?? - null) as string | null, - fallbackReason: null, - }, - onInputsResolved: async ({ runId, inputs }) => { - const inputRecord = record(inputs); - const workPackage = record(inputRecord.workPackage); - const workPackageContext = record(workPackage.context); - await recordAssignmentModeRun(options, { - status: "running", - selectedInput: decisionInput.input, - capacityEnvelope, - outputs: { - status: "inputs_resolved", - summary: `Resolved execution inputs for ${agent.slug}.`, - metadata: { - source: "agent_kernel_inputs_resolved", - agentRunId: runId, - handlerKind: runtimeAgent.handler, - resolvedInputs: inputRecord, - workPackage, - contextDiagnostics: - workPackageContext.contextDiagnostics ?? - record(workPackage.metadata).contextDiagnostics ?? - null, - assignedObjective: workPackageContext.assignedObjective ?? null, - editorialContext: workPackageContext.editorialContext ?? null, - }, - }, - traceRefs: { - agentRunId: runId, - agentSlug: agent.slug, - handlerKind: runtimeAgent.handler, - editorialContextDigest: record(workPackage.metadata).editorialContextDigest ?? null, - editorialContextSchemaVersion: record(workPackage.metadata).editorialContextSchemaVersion ?? null, - editorialContextLayers: record(workPackage.metadata).editorialContextLayers ?? [], - }, - metadata: { - source: "agent_kernel_inputs_resolved", - assignmentId: assignment.id, - runnerId: options.runnerId ?? null, - }, - }); - }, - onExecutionReturned: async ({ runId, inputs, result }) => { - const inputRecord = record(inputs); - const resultRecord = record(result); - await recordAssignmentModeRun(options, { - status: "running", - selectedInput: decisionInput.input, - capacityEnvelope, - outputs: { - status: "handler_returned", - summary: `Handler returned execution result for ${agent.slug}.`, - metadata: { - source: "agent_kernel_handler_returned", - agentRunId: runId, - handlerKind: runtimeAgent.handler, - artifactKind: inputRecord.artifactKind ?? null, - result: resultRecord, - }, - }, - traceRefs: { - agentRunId: runId, - agentSlug: agent.slug, - handlerKind: runtimeAgent.handler, - }, - metadata: { - source: "agent_kernel_handler_returned", - assignmentId: assignment.id, - runnerId: options.runnerId ?? null, - }, - }); - }, - onOutputsEmitted: async ({ runId, inputs, output }) => { - const inputRecord = record(inputs); - await recordAssignmentModeRun(options, { - status: - output.status === "completed" - ? "succeeded" - : output.status === "failed" - ? "failed" - : "running", - selectedInput: decisionInput.input, - capacityEnvelope, - outputs: { - status: "outputs_emitted", - summary: output.summary, - stdout: output.stdout ?? null, - stderr: output.stderr ?? null, - metadata: { - source: "agent_kernel_outputs_emitted", - agentRunId: runId, - handlerKind: runtimeAgent.handler, - artifactKind: inputRecord.artifactKind ?? null, - outputMetadata: output.metadata ?? {}, - }, - }, - traceRefs: { - agentRunId: runId, - agentSlug: agent.slug, - handlerKind: runtimeAgent.handler, - }, - completedAt: output.status === "completed" ? nowIso() : null, - failedAt: output.status === "failed" ? nowIso() : null, - metadata: { - source: "agent_kernel_outputs_emitted", - assignmentId: assignment.id, - runnerId: options.runnerId ?? null, - }, - }); - }, - }); - const outputValidation = this.outputValidator.validate({ - mode, - outputs: { - status: executed.output.status, - metadata: executed.output.metadata ?? {}, - }, - allowedOutputs: assignment.allowedOutputs ?? null, - }); - if (!outputValidation.ok) { - return boundedAssignmentResult( - options, - createAgentKernelModeFallback( - "assignment_output_invalid", - outputValidation.reason ?? - "Agent output is not allowed for this assignment.", - { retryable: false, metadata: outputValidation.metadata }, - ), - "failed", - ); - } - const completedAt = nowIso(); - const outputStatus = executed.output.status; - const terminalWaiting = waitingOutputIsTerminal(executed.output); - const executionSnapshot = record(record(executed.output.metadata).executionSnapshot); - const executionRetryRequested = outputStatus === "waiting" - && executionSnapshot.status === "failed" - && executionSnapshot.retryable === true; - const executionStatus: AgentKernelModeExecutionResult["status"] = - outputStatus === "completed" - ? "completed" - : outputStatus === "failed" || terminalWaiting - ? "failed" - : "returned"; - const artifactManifest = buildAgentArtifactManifest({ - assignment, - modeRunId: - options.modeRunId ?? - `${assignment.id}:${mode}:${agent.slug}:${String(runtimeAgent.handler)}`, - runnerId: options.runnerId ?? null, - agentId: agent.slug, - handlerId: String(runtimeAgent.handler), - activityType: runtimeAgent.activityType ?? mode, - status: executionStatus, - output: executed.output, - createdAt: completedAt, - }); - const artifactValidation = - validateAgentArtifactManifest(artifactManifest, { - publishedSignals: assignment.allowedOutputs?.publishedSignals, - }); - if (!artifactValidation.ok) { - const failedManifest = { - ...artifactManifest, - status: "failed" as const, - diagnostics: [ - ...artifactManifest.diagnostics, - { - code: "durable-artifact-required", - message: artifactValidation.reason, - retryable: false, - }, - ], - }; - return boundedAssignmentResult( - options, - createAgentKernelModeFallback( - "assignment_output_invalid", - artifactValidation.reason, - { - retryable: false, - metadata: { phase: "artifact_manifest_validation" }, - }, - ), - "failed", - { - artifactManifest: failedManifest, - outputs: { - status: outputStatus, - summary: executed.output.summary, - metadata: executed.output.metadata ?? {}, - }, - traceRefs: { - agentRunId: executed.runId, - agentSlug: agent.slug, - handlerKind: runtimeAgent.handler, - }, - }, - ); - } - const modeRunStatus: AgentModeRunStatus = - outputStatus === "completed" - ? "succeeded" - : outputStatus === "failed" - ? "failed" - : "cancelled"; - const fallback = - outputStatus === "waiting" - ? createAgentKernelModeFallback( - terminalWaiting - ? "assignment_waiting_for_required_context" - : executionRetryRequested - ? "assignment_execution_retry_requested" - : "assignment_waiting_for_external_completion", - executed.output.summary, - { retryable: !terminalWaiting }, - ) - : null; - await recordAssignmentModeRun(options, { - status: modeRunStatus, - selectedInput: decisionInput.input, - capacityEnvelope, - outputs: { - status: outputStatus, - summary: executed.output.summary, - stdout: executed.output.stdout ?? null, - stderr: executed.output.stderr ?? null, - metadata: executed.output.metadata ?? {}, - artifactManifest, - }, - traceRefs: { - agentRunId: executed.runId, - agentSlug: agent.slug, - handlerKind: runtimeAgent.handler, - }, - fallbackReason: fallback?.reason ?? null, - completedAt: modeRunStatus === "succeeded" ? completedAt : null, - failedAt: modeRunStatus === "failed" ? completedAt : null, - metadata: { - source: "agent_kernel_mode_runtime", - assignmentId: assignment.id, - runnerId: options.runnerId ?? null, - }, - }); - return { - status: executionStatus, - mode, - assignmentId: assignment.id, - projectId: assignment.projectId, - projectAgentClassId: assignment.projectAgentClassId, - agentId: agent.slug, - handlerId: String(runtimeAgent.handler), - summary: executed.output.summary, - outputs: { - status: outputStatus, - summary: executed.output.summary, - stdout: executed.output.stdout ?? null, - stderr: executed.output.stderr ?? null, - metadata: executed.output.metadata ?? {}, - }, - selectedInput: decisionInput.input, - capacityEnvelope, - traceRefs: { - agentRunId: executed.runId, - agentSlug: agent.slug, - handlerKind: runtimeAgent.handler, - }, - artifactManifest, - fallback, - metadata: { - source: "agent_kernel_mode_runtime", - startedAt, - completedAt, - }, - }; - } catch (error) { - const retryable = isRetryableAgentExecutionFailure(error); - return boundedAssignmentResult( - options, - createAgentKernelModeFallback( - retryable - ? "assignment_handler_transient_failure" - : "assignment_handler_failed", - error instanceof Error ? error.message : String(error), - { retryable }, - ), - retryable ? "returned" : "failed", - ); - } - } -} diff --git a/src/agents/kernel/artifacts/artifact-manifest.ts b/src/agents/kernel/artifacts/artifact-manifest.ts deleted file mode 100644 index 948dc566..00000000 --- a/src/agents/kernel/artifacts/artifact-manifest.ts +++ /dev/null @@ -1,329 +0,0 @@ -import type { - AgentArtifactManifest, - ArtifactMutationReceipt, - AgentDiagnosticReference, - AgentSignal, - AgentToolEventReference, - AgentVerificationResult, - ProviderAssignment, - ResearchCitation, -} from '@treeseed/sdk/agent-capacity'; -import type { ArtifactRef } from '@treeseed/sdk/treedx/types'; -import { assertResearchCitations, validateAgentArtifactManifest } from '@treeseed/sdk/agent-capacity'; -import type { ExecutionRunSnapshot, ExecutionUsageActual } from '@treeseed/sdk/types/agents'; -import type { AgentHandlerOutput } from '../../runtime/runtime-types.ts'; -import { artifactContentReferences, artifactToolEvents } from './artifact-receipts.ts'; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function records(value: unknown): Record[] { - return Array.isArray(value) ? value.map(record).filter((entry) => Object.keys(entry).length > 0) : []; -} - -function strings(value: unknown): string[] { - return Array.isArray(value) - ? value.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0).map((entry) => entry.trim()) - : []; -} - -function text(...values: unknown[]): string | null { - for (const value of values) { - if (typeof value === 'string' && value.trim()) return value.trim(); - } - return null; -} - -function safeUri(value: unknown): string | null { - if (typeof value !== 'string' || !value.trim()) return null; - try { - const parsed = new URL(value.trim()); - parsed.username = ''; - parsed.password = ''; - for (const key of [...parsed.searchParams.keys()]) { - if (/(?:token|secret|key|password|credential|signature)/iu.test(key)) parsed.searchParams.set(key, '[redacted]'); - } - return parsed.toString(); - } catch { - return value.trim().replace(/([?&](?:token|secret|key|password|credential|signature)=)[^&#\s]+/giu, '$1[redacted]'); - } -} - -function citationCandidates(snapshot: ExecutionRunSnapshot, outputMetadata: Record) { - const outputs = record(snapshot.outputs); - const fetched = records(outputs.toolTelemetry).flatMap((entry) => records(entry.derivedEvents)) - .filter((event) => event.type === 'research_citation_fetched') - .map((event) => record(event.citation)); - return [...records(outputs.citations), ...records(outputMetadata.citations), ...fetched]; -} - -function citations(snapshot: ExecutionRunSnapshot, outputMetadata: Record): ResearchCitation[] { - const result = citationCandidates(snapshot, outputMetadata).map((entry) => { - const sourceUrl = safeUri(text(entry.sourceUrl, entry.uri, entry.url, entry.href)); - const title = text(entry.title, entry.name); - const retrievedAt = text(entry.retrievedAt, entry.accessedAt); - const contentHash = text(entry.contentHash, entry.hash, entry.digest); - const claimIds = strings(entry.claimIds); - const confidence = text(entry.confidence); - return { - sourceUrl: sourceUrl ?? '', - title: title ?? '', - author: text(entry.author) ?? undefined, - publisher: text(entry.publisher) ?? undefined, - publishedAt: text(entry.publishedAt) ?? undefined, - retrievedAt: retrievedAt ?? '', - contentHash: contentHash ?? '', - excerpt: text(entry.excerpt) ?? undefined, - license: text(entry.license) ?? undefined, - claimIds, - confidence: confidence as ResearchCitation['confidence'], - }; - }); - const validated = assertResearchCitations(result, 'executionSnapshot.outputs.citations'); - return [...new Map(validated.map((entry) => [`${entry.sourceUrl}:${entry.contentHash}`, entry])).values()]; -} - -function verification(snapshot: ExecutionRunSnapshot, outputMetadata: Record): AgentVerificationResult[] { - const outputs = record(snapshot.outputs); - const candidates = [outputs.verification, record(snapshot.metadata).verification, outputMetadata.verification] - .flatMap((value) => Array.isArray(value) ? value : Object.keys(record(value)).length ? [value] : []); - for (const toolEvent of records(outputs.toolTelemetry)) { - if (toolEvent.status !== 'completed') continue; - for (const event of records(toolEvent.derivedEvents)) { - if (event.type === 'verification_completed') candidates.push(event); - } - } - return candidates.map((value) => { - const entry = record(value); - const rawStatus = text(entry.status, entry.result) ?? 'unknown'; - return { - status: ['completed', 'passed', 'success'].includes(rawStatus) ? 'passed' : ['failed', 'error'].includes(rawStatus) ? 'failed' : rawStatus, - summary: text(entry.summary, entry.message), - commands: strings(entry.commands), - evidenceRefs: strings(entry.evidenceRefs).map((item) => safeUri(item) ?? item), - }; - }); -} - -function diagnosticReferences(snapshot: ExecutionRunSnapshot): AgentDiagnosticReference[] { - return [ - ...(snapshot.code ? [{ code: snapshot.code, message: snapshot.summary, retryable: snapshot.retryable ?? null }] : []), - ...(snapshot.retryable === true ? [{ code: 'execution-provider-retryable', message: snapshot.summary, retryable: true }] : []), - ]; -} - -function signals(snapshot: ExecutionRunSnapshot, outputMetadata: Record, assignment: ProviderAssignment, _completed: boolean): AgentSignal[] { - const outputs = record(snapshot.outputs); - const events = records(outputs.toolTelemetry).flatMap((entry) => records(entry.derivedEvents)); - const researchStage = text(record(record(assignment.decisionInput).input).researchStage); - const toolSignals = events - .filter((entry) => entry.type === 'review_decision_recorded') - .map((entry): Record => ({ - code: researchStage?.startsWith('citation-review-') - ? (entry.disposition === 'approved' ? 'research_review_approved' : 'research_review_rejected') - : (entry.disposition === 'approved' ? 'review_approved' : 'review_rejected'), - severity: entry.disposition === 'approved' ? 'info' : 'warning', - message: entry.summary, - metadata: { source: 'treeseed.review_decision' }, - })); - const claimSignals = events.filter((entry) => entry.type === 'research_claims_recorded') - .flatMap((entry) => records(entry.claims)) - .map((claim): Record => ({ code: 'research_claim', severity: 'info', metadata: claim })); - const requestedSignals = events.filter((entry) => entry.type === 'signal_requested').map((entry) => { - const requested = record(entry.signal); - return { code: requested.contractId, severity: 'info', message: requested.message, metadata: { ...requested, payload: record(requested.payload) } }; - }); - return [...records(outputs.signals), ...records(outputMetadata.signals), ...requestedSignals, ...toolSignals, ...claimSignals].flatMap((entry) => { - const signal = record(entry); - const code = text(signal.code, signal.type); - const severity = text(signal.severity) ?? 'info'; - if (!code || !['info', 'warning', 'error'].includes(severity)) return []; - return [{ code, severity: severity as AgentSignal['severity'], message: text(signal.message), metadata: record(signal.metadata) }]; - }); -} - -function controlPlaneReferences(outputMetadata: Record) { - const estimate = record(outputMetadata.structuredEstimate); - const estimateValidation = record(outputMetadata.estimateValidation); - const estimateId = text(estimate.id); - if (estimateId && estimateValidation.ok === true) return [{ - kind: 'structured_agent_estimate', - id: estimateId, - status: 'submitted', - metadata: { - decisionId: text(estimate.decisionId), - proposalId: text(estimate.proposalId), - agentId: text(estimate.agentId), - }, - }]; - return []; -} - -function contentReferences( - assignmentId: string, - snapshot: ExecutionRunSnapshot, - toolEvents: AgentToolEventReference[], - outputMetadata: Record, -) { - const receipts = artifactContentReferences(assignmentId, snapshot, toolEvents); - const classified = records(outputMetadata.classifiedContentReferences); - return receipts.map((receipt) => { - const owned = classified.find((entry) => text(entry.contentPath, entry.path) === receipt.contentPath - && (!text(entry.model) || text(entry.model) === receipt.model)); - if (!owned) return receipt; - return { - ...receipt, - subjectId: text(owned.subjectId) ?? receipt.subjectId, - subjectField: text(owned.subjectField) ?? receipt.subjectField, - artifactKind: text(owned.artifactKind) ?? receipt.artifactKind, - producedByAgent: text(owned.producedByAgent) ?? receipt.producedByAgent, - commitSha: text(owned.commitSha) ?? receipt.commitSha, - ref: text(owned.ref) ?? receipt.ref, - groupIds: Array.isArray(owned.groupIds) ? [...new Set(owned.groupIds.map(String).filter(Boolean))] : receipt.groupIds, - }; - }); -} - -function sourceWorktree(snapshot: ExecutionRunSnapshot) { - const changedPaths = (snapshot.artifacts ?? []).filter((artifact) => artifact.kind === 'changed_path') - .map((artifact) => text(artifact.name, artifact.uri?.replace(/^repo:\/\//u, ''))).filter((path): path is string => Boolean(path)); - if (!changedPaths.length) return undefined; - const metadata = record(snapshot.metadata); - const codex = record(metadata.codex); - return { - root: text(metadata.worktreeRoot, codex.worktreeRoot), - branch: text(metadata.worktreeBranch, codex.worktreeBranch), - baseRef: text(metadata.baseRef, codex.baseRef), - changedPaths: [...new Set(changedPaths)].sort(), - }; -} - -function sourceCommit(snapshot: ExecutionRunSnapshot) { - const outputs = record(snapshot.outputs); - for (const entry of records(outputs.toolTelemetry).reverse()) { - if (entry.status !== 'completed') continue; - for (const event of records(entry.derivedEvents).reverse()) { - if (event.type !== 'source_checkpoint_committed') continue; - const sha = text(event.commitSha); - if (sha) return { sha, ref: text(event.branchRef) }; - } - } - return undefined; -} - -function mutationReceipts(input: { - assignment: ProviderAssignment; - modeRunId: string; - createdAt: string; - content: ReturnType; - source: ReturnType; - commit: ReturnType; -}): ArtifactMutationReceipt[] { - const receipts: ArtifactMutationReceipt[] = []; - const executionMode = input.assignment.executionMode === 'production' || input.assignment.metadata?.executionMode === 'production' - ? 'production' as const : 'simulation' as const; - const upstreamMutationPolicy = executionMode === 'production' ? 'checkpoint-only' as const : 'denied' as const; - const treeDx = record(input.assignment.treedxProxyHandle); - const treeDxBase = text(treeDx.baseCommitSha, treeDx.immutableRef, treeDx.baseRef); - const contentByRef = new Map(); - for (const reference of input.content) { - const effectiveRef = text(reference.commitSha, reference.ref); - if (!effectiveRef) continue; - contentByRef.set(effectiveRef, [...(contentByRef.get(effectiveRef) ?? []), reference]); - } - if (treeDxBase) for (const [effectiveRef, references] of contentByRef) { - if (effectiveRef === treeDxBase) continue; - receipts.push({ - schemaVersion: 'treeseed.artifact-mutation-receipt/v1', id: `${input.assignment.id}:mutation:treedx:${effectiveRef}`, - kind: 'treedx-content', phase: 'provisional', executionMode, upstreamMutationPolicy, - assignmentId: input.assignment.id, modeRunId: input.modeRunId, - teamId: input.assignment.teamId, projectId: input.assignment.projectId, baseRef: treeDxBase, effectiveRef, - changedPaths: [...new Set(references.map((reference) => reference.contentPath))].sort(), - before: { ref: treeDxBase, artifactRefs: [] }, - after: { ref: effectiveRef, artifactRefs: references.map((reference) => reference.receiptId).sort() }, createdAt: input.createdAt, - }); - } - const sourceBase = text(input.source?.baseRef); - if (sourceBase && input.commit?.sha && sourceBase !== input.commit.sha && input.source?.changedPaths.length) receipts.push({ - schemaVersion: 'treeseed.artifact-mutation-receipt/v1', id: `${input.assignment.id}:mutation:source:${input.commit.sha}`, - kind: 'source-checkpoint', phase: 'provisional', executionMode, upstreamMutationPolicy, - assignmentId: input.assignment.id, modeRunId: input.modeRunId, - teamId: input.assignment.teamId, projectId: input.assignment.projectId, baseRef: sourceBase, effectiveRef: input.commit.sha, - changedPaths: input.source.changedPaths, before: { ref: sourceBase, artifactRefs: [] }, - after: { ref: input.commit.sha, artifactRefs: input.source.changedPaths.map((path) => `repo://${path}`) }, createdAt: input.createdAt, - }); - return receipts; -} - -function typedArtifactReferences(snapshot: ExecutionRunSnapshot, outputMetadata: Record): ArtifactRef[] { - const candidates = [ - ...records(outputMetadata.artifactReferences), - ...(snapshot.artifacts ?? []).flatMap((artifact) => { - const metadata = record(artifact.metadata); - return [record(metadata.artifactRef), ...records(metadata.artifactReferences), ...records(record(metadata.changeset).artifacts)]; - }), - ]; - return candidates.flatMap((candidate) => { - if (candidate.contract !== 'treeseed.artifact-ref/v1') return []; - const kind = text(candidate.kind); - const visibility = text(candidate.visibility); - const sha256 = candidate.sha256 === null ? null : text(candidate.sha256); - if (!kind || !visibility || sha256 !== null && !/^[0-9a-f]{64}$/u.test(sha256)) return []; - return [{ ...candidate, contract: 'treeseed.artifact-ref/v1', kind, visibility, sha256 } as ArtifactRef]; - }); -} - -export function buildAgentArtifactManifest(input: { - assignment: ProviderAssignment; - modeRunId: string; - runnerId?: string | null; - agentId: string; - handlerId: string; - activityType: string; - status: 'completed' | 'returned' | 'failed'; - output: AgentHandlerOutput; - createdAt?: string; -}): AgentArtifactManifest { - const outputMetadata = record(input.output.metadata); - const snapshot = record(outputMetadata.executionSnapshot) as unknown as ExecutionRunSnapshot; - const toolEvents = artifactToolEvents(input.assignment.id, snapshot); - const artifactReferences = contentReferences(input.assignment.id, snapshot, toolEvents, outputMetadata); - const commitReference = sourceCommit(snapshot); - const sourceReference = sourceWorktree(snapshot); - const createdAt = input.createdAt ?? new Date().toISOString(); - return { - schemaVersion: 1, - assignmentId: input.assignment.id, - modeRunId: input.modeRunId, - teamId: input.assignment.teamId, - projectId: input.assignment.projectId, - workDayId: input.assignment.workDayId ?? null, - providerId: input.assignment.capacityProviderId, - runnerId: input.runnerId ?? input.assignment.runnerId ?? null, - executionProviderId: input.assignment.executionProviderId ?? null, - mode: input.assignment.mode === 'acting' ? 'acting' : 'planning', - agentClassId: input.assignment.projectAgentClassId, - agentId: input.agentId, - handlerId: input.handlerId, - activityType: input.activityType, - status: input.status, - summary: input.output.summary, - toolEvents, - contentReferences: artifactReferences, - mutationReceipts: mutationReceipts({ assignment: input.assignment, modeRunId: input.modeRunId, createdAt, - content: artifactReferences, source: sourceReference, commit: commitReference }), - artifactReferences: typedArtifactReferences(snapshot, outputMetadata), - sourceWorktree: sourceReference, - commit: commitReference, - verification: verification(snapshot, outputMetadata), - citations: citations(snapshot, outputMetadata), - signals: signals(snapshot, outputMetadata, input.assignment,input.status === 'completed'), - controlPlaneReferences: controlPlaneReferences(outputMetadata), - usage: Array.isArray(snapshot.usage) ? snapshot.usage as ExecutionUsageActual[] : [], - diagnostics: diagnosticReferences(snapshot), - createdAt, - }; -} - -export { validateAgentArtifactManifest }; diff --git a/src/agents/kernel/artifacts/artifact-receipts.ts b/src/agents/kernel/artifacts/artifact-receipts.ts deleted file mode 100644 index 7bcd55d2..00000000 --- a/src/agents/kernel/artifacts/artifact-receipts.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { - AgentContentReference, - AgentToolEventReference, -} from '@treeseed/sdk/agent-capacity'; -import type { ExecutionRunSnapshot } from '@treeseed/sdk/types/agents'; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function records(value: unknown): Record[] { - return Array.isArray(value) ? value.map(record).filter((entry) => Object.keys(entry).length > 0) : []; -} - -function text(...values: unknown[]): string | null { - for (const value of values) { - if (typeof value === 'string' && value.trim()) return value.trim(); - } - return null; -} - -function telemetry(snapshot: ExecutionRunSnapshot) { - const outputs = record(snapshot.outputs); - const entries = records(outputs.toolTelemetry).length - ? records(outputs.toolTelemetry) - : records(record(snapshot.metadata).toolTelemetry); - return entries.filter((entry) => entry.status === 'completed' || entry.status === 'failed'); -} - -function eventId(assignmentId: string, entry: Record, index: number) { - return `${assignmentId}:tool:${index + 1}:${text(entry.toolId, entry.tool, entry.operation) ?? 'unknown'}`; -} - -export function artifactToolEvents(assignmentId: string, snapshot: ExecutionRunSnapshot): AgentToolEventReference[] { - return telemetry(snapshot).map((entry, index) => ({ - id: eventId(assignmentId, entry, index), - toolId: text(entry.toolId, entry.tool, entry.name) ?? 'unknown', - status: entry.status === 'failed' ? 'failed' : 'completed', - operation: text(record(entry.operation).name, entry.operation), - startedAt: text(entry.startedAt), - completedAt: text(entry.completedAt), - durationMs: Number.isFinite(Number(entry.durationMs)) ? Number(entry.durationMs) : null, - derivedEventTypes: records(entry.derivedEvents).map((event) => text(event.type)).filter((value): value is string => Boolean(value)), - capturedInputRef: text(entry.capturedInputRef), - capturedOutputRef: text(entry.capturedOutputRef), - })); -} - -export function artifactContentReferences( - assignmentId: string, - snapshot: ExecutionRunSnapshot, - toolEvents: AgentToolEventReference[], -): AgentContentReference[] { - const completedTelemetry = telemetry(snapshot).filter((entry) => entry.status === 'completed'); - const references = (snapshot.artifacts ?? []).flatMap((artifact, receiptIndex) => { - if (artifact.kind !== 'treedx_content_receipt') return []; - const metadata = record(artifact.metadata); - const receiptTelemetry = record(metadata.telemetry); - const toolId = text(metadata.toolId, receiptTelemetry.toolId); - const telemetryIndex = completedTelemetry.findIndex((entry) => entry === receiptTelemetry - || (text(entry.toolId) === toolId && records(entry.derivedEvents).some((event) => event.type === 'content_created'))); - if (!toolId || telemetryIndex < 0) return []; - const ref = record(metadata.contentRef); - const model = text(ref.model); - const contentPath = text(ref.contentPath, ref.path); - if (!model || !contentPath) return []; - const matchingEventIndex = telemetry(snapshot).findIndex((entry) => entry === completedTelemetry[telemetryIndex]); - const toolEventId = toolEvents[matchingEventIndex]?.id; - if (!toolEventId) return []; - return [{ - model, - contentPath, - receiptId: `${assignmentId}:content:${receiptIndex + 1}`, - toolEventId, - subjectId: text(ref.subjectId), - subjectField: text(ref.subjectField), - artifactKind: text(ref.artifactKind), - producedByAgent: text(ref.producedByAgent), - commitSha: text(ref.commitSha), - ref: text(ref.ref, ref.branchRef), - }]; - }); - return [...new Map(references.map((reference) => [`${reference.model}:${reference.contentPath}`, reference])).values()]; -} diff --git a/src/agents/kernel/capacity/assignments/assignment-preflight.ts b/src/agents/kernel/capacity/assignments/assignment-preflight.ts deleted file mode 100644 index 04dc2b47..00000000 --- a/src/agents/kernel/capacity/assignments/assignment-preflight.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - createAgentKernelModeFallback, - deriveAgentCapacityEnvelopeFromAssignment, - deriveDecisionExecutionInputFromAssignment, - normalizeAgentExecutionMode, - validateAgentKernelModeExecutionInput, - type AgentKernelModeFallback, -} from '@treeseed/sdk/agent-capacity'; -import type { AgentKernelAssignmentRunOptions } from '../../execution/run-types.ts'; -import { assignmentTreeDxProxyHandle } from '../../runtime/runtime-helpers.ts'; - -function actingReadinessAllowsExecution(readiness: Record | null | undefined) { - if (!readiness) return false; - return (readiness.executionReadiness === 'ready' || readiness.executionReadiness === 'waived') - && (readiness.planningInputsStatus === 'complete' || readiness.planningInputsStatus === 'waived'); -} - -export function preflightAssignment(options: AgentKernelAssignmentRunOptions) { - const assignment = options.assignment; - const mode = normalizeAgentExecutionMode(assignment.mode); - const capacityEnvelope = options.capacityEnvelope ?? deriveAgentCapacityEnvelopeFromAssignment(assignment); - const decisionInput = options.decisionInput ?? deriveDecisionExecutionInputFromAssignment(assignment); - const treedxProxyHandle = assignmentTreeDxProxyHandle(assignment, options.treedxProxyHandle); - let fallback: AgentKernelModeFallback | null = validateAgentKernelModeExecutionInput({ - ...options, - capacityEnvelope, - decisionInput, - readiness: options.readiness ?? null, - treedxProxyHandle: treedxProxyHandle as Parameters[0]['treedxProxyHandle'], - }); - if (!fallback && mode === 'acting' && options.readiness - && !actingReadinessAllowsExecution(options.readiness as unknown as Record)) { - fallback = createAgentKernelModeFallback( - 'assignment_decision_not_ready', - `Assignment ${assignment.id} is not ready for acting execution.`, - { retryable: true, metadata: { readiness: options.readiness } }, - ); - } - return { - assignment, - mode, - capacityEnvelope, - decisionInput, - treedxProxyHandle, - fallback, - }; -} diff --git a/src/agents/kernel/execution/context-loader.ts b/src/agents/kernel/execution/context-loader.ts deleted file mode 100644 index 20f51b78..00000000 --- a/src/agents/kernel/execution/context-loader.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - createAgentKernelModeFallback, - type AgentKernelModeFallback, - type AgentCapacityEnvelope, - type DecisionExecutionInput, - type ProviderAssignment, -} from '@treeseed/sdk/agent-capacity'; -import type { AgentSdk } from '@treeseed/sdk/sdk'; -import { AGENT_ACTIVITY_TYPES, type AgentActivityType, type AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; -import { loadActiveAgentSpecs } from '../../support/spec-loader.ts'; -import { selectAgentActivityProfile } from '../telemetry/activity-profile-resolver.ts'; -import { AGENT_SPEC_LOAD_TIMEOUT_MS, record, stringValue, withTimeout } from '../runtime/runtime-helpers.ts'; - -interface AssignmentActivityContext { - agent: AgentRuntimeSpec | null; - runtimeAgent: AgentRuntimeSpec | null; - fallback: AgentKernelModeFallback | null; -} - -export function assignmentProcessTimeoutSeconds(productiveTimeoutSeconds:number,envelope:AgentCapacityEnvelope){ - const reservedSeconds=Number(envelope.reservedSeconds); const budget=record(envelope.budget); const time=record(budget.time); - const productive=Number.isFinite(reservedSeconds)&&reservedSeconds>0?Math.min(productiveTimeoutSeconds,Math.floor(reservedSeconds)):productiveTimeoutSeconds; - const preparation=Number(time.preparationSeconds); const closeout=Number(time.closeoutSeconds); - return productive+(Number.isInteger(preparation)&&preparation>0?preparation:0)+(Number.isInteger(closeout)&&closeout>0?closeout:0); -} - -function failed(code: string, reason: string, metadata?: Record): AssignmentActivityContext { - return { - agent: null, - runtimeAgent: null, - fallback: createAgentKernelModeFallback(code, reason, { retryable: false, metadata }), - }; -} - -export async function loadAssignmentActivityContext(input: { - sdk: AgentSdk; - assignment: ProviderAssignment; - decisionInput: DecisionExecutionInput; - mode: 'planning' | 'acting'; - capacityEnvelope: AgentCapacityEnvelope; -}): Promise { - let loaded: Awaited>; - try { - loaded = await withTimeout({ - promise: loadActiveAgentSpecs(input.sdk), - timeoutMs: AGENT_SPEC_LOAD_TIMEOUT_MS, - code: 'assignment_agent_spec_load_timeout', - message: `Active agent spec loading exceeded ${AGENT_SPEC_LOAD_TIMEOUT_MS}ms.`, - }); - } catch (error) { - return { - agent: null, - runtimeAgent: null, - fallback: createAgentKernelModeFallback( - (error as Error & { code?: string }).code ?? 'assignment_agent_spec_load_failed', - error instanceof Error ? error.message : String(error), - { retryable: true, metadata: { phase: 'load_active_agent_specs', timeoutMs: AGENT_SPEC_LOAD_TIMEOUT_MS } }, - ), - }; - } - const errors = loaded.diagnostics.filter((entry) => entry.severity === 'error'); - if (errors.length) { - return failed( - 'assignment_agent_not_found', - `Agent spec validation failed: ${errors.map((entry) => `${entry.slug}:${entry.field}:${entry.message}`).join(' | ')}`, - ); - } - const agentSlug = input.decisionInput.agentId ?? input.assignment.agentId; - const agent = agentSlug ? loaded.specs.find((entry) => entry.slug === agentSlug) ?? null : null; - if (!agent) { - return failed( - 'assignment_agent_not_found', - `Agent ${agentSlug ?? ''} is not enabled or was not found in project ${input.assignment.projectId}.`, - ); - } - const requestedActivity = stringValue(record(input.decisionInput.input).activityType, ''); - const requestedActivityType = AGENT_ACTIVITY_TYPES.includes(requestedActivity as AgentActivityType) - ? requestedActivity as AgentActivityType - : null; - let runtimeAgent = selectAgentActivityProfile(agent, input.mode, requestedActivityType); - if (!runtimeAgent) { - return failed( - 'assignment_activity_profile_unavailable', - `Agent ${agent.slug} does not define an enabled ${requestedActivityType ?? input.mode} activity profile allowed under ${input.mode} capacity.`, - { agentSlug: agent.slug, mode: input.mode, requestedActivityType }, - ); - } - if (input.decisionInput.handlerId && input.decisionInput.handlerId !== runtimeAgent.handler) { - return failed( - 'assignment_handler_profile_mismatch', - `Assignment handler ${input.decisionInput.handlerId} does not match ${agent.slug}'s configured ${input.mode} handler ${runtimeAgent.handler}.`, - { - agentSlug: agent.slug, - mode: input.mode, - assignedHandler: input.decisionInput.handlerId, - configuredHandler: runtimeAgent.handler, - }, - ); - } - const reservedSeconds = Number(input.capacityEnvelope.reservedSeconds); - if (Number.isFinite(reservedSeconds) && reservedSeconds > 0) { - runtimeAgent = { - ...runtimeAgent, - execution: { - ...runtimeAgent.execution, - timeoutSeconds: assignmentProcessTimeoutSeconds(runtimeAgent.execution.timeoutSeconds,input.capacityEnvelope), - }, - }; - } - return { agent, runtimeAgent, fallback: null }; -} diff --git a/src/agents/kernel/execution/execution-dispatcher.ts b/src/agents/kernel/execution/execution-dispatcher.ts deleted file mode 100644 index a7388efe..00000000 --- a/src/agents/kernel/execution/execution-dispatcher.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { AgentSdk } from '@treeseed/sdk/sdk'; -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; -import { randomUUID } from 'node:crypto'; -import { resolveAgentHandler } from '../../support/registry.ts'; -import type { - AgentContext, - AgentHandlerOutput, - AgentMutationAdapter, - AgentNotificationAdapter, - AgentOperationsAdapter, - AgentRepositoryInspectionAdapter, - AgentResearchAdapter, - AgentTreeDxAdapter, - AgentTriggerInvocation, - AgentVerificationAdapter, - ExecutionProviderAdapter, -} from '../../runtime/runtime-types.ts'; -import type { AgentRunTrace } from '../../contracts/run.ts'; -import { classifyAgentExecutionFailure } from '../validation/failure-classifier.ts'; -import { nowIso, record } from '../runtime/runtime-helpers.ts'; - -interface ExecutionDispatcherInput { - sdk: AgentSdk; - agent: AgentRuntimeSpec; - trigger: AgentTriggerInvocation; - tenantRoot: string; - executionRoot: string; - execution: ExecutionProviderAdapter; - mutations: AgentMutationAdapter; - repository: AgentRepositoryInspectionAdapter; - verification: AgentVerificationAdapter; - notifications: AgentNotificationAdapter; - research: AgentResearchAdapter; - operations: AgentOperationsAdapter; - treeDx: AgentTreeDxAdapter | null; - activeRuns: Set; - capacity?: AgentContext['capacity']; - onInputsResolved?: (event: { runId: string; inputs: unknown; context: AgentContext }) => Promise | void; - onExecutionReturned?: (event: { runId: string; inputs: unknown; result: unknown; context: AgentContext }) => Promise | void; - onOutputsEmitted?: (event: { runId: string; inputs: unknown; result: unknown; output: AgentHandlerOutput; context: AgentContext }) => Promise | void; -} - -function trace(input: { - agent: AgentRuntimeSpec; - runId: string; - trigger: AgentTriggerInvocation; - overrides?: Partial; -}): AgentRunTrace { - return { - runId: input.runId, - agentSlug: input.agent.slug, - handlerKind: input.agent.handler, - triggerKind: input.trigger.kind, - triggerSource: input.trigger.source, - claimedMessageId: null, - selectedItemKey: null, - branchName: null, - commitSha: null, - changedPaths: [], - summary: null, - error: null, - errorCategory: null, - startedAt: nowIso(), - finishedAt: null, - status: 'running', - ...(input.overrides ?? {}), - }; -} - -export async function dispatchAssignmentExecution( - input: ExecutionDispatcherInput, -): Promise<{ runId: string; output: AgentHandlerOutput }> { - if (input.activeRuns.has(input.agent.slug)) { - return { - runId: '', - output: { status: 'waiting', summary: `Agent ${input.agent.slug} is already running.` }, - }; - } - input.activeRuns.add(input.agent.slug); - const runId = randomUUID(); - const handler = await resolveAgentHandler(input.agent.handler, { tenantRoot: input.tenantRoot }); - const scopedSdk = input.sdk.scopeForAgent(input.agent); - const context: AgentContext = { - runId, - repoRoot: input.executionRoot, - agent: input.agent, - capacity: input.capacity, - sdk: scopedSdk, - trigger: input.trigger, - execution: input.execution, - mutations: input.mutations, - repository: input.repository, - verification: input.verification, - notifications: input.notifications, - research: input.research, - operations: input.operations, - treeDx: input.treeDx, - }; - await input.sdk.recordRun({ run: trace({ agent: input.agent, runId, trigger: input.trigger }) }); - try { - const inputs = await handler.resolveInputs(context); - const resolvedInputs = record(inputs); - await input.onInputsResolved?.({ runId, inputs, context }); - const result = await handler.execute(context, inputs); - await input.onExecutionReturned?.({ runId, inputs, result, context }); - const output = await handler.emitOutputs(context, result); - await input.onOutputsEmitted?.({ runId, inputs, result, output, context }); - await input.sdk.recordRun({ - run: trace({ - agent: input.agent, - runId, - trigger: input.trigger, - overrides: { - status: output.status, - branchName: (output.metadata?.branchName as string | undefined) ?? null, - commitSha: (output.metadata?.commitSha as string | undefined) ?? null, - changedPaths: (output.metadata?.changedPaths as string[] | undefined) ?? [], - summary: output.summary, - error: output.status === 'failed' ? output.stderr ?? output.summary : null, - errorCategory: output.status === 'failed' ? output.errorCategory ?? 'execution_error' : null, - finishedAt: nowIso(), - }, - }), - }); - return { runId, output }; - } catch (error) { - await input.sdk.recordRun({ - run: trace({ - agent: input.agent, - runId, - trigger: input.trigger, - overrides: { - status: 'failed', - error: error instanceof Error ? error.message : String(error), - errorCategory: classifyAgentExecutionFailure(error), - finishedAt: nowIso(), - }, - }), - }); - throw error; - } finally { - input.activeRuns.delete(input.agent.slug); - } -} diff --git a/src/agents/kernel/execution/execution-result.ts b/src/agents/kernel/execution/execution-result.ts deleted file mode 100644 index 4ff9f358..00000000 --- a/src/agents/kernel/execution/execution-result.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - deriveAgentCapacityEnvelopeFromAssignment, - deriveDecisionExecutionInputFromAssignment, - normalizeAgentExecutionMode, - type AgentKernelModeExecutionResult, - type AgentKernelModeFallback, -} from '@treeseed/sdk/agent-capacity'; -import { AgentKernelFallbackController } from '../validation/fallback-controller.ts'; -import type { AgentKernelAssignmentRunOptions } from './run-types.ts'; -import { recordAssignmentModeRun } from '../telemetry/telemetry.ts'; -import { nowIso } from '../runtime/runtime-helpers.ts'; - -const fallbackController = new AgentKernelFallbackController(); - -export async function boundedAssignmentResult( - options: AgentKernelAssignmentRunOptions, - fallback: AgentKernelModeFallback, - status: AgentKernelModeExecutionResult['status'] = fallback.retryable ? 'returned' : 'failed', - evidence?: Pick, -): Promise { - const assignment = options.assignment; - const mode = normalizeAgentExecutionMode(assignment.mode); - const capacityEnvelope = options.capacityEnvelope ?? deriveAgentCapacityEnvelopeFromAssignment(assignment); - const decisionInput = options.decisionInput ?? deriveDecisionExecutionInputFromAssignment(assignment); - const timestamp = nowIso(); - await recordAssignmentModeRun(options, { - status: status === 'failed' ? 'failed' : 'cancelled', - selectedInput: decisionInput.input, - capacityEnvelope, - outputs: { - ...(evidence?.outputs ?? {}), - status, - summary: fallback.reason, - artifactManifest: evidence?.artifactManifest ?? null, - }, - validation: { code: fallback.code, retryable: fallback.retryable, ...(fallback.metadata ?? {}) }, - fallbackReason: fallback.reason, - failedAt: status === 'failed' ? timestamp : null, - completedAt: status !== 'failed' ? timestamp : null, - metadata: { - recordKind: 'mode-run', - source: 'agent_kernel_mode_runtime', - assignmentId: assignment.id, - runnerId: options.runnerId ?? null, - }, - }); - if (options.recordFallbackOutput) { - await options.recordFallbackOutput(fallbackController.buildOutput({ - assignmentId: assignment.id, - projectId: assignment.projectId, - mode, - fallback, - metadata: { - status, - runnerId: options.runnerId ?? null, - attemptCount: assignment.attemptCount ?? 0, - }, - })); - } - return { - status, - mode, - assignmentId: assignment.id, - projectId: assignment.projectId, - projectAgentClassId: assignment.projectAgentClassId, - agentId: decisionInput.agentId ?? assignment.agentId ?? null, - handlerId: decisionInput.handlerId ?? assignment.handlerId ?? null, - summary: fallback.reason, - outputs: { ...(evidence?.outputs ?? {}), status, summary: fallback.reason }, - selectedInput: decisionInput.input, - capacityEnvelope, - traceRefs: evidence?.traceRefs ?? {}, - artifactManifest: evidence?.artifactManifest ?? null, - fallback, - metadata: { recordKind: 'mode-run', source: 'agent_kernel_mode_runtime' }, - }; -} diff --git a/src/agents/kernel/execution/run-types.ts b/src/agents/kernel/execution/run-types.ts deleted file mode 100644 index a8a4793d..00000000 --- a/src/agents/kernel/execution/run-types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { AgentCapacityEnvelope, AgentKernelModeExecutionInput, AgentModeRunStatus } from '@treeseed/sdk/agent-capacity'; - -export interface AgentKernelModeRunTelemetryInput { - id?: string; - status: AgentModeRunStatus; - selectedInput: Record; - capacityEnvelope: AgentCapacityEnvelope; - outputs?: Record; - traceRefs?: Record; - usageActual?: Record | null; - validation?: Record; - fallbackReason?: string | null; - startedAt?: string | null; - completedAt?: string | null; - failedAt?: string | null; - metadata?: Record; -} - -export interface AgentKernelAssignmentRunOptions extends AgentKernelModeExecutionInput { - signal?: AbortSignal; - recordModeRun?: (run: AgentKernelModeRunTelemetryInput) => Promise; - recordFallbackOutput?: (output: Record) => Promise; -} diff --git a/src/agents/kernel/runtime/kernel-runtime.ts b/src/agents/kernel/runtime/kernel-runtime.ts deleted file mode 100644 index 1c124bb5..00000000 --- a/src/agents/kernel/runtime/kernel-runtime.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; -import { getAgentProviderSelections } from '@treeseed/sdk/platform/deploy-runtime'; -import type { AgentSdk } from '@treeseed/sdk/sdk'; -import type { ExecutionProviderAdapter } from '../../runtime/runtime-types.ts'; -import { resolveAgentRuntimeProviders } from '../../../agent-runtime.ts'; -import { resolveAgentHandler } from '../../support/registry.ts'; -import { loadAllAgentSpecs, summarizeAgentSpec } from '../../support/spec-loader.ts'; - -export function resolveKernelAgentExecution(input: { - agent: AgentRuntimeSpec; - executionOverride?: ExecutionProviderAdapter; - execution: ExecutionProviderAdapter; - providerSelections: ReturnType; - executionRoot: string; -}) { - if (input.executionOverride) return input.executionOverride; - const provider = input.agent.execution.provider ?? input.providerSelections.execution; - if (provider === input.providerSelections.execution) return input.execution; - return resolveAgentRuntimeProviders(input.executionRoot, { ...input.providerSelections, execution: provider }).execution; -} - -export async function inspectAgentKernel(sdk: AgentSdk, tenantRoot: string) { - const { specs, diagnostics } = await loadAllAgentSpecs(sdk); - for (const agent of specs.filter((entry) => entry.enabled)) await resolveAgentHandler(agent.handler, { tenantRoot }); - const errors = diagnostics.filter((entry) => entry.severity === 'error'); - if (errors.length) throw new Error(`Agent spec validation failed: ${errors.map((entry) => `${entry.slug}:${entry.field}:${entry.message}`).join(' | ')}`); - return { agents: specs.map(summarizeAgentSpec), diagnostics }; -} diff --git a/src/agents/kernel/runtime/runtime-helpers.ts b/src/agents/kernel/runtime/runtime-helpers.ts deleted file mode 100644 index 15ad92f8..00000000 --- a/src/agents/kernel/runtime/runtime-helpers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { ProviderAssignment } from '@treeseed/sdk/agent-capacity'; -import { loadDeployConfigFromPath } from '@treeseed/sdk/platform/deploy-config'; -import { existsSync } from 'node:fs'; -import { resolve } from 'node:path'; - -export const AGENT_SPEC_LOAD_TIMEOUT_MS = 60_000; - -export function nowIso() { - return new Date().toISOString(); -} - -export function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export function stringValue(...values: unknown[]) { - for (const value of values) { - if (typeof value === 'string' && value.trim()) return value; - } - return null; -} - -export function assignmentTreeDxProxyHandle(assignment: ProviderAssignment, explicit?: unknown) { - const direct = record(explicit); - if (Object.keys(direct).length > 0) return direct; - const root = record(assignment.treedxProxyHandle); - if (Object.keys(root).length > 0) return root; - const workspace = record(assignment.workspaceContext); - const workspaceHandle = record(workspace.treedxProxyHandle); - return Object.keys(workspaceHandle).length > 0 ? workspaceHandle : null; -} - -export async function withTimeout(input: { promise: Promise; timeoutMs: number; code: string; message: string }): Promise { - let timer: ReturnType | null = null; - try { - return await Promise.race([ - input.promise, - new Promise((_, reject) => { - timer = setTimeout(() => { - const error = new Error(input.message); - (error as Error & { code?: string }).code = input.code; - reject(error); - }, input.timeoutMs); - }), - ]); - } finally { - if (timer) clearTimeout(timer); - } -} - -export function resolveExecutionRoot(tenantRoot: string) { - const configPath = resolve(tenantRoot, 'treeseed.site.yaml'); - if (!existsSync(configPath)) return tenantRoot; - const deployConfig = loadDeployConfigFromPath(configPath) as { __projectRoot?: string }; - return deployConfig.__projectRoot ?? tenantRoot; -} diff --git a/src/agents/kernel/telemetry/activity-profile-resolver.ts b/src/agents/kernel/telemetry/activity-profile-resolver.ts deleted file mode 100644 index b8c1502d..00000000 --- a/src/agents/kernel/telemetry/activity-profile-resolver.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { - AgentActivityProfile, - AgentActivityType, - AgentRuntimeSpec, -} from '@treeseed/sdk/types/agents'; -import { allowedSafetyModesForActivity,compileAgentAuthoritySnapshot } from '@treeseed/sdk/agent-capacity'; -import { permissionProjectionForProfile } from '../../support/spec-normalizer-policy.ts'; - -function executionForProfile(agent: AgentRuntimeSpec, profile: AgentActivityProfile) { - return { - ...agent.execution, - ...profile.execution, - timeoutSeconds: profile.execution?.maxRuntimeSeconds ?? agent.execution.timeoutSeconds, - retryLimit: profile.execution?.maxRetries ?? agent.execution.retryLimit, - allowedPaths: profile.execution?.allowedPaths ?? agent.execution.allowedPaths, - forbiddenPaths: profile.execution?.forbiddenPaths ?? agent.execution.forbiddenPaths, - }; -} - -const RESOURCE_STEWARDSHIP_PROMPT = `Resource stewardship: -- Your allocation is a maximum, not a target to consume. -- Finish as soon as every acceptance criterion is satisfied and no important in-scope work remains. -- Do not invent work, broaden scope, prolong discussion, or consume tokens merely because capacity remains. -- Before completing early, verify the result, persist every required durable artifact, and report why further work would have low value. -- Use blocked rather than completed_early when evidence, authority, credentials, or dependencies are missing. -- Near the deadline, narrow scope, checkpoint valid work, and return an honest partial or blocked result. -- A completed_early result must include acceptanceChecks, durableArtifactRefs, remainingBudget, completionReason, and noUsefulScopedWorkRemaining=true.`; - -function systemPromptForProfile(profile: AgentActivityProfile) { - const publications = profile.signals?.publishes ?? []; - const signalPrompt = publications.length - ? `\n\nSignal handoff: after creating the required durable content or control-plane evidence, call treeseed.publish_signal once for each applicable declared signal (${publications.join(', ')}). Use the durable subject identity, explain the change for the next agent, and include typed routing fields such as objective and proposalTypes. Do not claim completion until the signal request has succeeded.` - : ''; - return `${profile.prompt.system}\n\n${RESOURCE_STEWARDSHIP_PROMPT}${signalPrompt}`; -} - -export function agentActivityTypeForAssignmentMode(mode: 'planning' | 'acting'): AgentActivityType { - return mode; -} - -export function selectAgentActivityProfile( - agent: AgentRuntimeSpec, - mode: 'planning' | 'acting', - requestedActivityType?: AgentActivityType | null, -): AgentRuntimeSpec | null { - const activityType = requestedActivityType ?? agentActivityTypeForAssignmentMode(mode); - if (!allowedSafetyModesForActivity(activityType).includes(mode)) return null; - const profile = agent.activityProfiles?.[activityType]; - if (!profile?.enabled) return null; - const authority = compileAgentAuthoritySnapshot(activityType, profile); - if (authority.diagnostics.length > 0) return null; - return { - ...agent, - handler: profile.handler, - activityType, - branchPolicy: profile.branchPolicy, - questionPolicy: profile.questionPolicy, - systemPrompt: systemPromptForProfile(profile), - tools: authority.tools, - authorityPresetIds: authority.presetIds, - authoritySnapshot: { - presetIds: authority.presetIds, - permissions: authority.permissions, - tools: authority.tools, - branchPolicy: profile.branchPolicy, - }, - signalPolicy: profile.signals, - outputs: profile.outputs, - permissionProjection: permissionProjectionForProfile(authority.permissions), - execution: executionForProfile(agent, profile), - activityConfig: { - ...agent.activityConfig, - workPackageKind: profile.handler, - domain: activityType, - metadata: { - ...agent.activityConfig?.metadata, - activityType, - promptTask: profile.prompt.task, - }, - }, - }; -} diff --git a/src/agents/kernel/telemetry/telemetry.ts b/src/agents/kernel/telemetry/telemetry.ts deleted file mode 100644 index 4c4ca61e..00000000 --- a/src/agents/kernel/telemetry/telemetry.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { deriveDecisionExecutionInputFromAssignment } from '@treeseed/sdk/agent-capacity'; -import { randomUUID } from 'node:crypto'; -import type { AgentKernelAssignmentRunOptions, AgentKernelModeRunTelemetryInput } from '../execution/run-types.ts'; - -function transientTelemetryFailure(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error); - return /fetch failed|timed out|econnreset|econnrefused|socket|temporarily unavailable|network error/iu.test(message); -} - -async function waitBeforeRetry(attempt: number): Promise { - await new Promise((resolve) => setTimeout(resolve, attempt * 250)); -} - -export async function recordAssignmentModeRun( - options: AgentKernelAssignmentRunOptions, - run: AgentKernelModeRunTelemetryInput, -) { - if (!options.recordModeRun) return null; - const assignment = options.assignment; - const decisionInput = options.decisionInput ?? deriveDecisionExecutionInputFromAssignment(assignment); - const handlerId = decisionInput.handlerId ?? assignment.handlerId ?? 'handler'; - const agentId = decisionInput.agentId ?? assignment.agentId ?? 'agent'; - const mode = run.capacityEnvelope?.mode ?? assignment.mode ?? decisionInput.mode ?? 'planning'; - const source = typeof run.metadata?.source === 'string' && run.metadata.source.trim() - ? run.metadata.source.trim() - : 'agent_kernel_mode_runtime'; - const request = { - id: run.id ?? options.modeRunId ?? `${assignment.id}:${mode}:${agentId}:${handlerId}:${source}:${randomUUID()}`, - ...run, - }; - let lastError: unknown = new Error('Mode-run telemetry was not attempted.'); - for (let attempt = 1; attempt <= 3; attempt += 1) { - try { - return await options.recordModeRun(request); - } catch (error) { - lastError = error; - if (!transientTelemetryFailure(error) || attempt === 3) break; - await waitBeforeRetry(attempt); - } - } - throw lastError; -} diff --git a/src/agents/kernel/validation/failure-classifier.ts b/src/agents/kernel/validation/failure-classifier.ts deleted file mode 100644 index 2dc0f4ae..00000000 --- a/src/agents/kernel/validation/failure-classifier.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { AgentErrorCategory } from '../../contracts/run.ts'; - -export function classifyAgentExecutionFailure(error: unknown): AgentErrorCategory { - const message = error instanceof Error ? error.message : String(error); - if (message.includes('not allowed')) return 'permission_error'; - if (message.includes('message')) return 'message_claim_error'; - if (message.includes('lease')) return 'lease_error'; - if (message.includes('commit') || message.includes('worktree') || message.includes('artifact')) { - return 'mutation_error'; - } - if (message.includes('Copilot') || message.includes('execution')) return 'execution_error'; - return 'sdk_error'; -} - -export function isRetryableAgentExecutionFailure(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error); - return /fetch failed|timed out|econnreset|econnrefused|socket|temporarily unavailable|network error/iu.test(message); -} diff --git a/src/agents/kernel/validation/fallback-controller.ts b/src/agents/kernel/validation/fallback-controller.ts deleted file mode 100644 index 427bf891..00000000 --- a/src/agents/kernel/validation/fallback-controller.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { AgentKernelModeFallback } from '@treeseed/sdk/agent-capacity'; - -function idPart(value: string): string { - return value.trim().toLowerCase().replace(/[^a-z0-9_.:-]+/gu, '-').replace(/^-+|-+$/gu, '') || 'unknown'; -} - -export class AgentKernelFallbackController { - buildOutput(input: { assignmentId: string; mode: string; fallback: AgentKernelModeFallback; projectId: string; metadata?: Record }) { - const attemptCount = Number(input.metadata?.attemptCount ?? 0); - return { - id: `fallback:${idPart(input.assignmentId)}:${idPart(input.mode)}:${idPart(input.fallback.code)}:attempt-${Number.isFinite(attemptCount) ? Math.max(0, Math.floor(attemptCount)) : 0}`, - assignmentId: input.assignmentId, - projectId: input.projectId, - mode: input.mode, - code: input.fallback.code, - status: input.fallback.retryable ? 'draft' : 'suppressed', - output: { summary: input.fallback.reason, type: input.mode === 'planning' ? 'planning_documentation_draft' : 'weakness_proposal_draft' }, - provenance: { source: 'agent_kernel_fallback', assignmentId: input.assignmentId }, - quota: input.fallback.metadata?.quota ? { quota: input.fallback.metadata.quota } : {}, - metadata: input.metadata ?? {}, - }; - } -} diff --git a/src/agents/kernel/validation/output-validator.ts b/src/agents/kernel/validation/output-validator.ts deleted file mode 100644 index 3e52eeba..00000000 --- a/src/agents/kernel/validation/output-validator.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { AgentHandlerOutput } from '../../runtime/runtime-types.ts'; -import { record } from '../runtime/runtime-helpers.ts'; - -export function waitingOutputIsTerminal(output: AgentHandlerOutput) { - if (output.status !== 'waiting') return false; - const metadata = record(output.metadata); - const executionSnapshot = record(metadata.executionSnapshot); - return record(executionSnapshot.outputs).executionBlocked === true && executionSnapshot.retryable !== true; -} - -export class AgentKernelOutputValidator { - validate(input: { mode: string; outputs?: Record | null; allowedOutputs?: Record | null }) { - const allowed = record(input.allowedOutputs); - const outputs = record(input.outputs); - const allowedStatuses = Array.isArray(allowed.statuses) ? allowed.statuses.map(String) : []; - const status = typeof outputs.status === 'string' ? outputs.status : null; - if (allowedStatuses.length && (!status || !allowedStatuses.includes(status))) { - return { ok: false, reason: `Output status ${status ?? ''} is not allowed for ${input.mode}.`, metadata: { status, allowedStatuses } }; - } - const allowedTypes = Array.isArray(allowed.types) ? allowed.types.map(String) : []; - const metadata = record(outputs.metadata); - const completion = record(metadata.completion); - if (completion.disposition === 'completed_early') { - const acceptanceChecks = Array.isArray(completion.acceptanceChecks) ? completion.acceptanceChecks : []; - const durableArtifactRefs = Array.isArray(completion.durableArtifactRefs) ? completion.durableArtifactRefs : []; - if (completion.noUsefulScopedWorkRemaining !== true || !String(completion.completionReason ?? '').trim() || !acceptanceChecks.length || !completion.remainingBudget || !durableArtifactRefs.length) { - return { ok: false, reason: 'completed_early requires acceptance checks, durable artifacts, remaining budget, a reason, and noUsefulScopedWorkRemaining=true.', metadata: { completion } }; - } - if (acceptanceChecks.some((entry) => record(entry).passed !== true)) { - return { ok: false, reason: 'completed_early cannot be used while an acceptance check is unmet.', metadata: { completion } }; - } - } - if (!Object.keys(allowed).length) return { ok: true }; - const outputType = typeof metadata.type === 'string' ? metadata.type : typeof metadata.kind === 'string' ? metadata.kind : null; - if (allowedTypes.length && (!outputType || !allowedTypes.includes(outputType))) { - return { ok: false, reason: `Output type ${outputType ?? ''} is not allowed for ${input.mode}.`, metadata: { outputType, allowedTypes } }; - } - return { ok: true }; - } -} diff --git a/src/agents/runtime/runtime-types.ts b/src/agents/runtime/runtime-types.ts deleted file mode 100644 index 5f731db3..00000000 --- a/src/agents/runtime/runtime-types.ts +++ /dev/null @@ -1,264 +0,0 @@ -import type { - AgentExpectedOutput, - AgentHandlerKind, - AgentRuntimeSpec, - AgentRunStatus, - AgentWorkPackage, - ExecutionArtifactRef, - ExecutionPreparationResult, - ExecutionProviderDescriptor, - ExecutionProviderObservation, - ExecutionProviderObserveInput, - ExecutionRunRef, - ExecutionRunSnapshot, - ExecutionUsageActual, - ExecutionWorkspaceContext, - AgentTriggerConfig, -} from '@treeseed/sdk/types/agents'; -export type { ExecutionPreparationResult } from '@treeseed/sdk/types/agents'; -import type { AgentErrorCategory } from '../contracts/run.ts'; -import type { ScopedAgentSdk } from '@treeseed/sdk/sdk'; -import type { SdkMessageEntity } from '@treeseed/sdk/types'; -import type { - AgentOperationGrant, - AgentOperationRequest, - AgentOperationResult, -} from '@treeseed/sdk/operations/agent-tools'; -import type { - AgentCapacityEnvelope, - ProviderAssignmentCapabilityHandles, - AgentAssignmentWorkspaceAccessMode, - AgentExecutionMode, - AgentKernelPolicy, - AgentKernelProfile, - DecisionExecutionInput, - ProviderAssignment, - ProjectAgentClass, -} from '@treeseed/sdk/agent-capacity'; - -export interface AgentTriggerInvocation { - kind: 'startup' | 'schedule' | 'message' | 'manual' | 'follow'; - source: string; - trigger: AgentTriggerConfig; - message?: SdkMessageEntity | null; - followModels?: string[]; - cursorValue?: string | null; -} - -export interface AgentHandlerOutput { - status: AgentRunStatus; - summary: string; - stdout?: string; - stderr?: string; - errorCategory?: AgentErrorCategory | null; - metadata?: Record; -} - -export interface AgentMutationResult { - branchName: string | null; - commitMessage: string | null; - worktreePath: string | null; - commitSha: string | null; - changedPaths: string[]; -} - -export interface AgentRepositoryInspectionResult { - branchName: string | null; - changedPaths: string[]; - commitSha: string | null; - summary: string; -} - -export interface AgentVerificationResult { - status: 'completed' | 'failed' | 'waiting'; - summary: string; - stdout?: string; - stderr?: string; - errorCategory?: AgentErrorCategory | null; -} - -export interface AgentNotificationResult { - status: 'completed' | 'failed' | 'waiting'; - summary: string; - deliveredCount: number; -} - -export interface AgentResearchResult { - status: 'completed' | 'failed' | 'waiting'; - summary: string; - markdown: string; - sources?: string[]; - errorCategory?: AgentErrorCategory | null; -} - -export interface ExecutionProviderInvocation { - signal?: AbortSignal; - assignment: ProviderAssignment; - capacityEnvelope: AgentCapacityEnvelope; - decisionInput: DecisionExecutionInput; - agent: AgentRuntimeSpec; - workPackage: AgentWorkPackage; - leaseToken: string | null; - runnerId: string; - projectAgentClass?: ProjectAgentClass | null; - workspace?: ExecutionWorkspaceContext | null; - tools?: ExecutionProviderToolDescriptor[]; - metadata?: Record; -} - -export type ExecutionProviderToolKind = 'agent_tool'; - -export interface ExecutionProviderToolDescriptor { - kind: ExecutionProviderToolKind; - id: string; - name: string; - description: string; - inputSchema: Record; - outputSchema?: Record; - executionTarget: 'sdk_dispatch' | 'treedx_proxy' | 'treeseed_content' | 'provider_runner'; - mutability: 'read' | 'content_write' | 'worktree_write' | 'shared_state_write'; - metadata?: Record; -} - -export interface TreeDxProxyExecutionToolDescriptor extends ExecutionProviderToolDescriptor { - kind: 'agent_tool'; - executionTarget: 'treedx_proxy'; - projectId: string; - assignmentId: string; - handleId: string; - repositoryId?: string | null; - workspaceId?: string | null; - allowedOperations: string[]; - allowedPaths: string[]; - allowedReadPaths?: string[]; - allowedWritePaths?: string[]; - routes: { - buildContext: string; - readRepositoryFiles: string; - searchWorkspace: string; - readWorkspaceFile: string; - applyWorkspaceChangeset: string; - commitWorkspace: string; - }; -} - -export interface ExecutionProviderAdapter { - describe(): ExecutionProviderDescriptor | Promise; - observe(input: ExecutionProviderObserveInput): Promise; - prepare?(input: ExecutionProviderInvocation): Promise; - start(input: ExecutionProviderInvocation): Promise; - poll?(input: ExecutionRunRef): Promise; - resume?(input: ExecutionRunRef): Promise; - cancel?(input: ExecutionRunRef & { reason: string }): Promise; - collectUsage?(input: ExecutionRunRef): Promise; - collectArtifacts?(input: ExecutionRunRef): Promise; - releaseAssignmentResources?(input: { assignmentId: string; outcome: 'completed' | 'returned' | 'failed' | 'expired' | 'cancelled' }): Promise; -} - -export interface AgentMutationAdapter { - writeArtifact(input: { - runId: string; - agent: AgentRuntimeSpec; - relativePath: string; - content: string; - commitMessage: string; - }): Promise; -} - -export interface AgentRepositoryInspectionAdapter { - inspectBranch(input: { - repoRoot: string; - branchName: string | null; - }): Promise; -} - -export interface AgentVerificationAdapter { - runChecks(input: { - agent: AgentRuntimeSpec; - runId: string; - commands: string[]; - cwd?: string; - }): Promise; -} - -export interface AgentNotificationAdapter { - deliver(input: { - agent: AgentRuntimeSpec; - runId: string; - recipients: string[]; - subject: string; - body: string; - }): Promise; -} - -export interface AgentResearchAdapter { - research(input: { - agent: AgentRuntimeSpec; - runId: string; - questionId: string; - reason: string | null; - }): Promise; -} - -export interface AgentOperationsAdapter { - runOperation(input: { - request: AgentOperationRequest; - grants: AgentOperationGrant[]; - sdk?: ScopedAgentSdk; - }): Promise; -} - -export interface AgentTreeDxAdapter { - buildContext(input: { repoId: string; query?: string | null; paths?: string[]; body?: Record }): Promise>; - listRepositoryPaths(input: { repoId: string; path: string; ref?: string | null; body?: Record }): Promise>; - readRepositoryFiles(input: { repoId: string; paths: string[]; ref?: string | null; body?: Record }): Promise>; - searchWorkspace(input: { workspaceId: string; query: string; body?: Record }): Promise>; - readWorkspaceFile(input: { workspaceId: string; path: string }): Promise>; - applyWorkspaceChangeset(input: { workspaceId: string; body: Record }): Promise>; - commitWorkspace(input: { workspaceId: string; message: string; body?: Record }): Promise>; - closeWorkspace(input: { workspaceId: string }): Promise>; -} - -export interface AgentContext { - runId: string; - repoRoot: string; - agent: AgentRuntimeSpec; - capacity?: { - assignmentId: string; - providerId: string; - mode: AgentExecutionMode; - envelope: AgentCapacityEnvelope; - decisionInput: DecisionExecutionInput; - projectAgentClass?: ProjectAgentClass | null; - kernelProfile?: AgentKernelProfile | null; - kernelPolicy?: AgentKernelPolicy | null; - assignment?: ProviderAssignment; - readiness?: Record | null; - treedxProxyHandle?: Record | null; - capabilityHandles?: ProviderAssignmentCapabilityHandles | Record | null; - workspaceAccessMode?: AgentAssignmentWorkspaceAccessMode | string | null; - fallbackReason?: string | null; - signal?: AbortSignal; - }; - sdk: ScopedAgentSdk; - trigger: AgentTriggerInvocation; - execution: ExecutionProviderAdapter; - mutations: AgentMutationAdapter; - repository: AgentRepositoryInspectionAdapter; - verification: AgentVerificationAdapter; - notifications: AgentNotificationAdapter; - research: AgentResearchAdapter; - operations: AgentOperationsAdapter; - treeDx?: AgentTreeDxAdapter | null; -} - -export interface AgentHandler { - kind: AgentHandlerKind; - resolveInputs(context: AgentContext): Promise; - execute(context: AgentContext, inputs: TInputs): Promise; - emitOutputs(context: AgentContext, result: TResult): Promise; -} - -export type { AgentExpectedOutput, AgentWorkPackage }; - -export {}; diff --git a/src/agents/support/cli.ts b/src/agents/support/cli.ts deleted file mode 100644 index 04fd1a55..00000000 --- a/src/agents/support/cli.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { realpathSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -export type AgentCliCommandName = 'doctor'; - -export type AgentCliCommandSpec = { - name: AgentCliCommandName; - usage: string; - summary: string; -}; - -export type AgentCliContext = { - cwd?: string; - env?: NodeJS.ProcessEnv; - write?: (output: string, stream?: 'stdout' | 'stderr') => void; - outputFormat?: 'human' | 'json'; -}; - -const AGENT_COMMAND_SPECS: AgentCliCommandSpec[] = [ - { name: 'doctor', usage: 'doctor', summary: 'Inspect agent runtime readiness for the current tenant.' }, -] as const; - -function parseArgs(argv: string[]) { - const [command = 'doctor', ...rest] = argv; - return { - command, - args: rest, - }; -} - -export function listAgentCommands() { - return [...AGENT_COMMAND_SPECS]; -} - -export function renderAgentHelp() { - return [ - 'treeseed agents ', - '', - 'Commands:', - ...AGENT_COMMAND_SPECS.map((command) => ` ${command.usage.padEnd(24)}${command.summary}`), - ].join('\n'); -} - -function defaultWrite(output: string, stream: 'stdout' | 'stderr' = 'stdout') { - if (!output) return; - (stream === 'stderr' ? process.stderr : process.stdout).write(`${output}\n`); -} - -function resolveExecutablePath(path: string) { - try { - return realpathSync(path); - } catch { - return resolve(path); - } -} - -export async function runAgentCli(argv: string[], context: AgentCliContext = {}) { - const { command } = parseArgs(argv); - const write = context.write ?? defaultWrite; - if (command === '--help' || command === '-h' || command === 'help') { - write(renderAgentHelp(), 'stdout'); - return 0; - } - - const [{ AgentKernel }, { AgentSdk }] = await Promise.all([ - import('../kernel/agents/agent-kernel.ts'), - import('@treeseed/sdk/sdk'), - ]); - - const repoRoot = context.cwd ?? process.cwd(); - const env = { ...process.env, ...(context.env ?? {}) }; - const sdk = AgentSdk.createLocal({ - repoRoot, - databaseName: env.TREESEED_AGENT_D1_DATABASE ?? 'docs-site-data', - persistTo: env.TREESEED_AGENT_D1_PERSIST_TO ?? undefined, - }); - const kernel = new AgentKernel(sdk, repoRoot); - - const emitPayload = async (payload: Promise | unknown) => { - write(JSON.stringify(await payload, null, 2), 'stdout'); - return 0; - }; - - if (command === 'doctor') { - return emitPayload({ ok: true, command, ...(await kernel.doctor()) }); - } - throw new Error(`Unknown Treeseed command "${command}".`); -} - -const currentFile = resolveExecutablePath(fileURLToPath(import.meta.url)); -const entryFile = resolveExecutablePath(process.argv[1] ?? ''); - -if (entryFile === currentFile) { - runAgentCli(process.argv.slice(2)).catch((error) => { - defaultWrite( - JSON.stringify( - { - ok: false, - error: error instanceof Error ? error.message : String(error), - }, - null, - 2, - ), - 'stderr', - ); - process.exit(1); - }); -} diff --git a/src/agents/support/registry-helper.ts b/src/agents/support/registry-helper.ts deleted file mode 100644 index f1877410..00000000 --- a/src/agents/support/registry-helper.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { AgentHandlerKind } from '@treeseed/sdk/types/agents'; -import type { AgentHandler } from '../runtime/runtime-types.ts'; - -export function defineAgentHandlerRegistry( - registry: Partial>, -) { - return registry; -} - -export function resolveAgentHandlerFromRegistry( - registry: Partial>, - kind: AgentHandlerKind, -) { - const handler = registry[kind]; - if (!handler) { - throw new Error(`No runtime handler is registered for agent handler "${kind}".`); - } - return handler; -} diff --git a/src/agents/support/registry.ts b/src/agents/support/registry.ts deleted file mode 100644 index 36c7b987..00000000 --- a/src/agents/support/registry.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { mkdtempSync, rmSync } from 'node:fs'; -import { existsSync, mkdirSync, readdirSync } from 'node:fs'; -import { dirname, extname, resolve } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { build } from 'esbuild'; -import type { AgentHandlerKind } from '@treeseed/sdk/types/agents'; -import { getAgentProviderSelections } from '@treeseed/sdk/platform/deploy-runtime'; -import { resolveTenantRoot } from '@treeseed/sdk/platform/tenant-config'; -import type { AgentHandler } from '../runtime/runtime-types.ts'; -import { resolveAgentRuntimeProviders } from '../../agent-runtime.ts'; -import { actorHandler } from '../handlers/actor.ts'; -import { estimateHandler } from '../handlers/estimate.ts'; -import { releaserHandler } from '../handlers/releaser.ts'; -import { reporterHandler } from '../handlers/reporter.ts'; -import { writerHandler } from '../handlers/writer.ts'; - -const BUILTIN_HANDLER_KINDS = [ - 'writer', - 'actor', - 'estimate', - 'releaser', - 'reporter', -] as const; - -const HANDLER_EXPORT_NAMES: Record<(typeof BUILTIN_HANDLER_KINDS)[number], string> = { - writer: 'writerHandler', - actor: 'actorHandler', - estimate: 'estimateHandler', - releaser: 'releaserHandler', - reporter: 'reporterHandler', -}; - -const BUILTIN_HANDLERS: Record<(typeof BUILTIN_HANDLER_KINDS)[number], AgentHandler> = { - writer: writerHandler, - actor: actorHandler, - estimate: estimateHandler, - releaser: releaserHandler, - reporter: reporterHandler, -}; - -const SELF_CONTRACT_IMPORTS: Record = { - '@treeseed/agent/contracts/messages': 'contracts/messages', - '@treeseed/agent/contracts/run': 'contracts/run', -}; - -function normalizeHandlerKind(kind: AgentHandlerKind): AgentHandlerKind { - return kind; -} - -function handlerExportName(kind: string) { - if ((BUILTIN_HANDLER_KINDS as readonly string[]).includes(kind)) { - return HANDLER_EXPORT_NAMES[kind as (typeof BUILTIN_HANDLER_KINDS)[number]]; - } - const identifier = kind - .split(/[^a-zA-Z0-9]+/u) - .filter(Boolean) - .map((part, index) => { - const lower = part.charAt(0).toLowerCase() + part.slice(1); - return index === 0 ? lower : `${lower.charAt(0).toUpperCase()}${lower.slice(1)}`; - }) - .join(''); - return `${identifier || 'agent'}Handler`; -} - -export function getTenantAgentHandlerModulePaths( - kind: AgentHandlerKind, - tenantRoot = resolveTenantRoot(), -) { - return [ - resolve(tenantRoot, 'src/agent-handlers', `${kind}.ts`), - resolve(tenantRoot, 'src/agent-handlers', `${kind}.js`), - ]; -} - -function listTenantAgentHandlerKinds(tenantRoot: string) { - const agentsRoot = resolve(tenantRoot, 'src/agent-handlers'); - if (!existsSync(agentsRoot)) { - return [...BUILTIN_HANDLER_KINDS]; - } - const localKinds = readdirSync(agentsRoot, { withFileTypes: true }) - .filter((entry) => entry.isFile()) - .map((entry) => entry.name.match(/^(.+)\.(?:js|ts)$/u)?.[1] ?? null) - .filter((entry): entry is string => Boolean(entry && !entry.startsWith('_') && !entry.endsWith('.d'))); - return [...new Set([...BUILTIN_HANDLER_KINDS, ...localKinds])]; -} - -function findNearestTsconfig(startPath: string) { - let current = dirname(startPath); - while (true) { - const candidate = resolve(current, 'tsconfig.json'); - if (existsSync(candidate)) { - return candidate; - } - const parent = resolve(current, '..'); - if (parent === current) { - return null; - } - current = parent; - } -} - -function resolveSelfContractImport(importPath: string) { - const relativeContractPath = SELF_CONTRACT_IMPORTS[importPath]; - if (!relativeContractPath) { - return null; - } - - const moduleDir = dirname(fileURLToPath(import.meta.url)); - const candidates = [ - resolve(moduleDir, `${relativeContractPath}.ts`), - resolve(moduleDir, `${relativeContractPath}.js`), - resolve(moduleDir, '..', 'src', 'agents', `${relativeContractPath}.ts`), - resolve(moduleDir, '..', 'dist', 'agents', `${relativeContractPath}.js`), - ]; - return candidates.find((candidate) => existsSync(candidate)) ?? null; -} - -async function importTenantAgentHandlerModule(modulePath: string) { - if (extname(modulePath) !== '.ts') { - return await import(/* @vite-ignore */ pathToFileURL(modulePath).href) as Record; - } - - const outputParent = resolve(process.cwd(), '.treeseed'); - mkdirSync(outputParent, { recursive: true }); - const outputRoot = mkdtempSync(resolve(outputParent, 'agent-handler-')); - const outputFile = resolve(outputRoot, `${Date.now()}-${Math.random().toString(36).slice(2)}.js`); - const tsconfig = findNearestTsconfig(modulePath); - try { - await build({ - entryPoints: [modulePath], - outfile: outputFile, - bundle: true, - format: 'esm', - platform: 'node', - packages: 'external', - logLevel: 'silent', - tsconfig: tsconfig ?? undefined, - plugins: [{ - name: 'treeseed-agent-self-contracts', - setup(buildContext) { - buildContext.onResolve({ filter: /^@treeseed\/agent\/contracts\/(messages|run)$/ }, (args) => { - const path = resolveSelfContractImport(args.path); - if (!path) { - return null; - } - return { path }; - }); - }, - }], - }); - return await import(/* @vite-ignore */ pathToFileURL(outputFile).href) as Record; - } finally { - rmSync(outputRoot, { recursive: true, force: true }); - } -} - -export async function loadTenantAgentHandlerRegistry( - tenantRoot = resolveTenantRoot(), -): Promise> { - const registry: Record = {}; - - for (const kind of listTenantAgentHandlerKinds(tenantRoot)) { - const modulePath = getTenantAgentHandlerModulePaths(kind, tenantRoot).find((candidate) => existsSync(candidate)); - if (!modulePath) { - if ((BUILTIN_HANDLER_KINDS as readonly string[]).includes(kind)) { - registry[kind] = BUILTIN_HANDLERS[kind as (typeof BUILTIN_HANDLER_KINDS)[number]]; - } - continue; - } - - let moduleExports: Record; - try { - moduleExports = await importTenantAgentHandlerModule(modulePath); - } catch (error) { - const reason = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to import tenant agent handler "${kind}" from ${modulePath}: ${reason}`); - } - - const exportName = handlerExportName(kind); - const handler = moduleExports[exportName] ?? moduleExports.handler ?? moduleExports.default; - if (!handler) { - throw new Error( - `Tenant agent handler module "${modulePath}" must export "${exportName}", "handler", or a default handler for handler kind "${kind}".`, - ); - } - - const normalizedHandler = handler as AgentHandler; - if (normalizedHandler.kind !== kind) { - throw new Error( - `Tenant agent handler "${exportName}" from "${modulePath}" declares kind "${normalizedHandler.kind}", but "${kind}" was expected.`, - ); - } - - registry[kind] = normalizedHandler; - } - - return registry; -} - -const agentHandlerRegistryPromises = new Map>>(); - -async function getAgentHandlerRegistry(tenantRoot = resolveTenantRoot()) { - if (!agentHandlerRegistryPromises.has(tenantRoot)) { - agentHandlerRegistryPromises.set(tenantRoot, loadTenantAgentHandlerRegistry(tenantRoot)); - } - return agentHandlerRegistryPromises.get(tenantRoot)!; -} - -export async function listRegisteredAgentHandlers(options: { tenantRoot?: string } = {}) { - const tenantRoot = options.tenantRoot ?? resolveTenantRoot(); - const registry = await getAgentHandlerRegistry(tenantRoot); - const runtimeProviders = resolveAgentRuntimeProviders(tenantRoot, getAgentProviderSelections()); - return [...new Set([ - ...Object.keys(registry), - ...runtimeProviders.handlers.keys(), - ])]; -} - -export async function resolveAgentHandler(kind: AgentHandlerKind, options: { tenantRoot?: string } = {}) { - const normalizedKind = normalizeHandlerKind(kind); - const tenantRoot = options.tenantRoot ?? resolveTenantRoot(); - const registry = await getAgentHandlerRegistry(tenantRoot); - const runtimeProviders = resolveAgentRuntimeProviders(tenantRoot, getAgentProviderSelections()); - const handler = registry[normalizedKind] ?? runtimeProviders.handlers.get(normalizedKind); - if (!handler) { - if ((BUILTIN_HANDLER_KINDS as readonly string[]).includes(normalizedKind)) { - const expectedPath = getTenantAgentHandlerModulePaths(normalizedKind, tenantRoot).join('" or "'); - const expectedExport = HANDLER_EXPORT_NAMES[normalizedKind as (typeof BUILTIN_HANDLER_KINDS)[number]]; - throw new Error( - `No runtime handler is registered for agent handler "${normalizedKind}". Expected tenant file "${expectedPath}" exporting "${expectedExport}" or a plugin contribution.`, - ); - } - throw new Error(`No runtime handler is registered for agent handler "${normalizedKind}".`); - } - - return handler; -} diff --git a/src/agents/support/spec-loader.ts b/src/agents/support/spec-loader.ts deleted file mode 100644 index ff2bf373..00000000 --- a/src/agents/support/spec-loader.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; -import { AgentSdk } from '@treeseed/sdk/sdk'; -import { AGENT_MESSAGE_TYPES } from '../contracts/messages.ts'; -import { listRegisteredAgentHandlers } from './registry.ts'; -import { normalizeAgentRuntimeSpec } from './spec-normalizer.ts'; -import type { - AgentSpecDiagnostic, - NormalizedAgentRuntimeSpec, - RawAgentRuntimeSpec, -} from './spec-types.ts'; - -export interface AgentSpecLoadResult { - specs: NormalizedAgentRuntimeSpec[]; - diagnostics: AgentSpecDiagnostic[]; -} - -function extractRawSpec(entry: Record): RawAgentRuntimeSpec { - const frontmatter = - entry.frontmatter && typeof entry.frontmatter === 'object' - ? (entry.frontmatter as Record) - : {}; - return { - ...frontmatter, - body: entry.body, - id: entry.id, - }; -} - -export async function loadAgentSpecs( - sdk: AgentSdk, - options?: { enabled?: boolean }, -): Promise { - const entries = - typeof (sdk as AgentSdk & { listRawAgentSpecs?: unknown }).listRawAgentSpecs === 'function' - ? await sdk.listRawAgentSpecs(options) - : ((await (sdk as AgentSdk & { listAgentSpecs(options?: { enabled?: boolean }): Promise }).listAgentSpecs(options)).map( - (spec) => ({ - id: spec.slug, - body: '', - frontmatter: spec, - }), - ) as Record[]); - const diagnostics: AgentSpecDiagnostic[] = []; - const specs: NormalizedAgentRuntimeSpec[] = []; - const tenantRoot = typeof sdk.repoRoot === 'string' ? sdk.repoRoot : undefined; - const registeredHandlers = await listRegisteredAgentHandlers({ tenantRoot }); - - for (const entry of entries as Record[]) { - const result = normalizeAgentRuntimeSpec(extractRawSpec(entry), { - registeredHandlers: registeredHandlers as NormalizedAgentRuntimeSpec['handler'][], - messageTypes: [...AGENT_MESSAGE_TYPES], - }); - diagnostics.push(...result.diagnostics); - if (result.spec) { - specs.push(result.spec); - } - } - - return { specs, diagnostics }; -} - -export async function loadActiveAgentSpecs(sdk: AgentSdk) { - return loadAgentSpecs(sdk, { enabled: true }); -} - -export async function loadAllAgentSpecs(sdk: AgentSdk) { - return loadAgentSpecs(sdk); -} - -export function summarizeAgentSpec(agent: AgentRuntimeSpec) { - return { - slug: agent.slug, - handler: agent.handler, - enabled: agent.enabled, - triggers: agent.triggers.map((trigger) => trigger.type), - }; -} diff --git a/src/agents/support/spec-normalizer-activities.ts b/src/agents/support/spec-normalizer-activities.ts deleted file mode 100644 index 2f27be93..00000000 --- a/src/agents/support/spec-normalizer-activities.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { AgentActivityProfile, AgentActivityType, AgentBranchPolicy, AgentChatProfileConfiguration, AgentDefinitionIdentity, AgentHandlerKind, AgentOutputContract, AgentQuestionPolicy } from '@treeseed/sdk/types/agents'; -import { compileDefaultChatActivityProfile,validateAgentActivityProfilesConfiguration } from '@treeseed/sdk/agent-capacity'; -import type { AgentSpecDiagnostic } from './spec-types.ts'; -import { normalizeActivityPermissions, normalizeToolPolicy } from './spec-normalizer-policy.ts'; -import { ACTIVITY_TYPES, GENERIC_HANDLER_KINDS, ensureBoolean, ensureString, isPlainObject } from './spec-normalizer-primitives.ts'; - -function normalizeOutputs( - value: unknown, - _diagnostics: AgentSpecDiagnostic[], - _slug: string, -): AgentOutputContract { - const next = isPlainObject(value) ? value : {}; - return { - messageTypes: Array.isArray(next.messageTypes) ? next.messageTypes.filter((entry): entry is string => typeof entry === 'string') : [], - modelMutations: Array.isArray(next.modelMutations) ? next.modelMutations.filter((entry): entry is string => typeof entry === 'string') : [], - }; -} - -function normalizeBranchPolicy(value: unknown, field: string, diagnostics: AgentSpecDiagnostic[], slug: string): AgentBranchPolicy { - if (!isPlainObject(value)) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be an object.`, - }); - return { kind: 'read-only', base: 'main' }; - } - const kind = typeof value.kind === 'string' ? value.kind : 'read-only'; - switch (kind) { - case 'read-only': - return { kind, base: value.base === 'staging' ? 'staging' : 'main' }; - case 'main-planning-content': - return { kind, base: 'main' }; - case 'staging-content': - return { kind, base: 'staging' }; - case 'assignment-feature': - return { - kind, - base: 'staging', - target: 'staging', - prefix: typeof value.prefix === 'string' ? value.prefix : undefined, - branchNameTemplate: typeof value.branchNameTemplate === 'string' ? value.branchNameTemplate : 'agent/{agentSlug}/{assignmentId}', - worktree: value.worktree === 'reuse' ? 'reuse' : 'new', - updateBaseBeforeRun: value.updateBaseBeforeRun !== false, - mergeTargetBeforeSave: value.mergeTargetBeforeSave !== false, - }; - case 'staging-release': - return { kind, base: 'staging', target: 'main' }; - default: - diagnostics.push({ - severity: 'error', - slug, - field: `${field}.kind`, - message: `Unsupported branch policy "${kind}".`, - }); - return { kind: 'read-only', base: 'main' }; - } -} - -function normalizeQuestionPolicy(value: unknown, field: string, diagnostics: AgentSpecDiagnostic[], slug: string): AgentQuestionPolicy | undefined { - if (value === undefined || value === null) return undefined; - if (!isPlainObject(value)) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be an object.`, - }); - return undefined; - } - return value as AgentQuestionPolicy; -} - -export function normalizeIdentity(value: unknown, diagnostics: AgentSpecDiagnostic[], slug: string): AgentDefinitionIdentity | undefined { - if (value === undefined) return undefined; - if (!isPlainObject(value)) { - diagnostics.push({ severity: 'error', slug, field: 'identity', message: 'Expected identity to be an object.' }); - return undefined; - } - return { - purpose: typeof value.purpose === 'string' ? value.purpose : '', - responsibilities: Array.isArray(value.responsibilities) ? value.responsibilities.filter((entry): entry is string => typeof entry === 'string') : [], - durableInstructions: typeof value.durableInstructions === 'string' ? value.durableInstructions : '', - }; -} - -function normalizeActivityProfile( - value: unknown, - activityType: AgentActivityType, - diagnostics: AgentSpecDiagnostic[], - slug: string, -): AgentActivityProfile | null { - const field = `activityProfiles.${activityType}`; - if (!isPlainObject(value)) { - diagnostics.push({ severity: 'error', slug, field, message: `Expected ${field} to be an object.` }); - return null; - } - const enabled = ensureBoolean(value.enabled, `${field}.enabled`, diagnostics, slug, true); - const handler = ensureString(value.handler, `${field}.handler`, diagnostics, slug) as AgentHandlerKind; - if (!GENERIC_HANDLER_KINDS.has(handler)) { - diagnostics.push({ - severity: 'error', - slug, - field: `${field}.handler`, - message: `Unsupported first-party handler "${handler}". Use writer, actor, estimate, releaser, or reporter.`, - }); - } - const prompt = isPlainObject(value.prompt) ? value.prompt : {}; - if (!isPlainObject(value.prompt)) { - diagnostics.push({ severity: 'error', slug, field: `${field}.prompt`, message: `Expected ${field}.prompt to be an object.` }); - } - const execution = isPlainObject(value.execution) ? value.execution : {}; - return { - enabled, - handler: handler as AgentActivityProfile['handler'], - prompt: { - system: ensureString(prompt.system, `${field}.prompt.system`, diagnostics, slug), - task: typeof prompt.task === 'string' ? prompt.task : undefined, - templates: isPlainObject(prompt.templates) ? prompt.templates as Record : undefined, - }, - branchPolicy: normalizeBranchPolicy(value.branchPolicy, `${field}.branchPolicy`, diagnostics, slug), - permissions: normalizeActivityPermissions(value.permissions, diagnostics, slug, `${field}.permissions`), - authorityPresets: Array.isArray(value.authorityPresets) ? value.authorityPresets.map(String) as AgentActivityProfile['authorityPresets'] : undefined, - tools: normalizeToolPolicy(value.tools, `${field}.tools`, enabled, diagnostics, slug), - signals: isPlainObject(value.signals) ? value.signals as AgentActivityProfile['signals'] : undefined, - outputs: normalizeOutputs(value.outputs, diagnostics, slug), - planningIntent: isPlainObject(value.planningIntent) ? value.planningIntent as AgentActivityProfile['planningIntent'] : undefined, - questionPolicy: normalizeQuestionPolicy(value.questionPolicy, `${field}.questionPolicy`, diagnostics, slug), - execution: { - ...execution, - closeoutWarningSeconds: Number.isInteger(execution.closeoutWarningSeconds) && Number(execution.closeoutWarningSeconds) > 0 ? Number(execution.closeoutWarningSeconds) : 180, - maxTotalTokens: Number.isInteger(execution.maxTotalTokens) && Number(execution.maxTotalTokens) > 0 ? Number(execution.maxTotalTokens) : 136_000, - warningTokens: Number.isInteger(execution.warningTokens) && Number(execution.warningTokens) > 0 ? Number(execution.warningTokens) : 100_000, - enforcementConfidence: ['exact', 'bounded', 'estimated', 'opaque'].includes(String(execution.enforcementConfidence)) ? execution.enforcementConfidence : 'bounded', - } as AgentActivityProfile['execution'], - }; -} - - -function normalizeChatSpecialization(value: unknown, diagnostics: AgentSpecDiagnostic[], slug: string): AgentChatProfileConfiguration { - if (!isPlainObject(value)) { - return { foundation: 'discussion-v1' }; - } - if (value.foundation !== 'discussion-v1') diagnostics.push({ severity: 'error', slug, field: 'chatProfile.foundation', message: 'chatProfile.foundation must be discussion-v1.' }); - return { - foundation: 'discussion-v1', - responseStyle: typeof value.responseStyle === 'string' ? value.responseStyle : undefined, - promptTask: typeof value.promptTask === 'string' ? value.promptTask : undefined, - requiredCapabilities: Array.isArray(value.requiredCapabilities) ? value.requiredCapabilities.map(String).filter(Boolean) : undefined, - maxRuntimeSeconds: typeof value.maxRuntimeSeconds === 'number' ? value.maxRuntimeSeconds : undefined, - maxTotalTokens: typeof value.maxTotalTokens === 'number' ? value.maxTotalTokens : undefined, - warningTokens: typeof value.warningTokens === 'number' ? value.warningTokens : undefined, - maxCostAmount: typeof value.maxCostAmount === 'number' ? value.maxCostAmount : undefined, - costCurrency: typeof value.costCurrency === 'string' ? value.costCurrency : undefined, - toolAdditions: Array.isArray(value.toolAdditions) ? value.toolAdditions.map(String).filter(Boolean) : undefined, - contextModels: Array.isArray(value.contextModels) ? value.contextModels.map(String).filter(Boolean) : undefined, - }; -} - -export function normalizeActivityProfiles(value: unknown, diagnostics: AgentSpecDiagnostic[], slug: string, chatProfile?: unknown) { - if (!isPlainObject(value)) { - diagnostics.push({ - severity: 'error', - slug, - field: 'activityProfiles', - message: 'Expected activityProfiles to be an object.', - }); - return {}; - } - for (const diagnostic of validateAgentActivityProfilesConfiguration(value).diagnostics) { - diagnostics.push({ severity: 'error', slug, field: diagnostic.path, message: diagnostic.message }); - } - const profiles: Partial> = {}; - for (const [key, profile] of Object.entries(value)) { - if (!ACTIVITY_TYPES.has(key)) { - diagnostics.push({ - severity: 'error', - slug, - field: `activityProfiles.${key}`, - message: `Unsupported activity profile "${key}".`, - }); - continue; - } - const normalized = normalizeActivityProfile(profile, key as AgentActivityType, diagnostics, slug); - if (normalized) profiles[key as AgentActivityType] = normalized; - } - profiles.chat ??= compileDefaultChatActivityProfile(slug, normalizeChatSpecialization(chatProfile, diagnostics, slug)); - return profiles; -} - -export function selectDefaultActivityProfile( - profiles: Partial>, -): [AgentActivityType, AgentActivityProfile] | null { - for (const activity of ['acting', 'estimating', 'planning', 'reviewing', 'reporting', 'chat'] as const) { - const profile = profiles[activity]; - if (profile?.enabled) return [activity, profile]; - } - return null; -} diff --git a/src/agents/support/spec-normalizer-execution.ts b/src/agents/support/spec-normalizer-execution.ts deleted file mode 100644 index 76f5a258..00000000 --- a/src/agents/support/spec-normalizer-execution.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { AgentExecutionConfig, AgentProviderFallbackPolicy, AgentProviderProfile } from '@treeseed/sdk/types/agents'; -import type { AgentSpecDiagnostic } from './spec-types.ts'; -import { - APPROVAL_POLICIES, - DEFAULT_CODEX_ALLOWED_PATHS, - DEFAULT_CODEX_FORBIDDEN_PATHS, - EXECUTION_PROVIDERS, - REASONING_EFFORTS, - SANDBOX_MODES, - ensureBoolean, - ensurePositiveNumber, - ensureString, - isPlainObject, - normalizeOptionalString, - normalizeStringArray, - normalizeStringChoice, -} from './spec-normalizer-primitives.ts'; - -export function normalizeExecution( - value: unknown, - diagnostics: AgentSpecDiagnostic[], - slug: string, -): AgentExecutionConfig { - const next = isPlainObject(value) ? value : {}; - const worktree = isPlainObject(next.worktree) ? next.worktree : {}; - if (next.worktree !== undefined && !isPlainObject(next.worktree)) { - diagnostics.push({ - severity: 'error', - slug, - field: 'execution.worktree', - message: 'Expected execution.worktree to be an object.', - }); - } - return { - provider: normalizeStringChoice(next.provider, 'execution.provider', EXECUTION_PROVIDERS, diagnostics, slug, 'codex', { - warnOnUnknown: true, - }), - model: normalizeOptionalString(next.model, 'execution.model', diagnostics, slug) ?? 'gpt-5.5', - approvalPolicy: normalizeStringChoice(next.approvalPolicy, 'execution.approvalPolicy', APPROVAL_POLICIES, diagnostics, slug, 'never'), - sandboxMode: normalizeStringChoice(next.sandboxMode, 'execution.sandboxMode', SANDBOX_MODES, diagnostics, slug, 'workspace_write'), - reasoningEffort: normalizeStringChoice(next.reasoningEffort, 'execution.reasoningEffort', REASONING_EFFORTS, diagnostics, slug, 'medium'), - allowedPaths: normalizeStringArray(next.allowedPaths, 'execution.allowedPaths', diagnostics, slug, DEFAULT_CODEX_ALLOWED_PATHS), - forbiddenPaths: normalizeStringArray(next.forbiddenPaths, 'execution.forbiddenPaths', diagnostics, slug, DEFAULT_CODEX_FORBIDDEN_PATHS), - worktree: { - enabled: ensureBoolean(worktree.enabled, 'execution.worktree.enabled', diagnostics, slug, true), - root: normalizeOptionalString(worktree.root, 'execution.worktree.root', diagnostics, slug), - branchPrefix: normalizeOptionalString(worktree.branchPrefix, 'execution.worktree.branchPrefix', diagnostics, slug), - }, - maxConcurrency: ensurePositiveNumber(next.maxConcurrency, 'execution.maxConcurrency', diagnostics, slug, 1), - timeoutSeconds: ensurePositiveNumber(next.timeoutSeconds, 'execution.timeoutSeconds', diagnostics, slug, 900), - cooldownSeconds: ensurePositiveNumber(next.cooldownSeconds, 'execution.cooldownSeconds', diagnostics, slug, 30, true), - leaseSeconds: ensurePositiveNumber(next.leaseSeconds, 'execution.leaseSeconds', diagnostics, slug, 300), - retryLimit: ensurePositiveNumber(next.retryLimit, 'execution.retryLimit', diagnostics, slug, 3, true), - branchPrefix: ensureString(next.branchPrefix ?? 'agent', 'execution.branchPrefix', diagnostics, slug) || 'agent', - providerProfile: normalizeProviderProfile(next.providerProfile, diagnostics, slug), - }; -} - -function normalizeWeightedProviderList(value: unknown, field: string, diagnostics: AgentSpecDiagnostic[], slug: string) { - if (value === undefined) return []; - if (!Array.isArray(value)) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be an array.`, - }); - return []; - } - return value.flatMap((entry, index) => { - if (!isPlainObject(entry)) { - diagnostics.push({ - severity: 'error', - slug, - field: `${field}[${index}]`, - message: 'Expected execution-provider preference to be an object.', - }); - return []; - } - return [{ - providerId: typeof entry.providerId === 'string' ? entry.providerId : undefined, - provider: typeof entry.provider === 'string' ? entry.provider : undefined, - model: typeof entry.model === 'string' ? entry.model : undefined, - modelClass: typeof entry.modelClass === 'string' ? entry.modelClass : undefined, - weight: ensurePositiveNumber(entry.weight, `${field}[${index}].weight`, diagnostics, slug, 1, true), - reason: typeof entry.reason === 'string' ? entry.reason : undefined, - maxQualityPenalty: typeof entry.maxQualityPenalty === 'number' ? entry.maxQualityPenalty : undefined, - }]; - }); -} - -function normalizeProviderProfile( - value: unknown, - diagnostics: AgentSpecDiagnostic[], - slug: string, -): AgentProviderProfile | undefined { - if (value === undefined) return undefined; - if (!isPlainObject(value)) { - diagnostics.push({ - severity: 'error', - slug, - field: 'execution.providerProfile', - message: 'Expected execution.providerProfile to be an object.', - }); - return undefined; - } - const fallbackPolicy = typeof value.fallbackPolicy === 'string' - ? value.fallbackPolicy as AgentProviderFallbackPolicy - : 'allow_substitution'; - if (!['allow_substitution', 'require_same_model_class', 'fail_if_unavailable', 'ask_for_approval'].includes(fallbackPolicy)) { - diagnostics.push({ - severity: 'error', - slug, - field: 'execution.providerProfile.fallbackPolicy', - message: `Unsupported fallback policy "${String(value.fallbackPolicy)}".`, - }); - } - return { - requiredCapabilities: Array.isArray(value.requiredCapabilities) - ? value.requiredCapabilities.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0) - : [], - preferredExecutionProviders: normalizeWeightedProviderList(value.preferredExecutionProviders, 'execution.providerProfile.preferredExecutionProviders', diagnostics, slug), - acceptableFallbacks: normalizeWeightedProviderList(value.acceptableFallbacks, 'execution.providerProfile.acceptableFallbacks', diagnostics, slug).map((entry) => ({ - providerId: entry.providerId, - provider: entry.provider, - model: entry.model, - modelClass: entry.modelClass, - maxQualityPenalty: entry.maxQualityPenalty, - })), - disallowedProviders: Array.isArray(value.disallowedProviders) ? value.disallowedProviders.filter((entry): entry is string => typeof entry === 'string') : undefined, - disallowedRegions: Array.isArray(value.disallowedRegions) ? value.disallowedRegions.filter((entry): entry is string => typeof entry === 'string') : undefined, - fallbackPolicy, - }; -} - diff --git a/src/agents/support/spec-normalizer-policy.ts b/src/agents/support/spec-normalizer-policy.ts deleted file mode 100644 index 0d7fd538..00000000 --- a/src/agents/support/spec-normalizer-policy.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { assertKnownAgentToolIds } from '@treeseed/sdk'; -import { CONTENT_ACTIONS, type ContentAction } from '@treeseed/sdk/content-operations'; -import type { AgentActivityPermissions, AgentContentAccessPolicy, AgentToolPolicy, AgentTriggerConfig, AgentTriggerKind } from '@treeseed/sdk/types/agents'; -import type { AgentSpecDiagnostic } from './spec-types.ts'; -import { TRIGGER_KINDS, ensureString, isPlainObject } from './spec-normalizer-primitives.ts'; - -export function normalizeTrigger( - value: unknown, - index: number, - diagnostics: AgentSpecDiagnostic[], - slug: string, -): AgentTriggerConfig | null { - if (!isPlainObject(value)) { - diagnostics.push({ - severity: 'error', - slug, - field: `triggers[${index}]`, - message: 'Expected trigger to be an object.', - }); - return null; - } - const type = ensureString(value.type, `triggers[${index}].type`, diagnostics, slug) as AgentTriggerKind; - if (!TRIGGER_KINDS.includes(type)) { - diagnostics.push({ - severity: 'error', - slug, - field: `triggers[${index}].type`, - message: `Unsupported trigger type "${String(value.type ?? '')}".`, - }); - return null; - } - return { - type, - cron: typeof value.cron === 'string' ? value.cron : undefined, - messageTypes: Array.isArray(value.messageTypes) ? value.messageTypes.filter((entry): entry is string => typeof entry === 'string') : [], - models: Array.isArray(value.models) ? value.models.filter((entry): entry is string => typeof entry === 'string') : [], - sinceField: typeof value.sinceField === 'string' ? value.sinceField : undefined, - runOnStart: typeof value.runOnStart === 'boolean' ? value.runOnStart : false, - }; -} - -function normalizeTools( - value: unknown, - enabled: boolean, - diagnostics: AgentSpecDiagnostic[], - slug: string, -): AgentToolPolicy { - if (value === undefined || value === null) { - if (enabled) { - diagnostics.push({ - severity: 'error', - slug, - field: 'tools.allowed', - message: 'Enabled agents must declare tools.allowed, even when the list is empty.', - }); - } - return { allowed: [] }; - } - if (!isPlainObject(value)) { - diagnostics.push({ - severity: 'error', - slug, - field: 'tools', - message: 'Expected tools to be an object.', - }); - return { allowed: [] }; - } - if (!Array.isArray(value.allowed)) { - diagnostics.push({ - severity: 'error', - slug, - field: 'tools.allowed', - message: 'Expected tools.allowed to be an array of tool ids.', - }); - return { allowed: [] }; - } - const ids = value.allowed.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0) - .map((entry) => entry.trim()); - if (ids.length !== value.allowed.length) { - diagnostics.push({ - severity: 'error', - slug, - field: 'tools.allowed', - message: 'Expected tools.allowed to contain only non-empty strings.', - }); - } - const result = assertKnownAgentToolIds(ids); - for (const duplicate of result.duplicates) { - diagnostics.push({ - severity: 'warning', - slug, - field: 'tools.allowed', - message: `Duplicate tool id "${duplicate}" was ignored.`, - }); - } - for (const unknown of result.unknown) { - diagnostics.push({ - severity: 'error', - slug, - field: 'tools.allowed', - message: `Unknown agent tool id "${unknown}".`, - }); - } - return { allowed: result.known }; -} - -export function normalizeToolPolicy( - value: unknown, - field: string, - enabled: boolean, - diagnostics: AgentSpecDiagnostic[], - slug: string, -): AgentToolPolicy { - const result = normalizeTools(value, enabled, diagnostics, slug); - if (isPlainObject(value) && Array.isArray(value.denied)) { - return { - ...result, - denied: value.denied.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0), - }; - } - return result; -} - -function stringList(value: unknown, field: string, diagnostics: AgentSpecDiagnostic[], slug: string) { - if (value === undefined) return undefined; - if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string' && entry.trim())) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be an array of non-empty strings.`, - }); - return undefined; - } - return value.map((entry) => entry.trim()); -} - -function normalizeContentScope( - value: unknown, - field: string, - diagnostics: AgentSpecDiagnostic[], - slug: string, -) { - if (value === undefined || value === null) return undefined; - if (!isPlainObject(value)) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be an object.`, - }); - return undefined; - } - const models = stringList(value.models, `${field}.models`, diagnostics, slug) ?? []; - const actions = stringList(value.actions, `${field}.actions`, diagnostics, slug); - const invalidActions = (actions ?? []).filter((action) => !CONTENT_ACTIONS.includes(action as ContentAction)); - for (const action of invalidActions) { - diagnostics.push({ - severity: 'error', - slug, - field: `${field}.actions`, - message: `Unknown content action "${action}".`, - }); - } - return { - models, - actions: actions?.filter((action): action is ContentAction => CONTENT_ACTIONS.includes(action as ContentAction)), - books: stringList(value.books, `${field}.books`, diagnostics, slug), - paths: stringList(value.paths, `${field}.paths`, diagnostics, slug), - relations: stringList(value.relations, `${field}.relations`, diagnostics, slug), - }; -} - -export function normalizeContentAccess( - value: unknown, - diagnostics: AgentSpecDiagnostic[], - slug: string, - field = 'permissions', -): AgentContentAccessPolicy | undefined { - if (value === undefined || value === null) return undefined; - if (!isPlainObject(value)) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be an object.`, - }); - return undefined; - } - const commit = value.commit; - if (commit !== undefined && (!isPlainObject(commit) || typeof commit.allowed !== 'boolean')) { - diagnostics.push({ - severity: 'error', - slug, - field: `${field}.commit.allowed`, - message: `Expected ${field}.commit.allowed to be a boolean.`, - }); - } - return { - read: normalizeContentScope(value.read, `${field}.read`, diagnostics, slug), - write: normalizeContentScope(value.write, `${field}.write`, diagnostics, slug), - commit: isPlainObject(commit) && typeof commit.allowed === 'boolean' ? { allowed: commit.allowed } : { allowed: false }, - }; -} - -export function normalizeActivityPermissions(value: unknown, diagnostics: AgentSpecDiagnostic[], slug: string, field: string): AgentActivityPermissions | undefined { - if (value === undefined || value === null) return undefined; - if (!isPlainObject(value)) { - diagnostics.push({ severity:'error',slug,field,message:`Expected ${field} to be an object.` }); - return undefined; - } - return value as AgentActivityPermissions; -} - -export function permissionProjectionForProfile(permissions: AgentActivityPermissions | undefined): AgentContentAccessPolicy | undefined { - if (!permissions) return undefined; - const entries=Object.entries(permissions.content ?? {}); - const scope=(write:boolean) => { - const actions=write?['create','update','link','validate','commit']:['describe','query','read']; - const selected=entries.filter(([,grant])=>grant.operations.some((operation)=>actions.includes(operation))); - if(!selected.length) return undefined; - return { models:selected.map(([model])=>model),actions:[...new Set(selected.flatMap(([,grant])=>grant.operations.filter((operation)=>actions.includes(operation))))],books:[...new Set(selected.flatMap(([,grant])=>Array.isArray(grant.filters?.books)?grant.filters.books.map(String):[]))],paths:[...new Set(selected.flatMap(([,grant])=>Array.isArray(grant.filters?.paths)?grant.filters.paths.map(String):[]))] }; - }; - return { read:scope(false),write:scope(true),commit:permissions.commit ?? { allowed:false } }; -} diff --git a/src/agents/support/spec-normalizer-primitives.ts b/src/agents/support/spec-normalizer-primitives.ts deleted file mode 100644 index 6010c41a..00000000 --- a/src/agents/support/spec-normalizer-primitives.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { BUILT_IN_AGENT_EXECUTION_PROVIDER_IDS, type AgentTriggerKind } from '@treeseed/sdk/types/agents'; -import type { AgentSpecDiagnostic } from './spec-types.ts'; - -export const TRIGGER_KINDS: readonly AgentTriggerKind[] = ['schedule', 'message', 'follow', 'startup']; -export const ACTIVITY_TYPES = new Set(['planning', 'estimating', 'acting', 'reviewing', 'reporting']); -export const GENERIC_HANDLER_KINDS = new Set(['writer', 'actor', 'estimate', 'releaser', 'reporter']); -export const EXECUTION_PROVIDERS = new Set(BUILT_IN_AGENT_EXECUTION_PROVIDER_IDS); -export const APPROVAL_POLICIES = new Set(['never', 'on_request', 'always']); -export const SANDBOX_MODES = new Set(['read_only', 'workspace_write']); -export const REASONING_EFFORTS = new Set(['low', 'medium', 'high']); -export const DEFAULT_CODEX_ALLOWED_PATHS = ['**']; -export const DEFAULT_CODEX_FORBIDDEN_PATHS = ['.git/**', '.agent-worktrees/**', '.treeseed/secrets/**', 'node_modules/**']; -export const LEGACY_AGENT_FIELDS = [ - 'handler', - 'handlerConfig', - 'systemPrompt', - 'permissions', - 'tools', - 'context', - 'execution', - 'outputs', -] as const; - -export function isPlainObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' && !Array.isArray(value); -} - -export function ensureString(value: unknown, field: string, diagnostics: AgentSpecDiagnostic[], slug: string) { - if (typeof value !== 'string' || value.trim().length === 0) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be a non-empty string.`, - }); - return ''; - } - return value; -} - -export function ensureBoolean(value: unknown, field: string, diagnostics: AgentSpecDiagnostic[], slug: string, fallback = false) { - if (value === undefined) { - return fallback; - } - if (typeof value !== 'boolean') { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be a boolean.`, - }); - return fallback; - } - return value; -} - -export function ensurePositiveNumber( - value: unknown, - field: string, - diagnostics: AgentSpecDiagnostic[], - slug: string, - fallback: number, - allowZero = false, -) { - if (value === undefined) { - return fallback; - } - if (typeof value !== 'number' || Number.isNaN(value) || (!allowZero && value <= 0) || (allowZero && value < 0)) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be ${allowZero ? 'a non-negative' : 'a positive'} number.`, - }); - return fallback; - } - return value; -} - -export function normalizeOptionalString(value: unknown, field: string, diagnostics: AgentSpecDiagnostic[], slug: string): string | undefined { - if (value === undefined) return undefined; - if (typeof value !== 'string' || !value.trim()) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be a non-empty string.`, - }); - return undefined; - } - return value.trim(); -} - -export function normalizeStringChoice( - value: unknown, - field: string, - allowed: Set, - diagnostics: AgentSpecDiagnostic[], - slug: string, - fallback: T, - options: { warnOnUnknown?: boolean } = {}, -): T | string { - if (value === undefined) return fallback; - const normalized = normalizeOptionalString(value, field, diagnostics, slug)?.toLowerCase().replaceAll('-', '_'); - if (!normalized) return fallback; - if (allowed.has(normalized as T)) return normalized as T; - diagnostics.push({ - severity: options.warnOnUnknown ? 'warning' : 'error', - slug, - field, - message: `Unsupported ${field} "${String(value)}".`, - }); - return normalized; -} - -export function normalizeStringArray( - value: unknown, - field: string, - diagnostics: AgentSpecDiagnostic[], - slug: string, - fallback: string[], -) { - if (value === undefined) return [...fallback]; - if (!Array.isArray(value)) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to be an array of strings.`, - }); - return [...fallback]; - } - const values = value.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0) - .map((entry) => entry.trim()); - if (values.length !== value.length) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `Expected ${field} to contain only non-empty strings.`, - }); - } - return values; -} diff --git a/src/agents/support/spec-normalizer.ts b/src/agents/support/spec-normalizer.ts deleted file mode 100644 index 11ba3e17..00000000 --- a/src/agents/support/spec-normalizer.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { AGENT_CLI_ALLOW_TOOLS, type AgentHandlerKind, type AgentTriggerConfig } from '@treeseed/sdk/types/agents'; -import { AGENT_MESSAGE_TYPES } from '../contracts/messages.ts'; -import { normalizeAgentCliOptions } from '../cli-tools.ts'; -import { normalizeActivityProfiles, normalizeIdentity, selectDefaultActivityProfile } from './spec-normalizer-activities.ts'; -import { normalizeExecution } from './spec-normalizer-execution.ts'; -import { permissionProjectionForProfile, normalizeTrigger } from './spec-normalizer-policy.ts'; -import { LEGACY_AGENT_FIELDS, ensureBoolean, ensureString, isPlainObject } from './spec-normalizer-primitives.ts'; -import type { - AgentSpecDiagnostic, - AgentSpecNormalizationResult, - AgentSpecParts, - AgentSpecValidationContext, - NormalizedAgentRuntimeSpec, - RawAgentRuntimeSpec, -} from './spec-types.ts'; - -function normalizeParts( - raw: RawAgentRuntimeSpec, - slugHint: string, - diagnostics: AgentSpecDiagnostic[], -): AgentSpecParts | null { - const slug = ensureString(raw.slug ?? slugHint, 'slug', diagnostics, slugHint) || slugHint; - for (const field of LEGACY_AGENT_FIELDS) { - if (raw[field] !== undefined) { - diagnostics.push({ - severity: 'error', - slug, - field, - message: `${field} is legacy top-level agent configuration. Move it under activityProfiles..`, - }); - } - } - const activityProfiles = normalizeActivityProfiles(raw.activityProfiles, diagnostics, slug, raw.chatProfile); - const selected = selectDefaultActivityProfile(activityProfiles); - if (!selected) { - diagnostics.push({ - severity: 'error', - slug, - field: 'activityProfiles', - message: 'Expected at least one enabled activity profile.', - }); - return null; - } - const [activityType, profile] = selected; - const handler = profile.handler as AgentHandlerKind; - const triggers = Array.isArray(raw.triggers) - ? raw.triggers.map((entry, index) => normalizeTrigger(entry, index, diagnostics, slug)).filter((entry): entry is AgentTriggerConfig => Boolean(entry)) - : []; - if (!triggers.length) { - triggers.push({ type: 'startup', runOnStart: true }); - } - try { - const cli = normalizeAgentCliOptions(raw.cli); - const enabled = ensureBoolean(raw.enabled, 'enabled', diagnostics, slug, true); - const spec: AgentSpecParts = { - slug, - handler, - activityType, - activityProfiles, - branchPolicy: profile.branchPolicy, - questionPolicy: profile.questionPolicy, - identity: normalizeIdentity(raw.identity, diagnostics, slug), - projectAgentClassId: ensureString( - raw.projectAgentClassId ?? raw.agentClassId ?? raw.agentClass, - 'projectAgentClassId', - diagnostics, - slug, - ), - projectAgentClassSlug: ensureString( - raw.projectAgentClassSlug ?? raw.agentClassSlug ?? raw.projectAgentClassId ?? raw.agentClassId ?? raw.agentClass, - 'projectAgentClassSlug', - diagnostics, - slug, - ), - activityConfig: { - workPackageKind: profile.handler, - domain: activityType, - activityType, - branchPolicy: profile.branchPolicy, - questionPolicy: profile.questionPolicy, - }, - enabled, - systemPrompt: profile.prompt.system, - persona: typeof raw.persona === 'string' ? raw.persona : '', - cli, - triggers, - triggerPolicy: isPlainObject(raw.triggerPolicy) - ? { - maxRunsPerCycle: - typeof raw.triggerPolicy.maxRunsPerCycle === 'number' ? raw.triggerPolicy.maxRunsPerCycle : undefined, - messageBatchSize: - typeof raw.triggerPolicy.messageBatchSize === 'number' ? raw.triggerPolicy.messageBatchSize : undefined, - } - : undefined, - permissions: [], - tools: profile.tools, - permissionProjection: permissionProjectionForProfile(profile.permissions), - context: { queries: [] }, - execution: normalizeExecution(raw.execution, diagnostics, slug), - outputs: profile.outputs, - }; - return spec; - } catch (error) { - diagnostics.push({ - severity: 'error', - slug, - field: 'cli', - message: error instanceof Error ? error.message : String(error), - }); - return null; - } -} - -export function normalizeAgentRuntimeSpec( - raw: RawAgentRuntimeSpec, - context: AgentSpecValidationContext, -): AgentSpecNormalizationResult { - const slugHint = typeof raw.slug === 'string' && raw.slug ? raw.slug : 'unknown-agent'; - const diagnostics: AgentSpecDiagnostic[] = []; - const parts = normalizeParts(raw, slugHint, diagnostics); - if (!parts) { - return { spec: null, diagnostics }; - } - - const spec: NormalizedAgentRuntimeSpec = { - ...parts, - name: typeof raw.name === 'string' ? raw.name : undefined, - description: typeof raw.description === 'string' ? raw.description : undefined, - summary: typeof raw.summary === 'string' ? raw.summary : undefined, - operator: typeof raw.operator === 'string' ? raw.operator : undefined, - runtimeStatus: typeof raw.runtimeStatus === 'string' ? raw.runtimeStatus : undefined, - capabilities: Array.isArray(raw.capabilities) ? raw.capabilities.filter((entry): entry is string => typeof entry === 'string') : [], - tags: Array.isArray(raw.tags) ? raw.tags.filter((entry): entry is string => typeof entry === 'string') : [], - }; - - if (!context.registeredHandlers.includes(spec.handler)) { - diagnostics.push({ - severity: 'error', - slug: spec.slug, - field: 'handler', - message: `No runtime handler is registered for "${spec.handler}".`, - }); - } - - for (const trigger of spec.triggers) { - if (trigger.type === 'message') { - for (const messageType of trigger.messageTypes ?? []) { - if ( - !context.messageTypes.includes(messageType) - || !AGENT_MESSAGE_TYPES.includes(messageType as (typeof AGENT_MESSAGE_TYPES)[number]) - ) { - diagnostics.push({ - severity: 'error', - slug: spec.slug, - field: 'triggers.messageTypes', - message: `Unknown message trigger type "${messageType}".`, - }); - } - } - } - if (trigger.type === 'follow' && !(trigger.models?.length)) { - diagnostics.push({ - severity: 'error', - slug: spec.slug, - field: 'triggers.models', - message: 'Follow triggers must declare at least one model.', - }); - } - } - - for (const messageType of spec.outputs.messageTypes) { - if ( - !context.messageTypes.includes(messageType) - || !AGENT_MESSAGE_TYPES.includes(messageType as (typeof AGENT_MESSAGE_TYPES)[number]) - ) { - diagnostics.push({ - severity: 'error', - slug: spec.slug, - field: 'outputs.messageTypes', - message: `Unknown emitted message type "${messageType}".`, - }); - } - } - - if (spec.cli.allowTools?.some((tool) => !AGENT_CLI_ALLOW_TOOLS.includes(tool))) { - diagnostics.push({ - severity: 'error', - slug: spec.slug, - field: 'cli.allowTools', - message: 'Agent declared an unsupported tool allowance.', - }); - } - - return { - spec: diagnostics.some((entry) => entry.severity === 'error') ? null : spec, - diagnostics, - }; -} diff --git a/src/agents/support/spec-types.ts b/src/agents/support/spec-types.ts deleted file mode 100644 index 6cb08218..00000000 --- a/src/agents/support/spec-types.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { - AgentCliOptions, - AgentActivityProfile, - AgentActivityPermissions, - AgentActivityType, - AgentBranchPolicy, - AgentExecutionConfig, - AgentHandlerKind, - AgentDefinitionIdentity, - AgentContentAccessPolicy, - AgentOutputContract, - AgentPermissionConfig, - AgentQuestionPolicy, - AgentToolPolicy, - AgentTriggerConfig, -} from '@treeseed/sdk/types/agents'; -import type { DeclarativeContextQuery } from '@treeseed/sdk/graph/context-query-contracts'; - -export type AgentSpecDiagnosticSeverity = 'error' | 'warning'; - -export interface AgentSpecDiagnostic { - severity: AgentSpecDiagnosticSeverity; - slug: string; - field: string; - message: string; -} - -export interface AgentSpecValidationContext { - registeredHandlers: readonly AgentHandlerKind[]; - messageTypes: readonly string[]; -} - -export interface RawAgentRuntimeSpec { - id?: unknown; - body?: unknown; - slug?: unknown; - handler?: unknown; - projectAgentClassId?: unknown; - projectAgentClassSlug?: unknown; - agentClassId?: unknown; - agentClassSlug?: unknown; - agentClass?: unknown; - title?: unknown; - identity?: unknown; - activityProfiles?: unknown; - chatProfile?: unknown; - template?: unknown; - handlerConfig?: unknown; - enabled?: unknown; - systemPrompt?: unknown; - persona?: unknown; - cli?: unknown; - triggers?: unknown; - triggerPolicy?: unknown; - permissions?: unknown; - tools?: unknown; - context?: unknown; - execution?: unknown; - outputs?: unknown; - name?: unknown; - description?: unknown; - summary?: unknown; - operator?: unknown; - runtimeStatus?: unknown; - capabilities?: unknown; - tags?: unknown; -} - -export interface AgentSpecNormalizationResult { - spec: NormalizedAgentRuntimeSpec | null; - diagnostics: AgentSpecDiagnostic[]; -} - -export interface NormalizedTriggerPolicy { - maxRunsPerCycle?: number; - messageBatchSize?: number; -} - -export interface AgentSpecParts { - slug: string; - handler: AgentHandlerKind; - activityType: AgentActivityType; - activityProfiles?: Partial>; - authorityPresetIds?: import('@treeseed/sdk/agent-capacity').AgentAuthorityPresetId[]; - authoritySnapshot?: { presetIds: import('@treeseed/sdk/agent-capacity').AgentAuthorityPresetId[]; permissions?: AgentActivityPermissions; tools: AgentToolPolicy; branchPolicy: AgentBranchPolicy }; - branchPolicy?: AgentBranchPolicy; - questionPolicy?: AgentQuestionPolicy; - identity?: AgentDefinitionIdentity; - projectAgentClassId: string; - projectAgentClassSlug: string; - activityConfig?: Record; - enabled: boolean; - systemPrompt: string; - persona: string; - cli: AgentCliOptions; - triggers: AgentTriggerConfig[]; - triggerPolicy?: NormalizedTriggerPolicy; - permissions: AgentPermissionConfig[]; - tools: AgentToolPolicy; - permissionProjection?: AgentContentAccessPolicy; - context?: { - queries: DeclarativeContextQuery[]; - }; - execution: AgentExecutionConfig; - outputs: AgentOutputContract; -} - -export type NormalizedAgentRuntimeSpec = AgentSpecParts & { - name?: string; - description?: string; - summary?: string; - operator?: string; - runtimeStatus?: string; - capabilities?: string[]; - tags?: string[]; -}; diff --git a/src/agents/testing/agent-authoring-diagnostics.ts b/src/agents/testing/agent-authoring-diagnostics.ts deleted file mode 100644 index 3b545ee5..00000000 --- a/src/agents/testing/agent-authoring-diagnostics.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; - -export type AgentAuthoringDiagnosticCode = - | 'agent.handler_not_profile_handler' - | 'agent.missing_project_agent_class' - | 'agent.missing_activity_profiles' - | 'agent.missing_context_queries' - | 'agent.missing_required_capabilities' - | 'agent.actor_missing_path_constraints' - | 'agent.content_write_requires_treedx' - | 'agent.mutation_output_requires_content_commit' - | 'agent.output_contract_ambiguous' - | 'agent.execution_provider_too_specific' - | 'agent.local_filesystem_content_dependency'; - -export interface AgentAuthoringDiagnostic { - code: AgentAuthoringDiagnosticCode; - severity: 'info' | 'warning' | 'error'; - message: string; - filePath?: string | null; - fieldPath: string; - fixSuggestion: string; - docsLink: string; - agentSlug?: string | null; -} - -const PROFILE_HANDLERS = new Set(['writer', 'actor', 'estimate', 'releaser', 'reporter']); - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function array(value: unknown): unknown[] { - return Array.isArray(value) ? value : []; -} - -function stringArray(value: unknown) { - return array(value).filter((entry): entry is string => typeof entry === 'string'); -} - -function profileEntries(agent: Record) { - const profiles = record(agent.activityProfiles); - return Object.entries(profiles).filter(([, value]) => record(value).enabled !== false); -} - -function profilePath(activity: string, field: string) { - return `activityProfiles.${activity}.${field}`; -} - -export function diagnoseAgentAuthoring(spec: AgentRuntimeSpec | Record, options: { - filePath?: string | null; - docsBase?: string; -} = {}): AgentAuthoringDiagnostic[] { - const agent = spec as Record; - const slug = typeof agent.slug === 'string' ? agent.slug : null; - const docsBase = options.docsBase ?? '/docs/agent-docs'; - const diagnostics: AgentAuthoringDiagnostic[] = []; - const add = ( - code: AgentAuthoringDiagnosticCode, - severity: AgentAuthoringDiagnostic['severity'], - fieldPath: string, - message: string, - fixSuggestion: string, - ) => diagnostics.push({ - code, - severity, - message, - filePath: options.filePath ?? null, - fieldPath, - fixSuggestion, - docsLink: docsBase, - agentSlug: slug, - }); - const handler = typeof agent.handler === 'string' ? agent.handler : ''; - if (handler && !PROFILE_HANDLERS.has(handler)) { - add( - 'agent.handler_not_profile_handler', - 'error', - 'handler', - `Agent ${slug ?? ''} uses unsupported handler "${handler}".`, - 'Use one of writer, actor, estimate, releaser, or reporter through an activity profile.', - ); - } - if (!agent.projectAgentClassId && !agent.projectAgentClassSlug) { - add('agent.missing_project_agent_class', 'error', 'projectAgentClassId', 'Agent has no project agent class mapping.', 'Set projectAgentClassId or projectAgentClassSlug so allocation remains independent from handler choice.'); - } - const profiles = profileEntries(agent); - const hasProfiles = profiles.length > 0; - if (!hasProfiles) { - add('agent.missing_activity_profiles', 'error', 'activityProfiles', 'Agent has no enabled activity profiles.', 'Declare activityProfiles for planning, estimating, acting, reviewing, reporting, or releaser work.'); - } - const fallbackProfile = { - handler, - context: record(agent.context), - execution: record(agent.execution), - outputs: record(agent.outputs), - permissions: record(agent.permissionProjection), - }; - const entries = hasProfiles ? profiles : [['runtime', fallbackProfile] as [string, unknown]]; - for (const [activity, rawProfile] of entries) { - const profile = record(rawProfile); - const profileHandler = typeof profile.handler === 'string' ? profile.handler : handler; - if (profileHandler && !PROFILE_HANDLERS.has(profileHandler)) { - add( - 'agent.handler_not_profile_handler', - 'error', - profilePath(activity, 'handler'), - `Agent ${slug ?? ''} activity ${activity} uses unsupported handler "${profileHandler}".`, - 'Use one of writer, actor, estimate, releaser, or reporter.', - ); - } - const context = record(profile.context); - const permissions = record(profile.permissions); - const contentPermissions=record(permissions.content); - const readModels=Object.entries(contentPermissions).filter(([,grant])=>stringArray(record(grant).operations).some((operation)=>['describe','query','read'].includes(operation))).map(([model])=>model); - if (!array(context.queries).length && !readModels.length) { - add('agent.missing_context_queries', 'error', profilePath(activity, 'context.queries'), 'Activity profile has no TreeDX/API context queries or readable content models.', 'Declare context queries or per-model permissions so runtime context is scoped through TreeDX/API handles.'); - } - const execution = record(profile.execution); - const requiredCapabilities = stringArray(execution.requiredCapabilities ?? record(execution.providerProfile).requiredCapabilities); - if (!requiredCapabilities.length) { - add('agent.missing_required_capabilities', 'error', profilePath(activity, 'execution.requiredCapabilities'), 'Activity profile does not declare execution-provider capability requirements.', 'Declare required capabilities used by API/provider assignment.'); - } - const allowedPaths = stringArray(execution.allowedPaths); - const forbiddenPaths = stringArray(execution.forbiddenPaths); - if (profileHandler === 'actor' && (!allowedPaths.length || !forbiddenPaths.length)) { - add('agent.actor_missing_path_constraints', 'error', profilePath(activity, 'execution.allowedPaths'), 'Actor activity profile is missing explicit allowed or forbidden path constraints.', 'Set allowedPaths and forbiddenPaths for every mutating actor profile.'); - } - const contentLike = readModels.some((model) => /content|documentation|knowledge|note|question|proposal|decision/u.test(model)) - || allowedPaths.some((path) => /src\/content|docs\/src\/content|knowledge/u.test(path)); - if (profileHandler === 'actor' && contentLike && !requiredCapabilities.some((capability) => /treedx|workspace|content:write|files:write/u.test(capability))) { - add('agent.content_write_requires_treedx', 'error', profilePath(activity, 'execution.requiredCapabilities'), 'Content-writing actor profile does not require TreeDX workspace/write capability.', 'Add a TreeDX workspace or files:write capability requirement.'); - } - const outputs = record(profile.outputs); - if (!Object.keys(outputs).length) { - add('agent.output_contract_ambiguous', 'error', profilePath(activity, 'outputs'), 'Activity profile has no explicit output contract.', 'Declare content, tool, signal, or terminal output contracts so AgentKernel can validate results.'); - } - const modelMutations = stringArray(outputs.modelMutations); - const commitAllowed = record(permissions.commit).allowed === true; - const branchKind = typeof record(profile.branchPolicy).kind === 'string' - ? String(record(profile.branchPolicy).kind) - : ''; - const allowedTools = stringArray(record(profile.tools).allowed); - if (modelMutations.length > 0 && ( - !commitAllowed - || branchKind === 'read-only' - || !allowedTools.includes('treeseed.content.create') - || !allowedTools.includes('treeseed.content.commit') - )) { - add( - 'agent.mutation_output_requires_content_commit', - 'error', - profilePath(activity, 'outputs.modelMutations'), - 'Activity profile promises model mutations but cannot create and commit TreeSeed content through TreeDX.', - 'Use a writable content branch, allow content commits, and grant treeseed.content.create plus treeseed.content.commit.', - ); - } - if (typeof execution.provider === 'string' || typeof execution.providerId === 'string') { - add('agent.execution_provider_too_specific', 'warning', profilePath(activity, 'execution'), 'Activity profile pins a provider directly instead of declaring capability requirements.', 'Prefer execution.requiredCapabilities and let API/provider assignment select the execution provider.'); - } - const localDependency = stringArray(record(profile.metadata).requiredLocalPaths).length > 0 - || stringArray(record(agent.metadata).requiredLocalPaths).length > 0; - if (contentLike && localDependency) { - add('agent.local_filesystem_content_dependency', 'warning', profilePath(activity, 'metadata.requiredLocalPaths'), 'Content profile declares local filesystem content dependencies.', 'Move content access to TreeDX context queries or assignment-scoped TreeDX tools.'); - } - } - return diagnostics; -} - -export function summarizeAgentAuthoringDiagnostics(specs: Array>) { - const diagnostics = specs.flatMap((spec) => diagnoseAgentAuthoring(spec)); - return { - ok: diagnostics.every((diagnostic) => diagnostic.severity !== 'error'), - diagnostics, - }; -} diff --git a/src/agents/testing/agent-contracts.ts b/src/agents/testing/agent-contracts.ts deleted file mode 100644 index 9cb15b15..00000000 --- a/src/agents/testing/agent-contracts.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { existsSync } from 'node:fs'; -import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; -import { dirname, join, relative, resolve } from 'node:path'; -import { compileDeclarativeContextQuery } from '@treeseed/sdk/graph/context-query-contracts'; -import { parse as parseYaml } from 'yaml'; -import { AGENT_MESSAGE_TYPES } from '../contracts/messages.ts'; -import { listRegisteredAgentHandlers, resolveAgentHandler } from '../support/registry.ts'; -import { normalizeAgentRuntimeSpec } from '../support/spec-normalizer.ts'; -import { EXECUTION_PROVIDERS } from '../support/spec-normalizer-primitives.ts'; -import type { NormalizedAgentRuntimeSpec } from '../support/spec-types.ts'; -import type { AgentSpecDiagnostic, RawAgentRuntimeSpec } from '../support/spec-types.ts'; - -const PROFILE_HANDLERS = new Set(['writer', 'actor', 'estimate', 'releaser', 'reporter']); - -export interface AgentContractIssue { - severity: 'error' | 'warning'; - slug: string; - field: string; - message: string; -} - -export interface AgentContractCheckResult { - ok: boolean; - repoRoot: string; - generatedAt: string; - agents: Array<{ - slug: string; - handler: string; - projectAgentClassId: string; - enabled: boolean; - triggers: string[]; - outputMessageTypes: string[]; - permissions: string[]; - status: 'PASS' | 'WARN' | 'FAIL'; - issues: AgentContractIssue[]; - }>; - issues: AgentContractIssue[]; - reportPath: string; - jsonPath: string; -} - -function permissionKey(permission: { model: string; operations: string[] }) { - return `${permission.model}:${permission.operations.join(',')}`; -} - -function hasPermission(agent: NormalizedAgentRuntimeSpec, model: string, operation: string) { - return agent.permissions.some((permission) => - permission.model === model && permission.operations.includes(operation as never), - ); -} - -function issue(agent: NormalizedAgentRuntimeSpec, field: string, message: string, severity: AgentContractIssue['severity'] = 'error'): AgentContractIssue { - return { severity, slug: agent.slug, field, message }; -} - -function parseAgentSource(source: string) { - const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u); - if (!match) return { frontmatter: {}, body: source }; - const frontmatter = parseYaml(match[1] ?? '') as unknown; - return { - frontmatter: frontmatter && typeof frontmatter === 'object' && !Array.isArray(frontmatter) - ? frontmatter as Record - : {}, - body: source.slice(match[0].length), - }; -} - -async function discoverAgentContentRoots(repoRoot: string) { - const candidates = [ - resolve(repoRoot, 'src/content/agents'), - resolve(repoRoot, 'docs/src/content/agents'), - ]; - return candidates.filter((candidate) => existsSync(candidate)); -} - -async function listAgentSourceFiles(root: string) { - const entries = await readdir(root, { withFileTypes: true }).catch(() => []); - return entries - .filter((entry) => entry.isFile() && /\.mdx?$/iu.test(entry.name)) - .map((entry) => join(root, entry.name)) - .sort((left, right) => left.localeCompare(right)); -} - -async function loadSourceAgentSpecs(repoRoot: string): Promise<{ - specs: NormalizedAgentRuntimeSpec[]; - diagnostics: AgentSpecDiagnostic[]; -}> { - const contentRoots = await discoverAgentContentRoots(repoRoot); - const registeredHandlers = await listRegisteredAgentHandlers({ tenantRoot: repoRoot }); - const diagnostics: AgentSpecDiagnostic[] = []; - const specs: NormalizedAgentRuntimeSpec[] = []; - - for (const contentRoot of contentRoots) { - for (const sourcePath of await listAgentSourceFiles(contentRoot)) { - const source = await readFile(sourcePath, 'utf8'); - const parsed = parseAgentSource(source); - const rawSpec: RawAgentRuntimeSpec = { - ...parsed.frontmatter, - body: parsed.body, - id: relative(contentRoot, sourcePath).replace(/\\/gu, '/').replace(/\.mdx?$/iu, ''), - }; - const result = normalizeAgentRuntimeSpec(rawSpec, { - registeredHandlers: registeredHandlers as NormalizedAgentRuntimeSpec['handler'][], - messageTypes: [...AGENT_MESSAGE_TYPES], - }); - diagnostics.push(...result.diagnostics); - if (result.spec) specs.push(result.spec); - } - } - - return { specs, diagnostics }; -} - -async function validateAgent(agent: NormalizedAgentRuntimeSpec, knownHandlers: string[], repoRoot: string) { - const issues: AgentContractIssue[] = []; - if (!knownHandlers.includes(agent.handler)) { - issues.push(issue(agent, 'handler', `Handler "${agent.handler}" is not registered.`)); - } else { - await resolveAgentHandler(agent.handler, { tenantRoot: repoRoot }).catch((error) => { - issues.push(issue(agent, 'handler', error instanceof Error ? error.message : String(error))); - }); - } - if (!PROFILE_HANDLERS.has(agent.handler)) { - issues.push(issue(agent, 'handler', 'First-party agent specs must use writer, actor, estimate, releaser, or reporter.')); - } - if (!agent.projectAgentClassId?.trim()) { - issues.push(issue(agent, 'projectAgentClassId', 'Agents must declare the project agent class used for capacity allocation.')); - } - if (!agent.projectAgentClassSlug?.trim()) { - issues.push(issue(agent, 'projectAgentClassSlug', 'Agents must declare a project agent class slug.')); - } - const messageTriggers = agent.triggers.filter((trigger) => trigger.type === 'message'); - if (messageTriggers.length > 0) { - for (const [index, trigger] of messageTriggers.entries()) { - if (!trigger.messageTypes?.length) { - issues.push(issue(agent, `triggers.message[${index}].messageTypes`, 'Message triggers must declare at least one message type.')); - } - for (const messageType of trigger.messageTypes ?? []) { - if (!(AGENT_MESSAGE_TYPES as readonly string[]).includes(messageType)) { - issues.push(issue(agent, `triggers.message[${index}].messageTypes`, `Unknown trigger message type "${messageType}".`)); - } - } - } - } - for (const messageType of agent.outputs.messageTypes ?? []) { - if (!(AGENT_MESSAGE_TYPES as readonly string[]).includes(messageType)) { - issues.push(issue(agent, 'outputs.messageTypes', `Unknown output message type "${messageType}".`)); - } - } - if (agent.execution.sandboxMode === 'workspace_write') { - if (!agent.execution.allowedPaths?.length) { - issues.push(issue(agent, 'execution.allowedPaths', 'Write-capable agents must declare allowed paths.')); - } - if (!agent.execution.forbiddenPaths?.length) { - issues.push(issue(agent, 'execution.forbiddenPaths', 'Write-capable agents must declare forbidden paths.')); - } - if (agent.slug.includes('docs') && agent.execution.allowedPaths?.includes('**')) { - issues.push(issue(agent, 'execution.allowedPaths', 'Docs automation agents should avoid broad "**" write paths.', 'warning')); - } - } - if (agent.execution.provider && !EXECUTION_PROVIDERS.has(agent.execution.provider)) { - issues.push(issue(agent, 'execution.provider', `Unsupported execution provider "${agent.execution.provider}".`)); - } - if (agent.execution.providerProfile && !agent.execution.providerProfile.requiredCapabilities?.length) { - issues.push(issue(agent, 'execution.providerProfile.requiredCapabilities', 'Provider profiles must declare required execution-provider capabilities when present.')); - } - for (const [index, query] of (agent.context?.queries ?? []).entries()) { - if (!query.id?.trim()) { - issues.push(issue(agent, `context.queries[${index}].id`, 'Context queries must declare an id.')); - } - if (!query.query?.trim()) { - issues.push(issue(agent, `context.queries[${index}].query`, 'Context queries must declare a query.')); - } - if (typeof query.budget !== 'number' || query.budget <= 0) { - issues.push(issue(agent, `context.queries[${index}].budget`, 'Context queries must declare a positive budget.')); - } - const compiled = compileDeclarativeContextQuery(query, { defaultLimit: 8, maxDepth: 3 }); - for (const error of compiled.errors) { - issues.push(issue(agent, `context.queries[${index}]`, error)); - } - } - return issues; -} - -function renderReport(result: Omit) { - const lines = [ - '# Agent Contract Test Report', - '', - `Generated: ${result.generatedAt}`, - `Repository: ${result.repoRoot}`, - `Status: ${result.ok ? 'PASS' : 'FAIL'}`, - '', - ]; - for (const agent of result.agents) { - lines.push( - `## ${agent.slug}`, - '', - `Handler: ${agent.handler}`, - `Project agent class: ${agent.projectAgentClassId}`, - `Enabled: ${agent.enabled}`, - `Triggers: ${agent.triggers.length ? agent.triggers.join(', ') : 'none'}`, - `Declared outputs: ${agent.outputMessageTypes.length ? agent.outputMessageTypes.join(', ') : 'none'}`, - `Permissions: ${agent.permissions.length ? agent.permissions.join('; ') : 'none'}`, - `Status: ${agent.status}`, - ); - if (agent.issues.length) { - lines.push('Issues:'); - for (const entry of agent.issues) { - lines.push(`- ${entry.severity.toUpperCase()} ${entry.field}: ${entry.message}`); - } - } else { - lines.push('Issues: none'); - } - lines.push(''); - } - return `${lines.join('\n')}\n`; -} - -export async function runAgentContractChecks(options: { - repoRoot?: string; - reportPath?: string; - now?: Date; -} = {}): Promise { - const repoRoot = resolve(options.repoRoot ?? process.cwd()); - const generatedAt = (options.now ?? new Date()).toISOString(); - const loaded = await loadSourceAgentSpecs(repoRoot); - const knownHandlers = await listRegisteredAgentHandlers({ tenantRoot: repoRoot }); - const slugCounts = new Map(); - for (const spec of loaded.specs) { - slugCounts.set(spec.slug, (slugCounts.get(spec.slug) ?? 0) + 1); - } - const normalizationIssues: AgentContractIssue[] = loaded.diagnostics.map((diagnostic) => ({ - severity: diagnostic.severity, - slug: diagnostic.slug ?? 'unknown', - field: diagnostic.field, - message: diagnostic.message, - })); - const agents = []; - for (const spec of loaded.specs) { - const issues = await validateAgent(spec, knownHandlers, repoRoot); - if ((slugCounts.get(spec.slug) ?? 0) > 1) { - issues.push(issue(spec, 'slug', `Duplicate agent slug "${spec.slug}".`)); - } - issues.push(...normalizationIssues.filter((entry) => entry.slug === spec.slug)); - agents.push({ - slug: spec.slug, - handler: spec.handler, - projectAgentClassId: spec.projectAgentClassId, - enabled: spec.enabled, - triggers: spec.triggers.map((trigger) => trigger.type), - outputMessageTypes: spec.outputs.messageTypes ?? [], - permissions: spec.permissions.map(permissionKey), - status: issues.some((entry) => entry.severity === 'error') ? 'FAIL' as const : issues.length ? 'WARN' as const : 'PASS' as const, - issues, - }); - } - const issues = [ - ...normalizationIssues.filter((entry) => !loaded.specs.some((spec) => spec.slug === entry.slug)), - ...agents.flatMap((agent) => agent.issues), - ]; - const resultWithoutPath = { - ok: !issues.some((entry) => entry.severity === 'error'), - repoRoot, - generatedAt, - agents, - issues, - }; - const reportPath = resolve(options.reportPath ?? resolve(repoRoot, '.treeseed/test-reports/agent-contracts.md')); - const jsonPath = reportPath.replace(/\.md$/u, '.json'); - await mkdir(dirname(reportPath), { recursive: true }); - await writeFile(reportPath, renderReport(resultWithoutPath), 'utf8'); - await writeFile(jsonPath, `${JSON.stringify(resultWithoutPath, null, 2)}\n`, 'utf8'); - return { - ...resultWithoutPath, - reportPath, - jsonPath, - }; -} diff --git a/src/agents/testing/agent-test-catalog.ts b/src/agents/testing/agent-test-catalog.ts deleted file mode 100644 index 030e790f..00000000 --- a/src/agents/testing/agent-test-catalog.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { existsSync, readdirSync, readFileSync } from 'node:fs'; -import { mkdir, writeFile } from 'node:fs/promises'; -import { dirname, join, resolve } from 'node:path'; -import { parse as parseYaml } from 'yaml'; -import { resolveWorkspaceReportPath } from '../../services/report-paths.ts'; - -export interface AgentTestCatalogEntry { - id: string; - agent: string; - kind: string; - fixture: string | null; - trigger: Record; - expect: Record; - sourcePath: string; - status: 'PASS' | 'FAIL'; - issues: string[]; -} - -export interface AgentTestCatalogResult { - ok: boolean; - generatedAt: string; - repoRoot: string; - entries: AgentTestCatalogEntry[]; - reportPath: string; - jsonPath: string; -} - -function frontmatter(source: string) { - const match = source.match(/^---\n([\s\S]*?)\n---/u); - return match ? parseYaml(match[1]) as Record : {}; -} - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function renderCatalog(result: Omit) { - const lines = [ - '# Agent Test Catalog', - '', - `Generated: ${result.generatedAt}`, - `Repository: ${result.repoRoot}`, - `Status: ${result.ok ? 'PASS' : 'FAIL'}`, - '', - ]; - for (const entry of result.entries) { - lines.push( - `## ${entry.id}`, - '', - `Agent: ${entry.agent}`, - `Kind: ${entry.kind}`, - `Fixture: ${entry.fixture ?? 'none'}`, - `Source: ${entry.sourcePath}`, - `Status: ${entry.status}`, - ); - if (entry.issues.length) { - lines.push('Issues:', ...entry.issues.map((issue) => `- ${issue}`)); - } else { - lines.push('Issues: none'); - } - lines.push(''); - } - return `${lines.join('\n')}\n`; -} - -export async function runAgentTestCatalogChecks(options: { - repoRoot?: string; - reportPath?: string; - now?: Date; -} = {}): Promise { - const repoRoot = resolve(options.repoRoot ?? process.cwd()); - const root = resolve(repoRoot, 'src/content/agent-tests'); - const files = existsSync(root) - ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.mdx?$/iu.test(entry.name)) - : []; - const entries = files.map((file) => { - const sourcePath = join(root, file.name); - const data = frontmatter(readFileSync(sourcePath, 'utf8')); - const id = typeof data.id === 'string' ? data.id : file.name.replace(/\.mdx?$/iu, ''); - const agent = typeof data.agent === 'string' ? data.agent : ''; - const kind = typeof data.kind === 'string' ? data.kind : ''; - const fixture = typeof data.fixture === 'string' ? data.fixture : null; - const trigger = record(data.trigger); - const expect = record(data.expect); - const issues = [ - !agent ? 'agent is required' : '', - !kind ? 'kind is required' : '', - fixture && !existsSync(resolve(repoRoot, fixture)) ? `fixture path does not exist: ${fixture}` : '', - kind === 'workday' && Object.keys(trigger).length === 0 ? 'workday trigger contract is required' : '', - kind === 'workday' && Object.keys(expect).length === 0 ? 'workday expectation contract is required' : '', - ].filter(Boolean); - return { - id, - agent, - kind, - fixture, - trigger, - expect, - sourcePath, - status: issues.length ? 'FAIL' as const : 'PASS' as const, - issues, - }; - }); - const resultWithoutPaths = { - ok: entries.every((entry) => entry.status === 'PASS'), - generatedAt: (options.now ?? new Date()).toISOString(), - repoRoot, - entries, - }; - const reportPath = resolveWorkspaceReportPath(options.reportPath ?? '.treeseed/test-reports/agent-test-catalog.md'); - const jsonPath = resolveWorkspaceReportPath(reportPath.replace(/\.md$/u, '.json')); - await mkdir(dirname(reportPath), { recursive: true }); - await writeFile(reportPath, renderCatalog(resultWithoutPaths), 'utf8'); - await writeFile(jsonPath, `${JSON.stringify(resultWithoutPaths, null, 2)}\n`, 'utf8'); - return { - ...resultWithoutPaths, - reportPath, - jsonPath, - }; -} diff --git a/src/agents/testing/e2e-harness.ts b/src/agents/testing/e2e-harness.ts deleted file mode 100644 index 56b4cfd3..00000000 --- a/src/agents/testing/e2e-harness.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './e2e-harness/exec-file-async.ts'; -export * from './e2e-harness/patch-fixture-agent-specs.ts'; -export * from './e2e-harness/create-agent-test-runtime.ts'; diff --git a/src/agents/testing/e2e-harness/create-agent-test-runtime.ts b/src/agents/testing/e2e-harness/create-agent-test-runtime.ts deleted file mode 100644 index f187f5f0..00000000 --- a/src/agents/testing/e2e-harness/create-agent-test-runtime.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { existsSync } from 'node:fs'; -import { cp, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import type { AgentSdk } from '@treeseed/sdk/sdk'; -import { MemoryAgentDatabase } from '@treeseed/sdk/d1-store'; -import { resolveModelDefinition } from '@treeseed/sdk/models'; -import { type SdkMessageEntity } from '@treeseed/sdk/types'; -import { runFromRecord } from '@treeseed/sdk/stores/run-store'; -import type { ExecutionProviderAdapter, AgentMutationAdapter } from "../../runtime/runtime-types.ts"; -import type { AgentKernel } from "../../kernel/agents/agent-kernel.ts"; -import { AgentTestRuntime, createKnowledgeDocument, createObjectiveDocument, createQuestionDocument, initializeSandboxRepo, migrateDatabase, patchFixtureAgentSpecs, transpileFixtureAgentHandlers } from './patch-fixture-agent-specs.ts'; -import { createFixtureTreeDxFetch, linkWorkspaceNodeModules, resolveDocsRoot, resolveSharedNodeModules, runCommand } from './exec-file-async.ts'; - -export async function createAgentTestRuntime(options?: { - execution?: ExecutionProviderAdapter; - mutations?: AgentMutationAdapter; - executionMode?: 'codex' | 'copilot'; - databaseMode?: 'memory' | 'local-d1'; -}) : Promise { - const rootDir = await mkdtemp(path.join(os.tmpdir(), 'agents-e2e-')); - const repoRoot = path.join(rootDir, 'docs'); - const persistTo = path.join(rootDir, '.wrangler-state'); - const docsRoot = resolveDocsRoot(); - const previousContentRoot = process.env.TREESEED_AGENT_CONTENT_ROOT; - const previousExecutionMode = process.env.TREESEED_AGENT_EXECUTION_PROVIDER; - const previousTenantRoot = process.env.TREESEED_TENANT_ROOT; - const previousCwd = process.cwd(); - const sharedNodeModules = resolveSharedNodeModules(previousCwd); - - await cp(docsRoot, repoRoot, { - recursive: true, - filter(source) { - const relativePath = path.relative(docsRoot, source); - if (!relativePath) { - return true; - } - return ![ - '.wrangler', - '.agent-worktrees', - 'node_modules', - 'dist', - '.astro', - 'coverage', - ].some((prefix) => relativePath === prefix || relativePath.startsWith(`${prefix}${path.sep}`)); - }, - }); - if (existsSync(sharedNodeModules)) { - await linkWorkspaceNodeModules(sharedNodeModules, repoRoot, previousCwd); - } - await transpileFixtureAgentHandlers(repoRoot); - await patchFixtureAgentSpecs(repoRoot); - - process.env.TREESEED_AGENT_CONTENT_ROOT = path.join(repoRoot, 'src', 'content'); - process.env.TREESEED_AGENT_EXECUTION_PROVIDER = options?.executionMode ?? 'codex'; - process.env.TREESEED_TENANT_ROOT = repoRoot; - process.chdir(repoRoot); - - await mkdir(persistTo, { recursive: true }); - await initializeSandboxRepo(repoRoot); - const treeDxFetch = await createFixtureTreeDxFetch(repoRoot); - const [{ AgentKernel }, { AgentSdk }] = await Promise.all([ - import('../../kernel/agents/agent-kernel.ts'), - import('@treeseed/sdk/sdk'), - ]); - const sdk = - options?.databaseMode === 'local-d1' - ? (await migrateDatabase(repoRoot, persistTo), AgentSdk.createLocal({ - repoRoot, - databaseName: 'docs-site-data', - persistTo, - })) - : new AgentSdk({ - repoRoot, - database: new MemoryAgentDatabase(), - treeDx: { - baseUrl: 'https://treedx.fixture.test', - token: 'fixture-treedx-token', - repoId: 'fixture-repo', - ref: 'refs/heads/main', - fetchImpl: treeDxFetch, - }, - }); - const kernel = new AgentKernel(sdk, repoRoot, { - execution: options?.execution, - mutations: options?.mutations, - }); - - async function writeSeedFile(relativePath: string, source: string, message: string) { - const filePath = path.join(repoRoot, relativePath); - await mkdir(path.dirname(filePath), { recursive: true }); - await writeFile(filePath, source, 'utf8'); - await runCommand('git', ['add', relativePath], repoRoot); - await runCommand('git', ['commit', '-m', message], repoRoot); - } - - return { - rootDir, - repoRoot, - persistTo, - sdk, - kernel, - async seedObjectives(entries) { - for (const entry of entries) { - await writeSeedFile( - path.join('src', 'content', 'objectives', `${entry.slug}.mdx`), - createObjectiveDocument(entry.slug, entry.date ?? '2099-01-01T00:00:00.000Z'), - `test(seed): objective ${entry.slug}`, - ); - } - }, - async seedQuestions(entries) { - for (const entry of entries) { - await writeSeedFile( - path.join('src', 'content', 'questions', `${entry.slug}.mdx`), - createQuestionDocument( - entry.slug, - entry.date ?? '2099-01-01T00:00:00.000Z', - entry.relatedObjectives ?? [], - ), - `test(seed): question ${entry.slug}`, - ); - } - }, - async seedKnowledge(entries) { - for (const entry of entries) { - await writeSeedFile( - path.join('src', 'content', 'knowledge', `${entry.slug}.md`), - createKnowledgeDocument(entry.slug, entry.title ?? `Knowledge ${entry.slug}`), - `test(seed): knowledge ${entry.slug}`, - ); - } - }, - async seedMessages(entries) { - const messages = []; - for (const entry of entries) { - const created = await sdk.createMessage({ - ...entry, - actor: 'agents-e2e', - }); - messages.push(created.payload); - } - return messages; - }, - async clearModelContent(model) { - const definition = resolveModelDefinition(model); - if (!definition.contentDir) { - throw new Error(`Model ${model} is not content-backed.`); - } - const relativeContentDir = path.relative(repoRoot, definition.contentDir); - await rm(definition.contentDir, { recursive: true, force: true }); - await mkdir(definition.contentDir, { recursive: true }); - await runCommand('git', ['add', '-A', relativeContentDir], repoRoot); - await runCommand('git', ['commit', '-m', `test(seed): clear ${model}`], repoRoot); - }, - async readMessages() { - const response = await sdk.search({ - model: 'message', - sort: [{ field: 'created_at', direction: 'asc' }], - limit: 100, - }); - return response.payload as SdkMessageEntity[]; - }, - async readRunLogs() { - const database = sdk.database as { - db?: { prepare: (query: string) => { all: () => Promise<{ results: T[] }> } }; - inspectRuns?: () => Record[]; - }; - if (database.inspectRuns) { - return database.inspectRuns().map((row) => runFromRecord(row)); - } - return []; - }, - async readContentLeases() { - const database = sdk.database as { - db?: { prepare: (query: string) => { all: () => Promise<{ results: T[] }> } }; - inspectLeases?: () => Record[]; - }; - if (database.inspectLeases) { - return database.inspectLeases(); - } - if (!database.db) { - return []; - } - const rows = await database.db.prepare('SELECT * FROM lease_state ORDER BY item_key ASC').all>(); - return rows.results; - }, - async claimMessage(messageTypes, workerId = 'agents-e2e-claimer') { - const claimed = await sdk.claimMessage({ - workerId, - messageTypes, - leaseSeconds: 300, - }); - return claimed.payload; - }, - async claimObjectiveLease(itemKey, workerId = 'agents-e2e-lease-holder') { - return sdk.database.tryClaimContentLease({ - model: 'objective', - itemKey, - claimedBy: workerId, - leaseSeconds: 300, - }); - }, - async cleanup() { - if (previousContentRoot === undefined) { - delete process.env.TREESEED_AGENT_CONTENT_ROOT; - } else { - process.env.TREESEED_AGENT_CONTENT_ROOT = previousContentRoot; - } - if (previousExecutionMode === undefined) { - delete process.env.TREESEED_AGENT_EXECUTION_PROVIDER; - } else { - process.env.TREESEED_AGENT_EXECUTION_PROVIDER = previousExecutionMode; - } - if (previousTenantRoot === undefined) { - delete process.env.TREESEED_TENANT_ROOT; - } else { - process.env.TREESEED_TENANT_ROOT = previousTenantRoot; - } - process.chdir(previousCwd); - await rm(rootDir, { recursive: true, force: true }); - }, - }; -} diff --git a/src/agents/testing/e2e-harness/exec-file-async.ts b/src/agents/testing/e2e-harness/exec-file-async.ts deleted file mode 100644 index 935273fc..00000000 --- a/src/agents/testing/e2e-harness/exec-file-async.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { execFile } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { access, cp, mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import { promisify } from 'node:util'; -import { createRequire } from 'node:module'; -import { parseFrontmatterDocument } from '@treeseed/sdk/frontmatter'; - - -export const execFileAsync = promisify(execFile); - -export const require = createRequire(import.meta.url); - -export function nowIso() { - return new Date().toISOString(); -} - -export function resolveDocsRoot() { - if (process.env.TREESEED_AGENT_FIXTURE_ROOT) { - return path.resolve(process.env.TREESEED_AGENT_FIXTURE_ROOT); - } - - const cwd = process.cwd(); - const workspaceSdkPackageRoot = path.resolve(cwd, '../sdk'); - const installedSdkPackageRoot = path.resolve(path.dirname(require.resolve('@treeseed/sdk/platform/tenant-config')), '../..'); - const candidates: string[] = []; - let current = cwd; - while (true) { - candidates.push( - path.resolve(current, '.fixtures', 'treeseed-fixtures', 'sites', 'working-site'), - path.resolve(current, 'fixture'), - path.resolve(current, 'fixtures', 'sites', 'working-site'), - ); - const parent = path.resolve(current, '..'); - if (parent === current) { - break; - } - current = parent; - } - candidates.push( - path.resolve(workspaceSdkPackageRoot, '.fixtures', 'treeseed-fixtures', 'sites', 'working-site'), - path.resolve(workspaceSdkPackageRoot, 'fixture'), - path.resolve(installedSdkPackageRoot, '.fixtures', 'treeseed-fixtures', 'sites', 'working-site'), - path.resolve(installedSdkPackageRoot, 'fixture'), - ); - - for (const candidate of candidates) { - if (existsSync(path.join(candidate, 'src', 'manifest.yaml'))) { - return candidate; - } - } - - throw new Error( - `Unable to resolve an agent smoke fixture root. Checked: ${candidates.join(', ')}`, - ); -} - -export function resolveSharedNodeModules(startDir: string) { - const requiredPackages = ['@treeseed/sdk']; - const checked: string[] = []; - let current = startDir; - - while (true) { - const candidate = path.join(current, 'node_modules'); - checked.push(candidate); - if ( - existsSync(candidate) - && requiredPackages.every((packageName) => - existsSync(path.join(candidate, ...packageName.split('/')))) - ) { - return candidate; - } - - const parent = path.resolve(current, '..'); - if (parent === current) { - break; - } - current = parent; - } - - throw new Error( - `Unable to resolve a shared node_modules directory containing ${requiredPackages.join(', ')}. Checked: ${checked.join(', ')}`, - ); -} - -export async function resolveWranglerBin() { - if (process.env.TREESEED_AGENT_WRANGLER_BIN) { - return path.resolve(process.env.TREESEED_AGENT_WRANGLER_BIN); - } - - try { - const wranglerPackageRoot = path.resolve(path.dirname(require.resolve('wrangler/package.json'))); - const packageJson = JSON.parse(await readFile(path.join(wranglerPackageRoot, 'package.json'), 'utf8')); - const relativeBin = typeof packageJson.bin === 'string' ? packageJson.bin : packageJson.bin?.wrangler; - if (!relativeBin) { - throw new Error('Unable to resolve wrangler binary path from package.json.'); - } - return path.resolve(wranglerPackageRoot, relativeBin); - } catch { - const packageLocal = path.resolve(resolveDocsRoot(), 'node_modules', '.bin', 'wrangler'); - await access(packageLocal); - return packageLocal; - } -} - -export async function runCommand(command: string, args: string[], cwd: string) { - await execFileAsync(command, args, { - cwd, - env: process.env, - maxBuffer: 10 * 1024 * 1024, - }); -} - -export async function linkWorkspaceNodeModules(sharedNodeModules: string, repoRoot: string, localAgentPackageRoot: string) { - const targetRoot = path.join(repoRoot, 'node_modules'); - await mkdir(targetRoot, { recursive: true }); - - const entries = await readdir(sharedNodeModules, { withFileTypes: true }).catch(() => []); - for (const entry of entries) { - if (entry.name === '@treeseed') { - const scopedSource = path.join(sharedNodeModules, entry.name); - const scopedTarget = path.join(targetRoot, entry.name); - await mkdir(scopedTarget, { recursive: true }); - const scopedEntries = await readdir(scopedSource, { withFileTypes: true }).catch(() => []); - for (const scopedEntry of scopedEntries) { - const sourcePath = path.join(scopedSource, scopedEntry.name); - const targetPath = path.join(scopedTarget, scopedEntry.name); - if (scopedEntry.name === 'agent') { - continue; - } - await symlink(sourcePath, targetPath, scopedEntry.isDirectory() ? 'dir' : 'file').catch(() => undefined); - } - continue; - } - - const sourcePath = path.join(sharedNodeModules, entry.name); - const targetPath = path.join(targetRoot, entry.name); - await symlink(sourcePath, targetPath, entry.isDirectory() ? 'dir' : 'file').catch(() => undefined); - } - - const installedAgentRoot = path.join(targetRoot, '@treeseed', 'agent'); - await rm(installedAgentRoot, { recursive: true, force: true }); - await mkdir(installedAgentRoot, { recursive: true }); - await cp(path.join(localAgentPackageRoot, 'dist'), path.join(installedAgentRoot, 'dist'), { recursive: true }); - await writeFile( - path.join(installedAgentRoot, 'package.json'), - JSON.stringify({ - name: '@treeseed/agent', - type: 'module', - exports: { - '.': './dist/index.js', - './runtime-types': './dist/agents/runtime-types.js', - './contracts/messages': './dist/agents/contracts/messages.js', - './contracts/run': './dist/agents/contracts/run.js', - }, - }, null, 2), - 'utf8', - ); -} - -export async function walkFiles(root: string): Promise { - const entries = await readdir(root, { withFileTypes: true }).catch(() => []); - const nested = await Promise.all( - entries.map(async (entry) => { - const fullPath = path.join(root, entry.name); - if (entry.isDirectory()) { - return walkFiles(fullPath); - } - return [fullPath]; - }), - ); - return nested.flat(); -} - -export function jsonResponse(payload: unknown, status = 200) { - return new Response(JSON.stringify(payload), { - status, - headers: { 'content-type': 'application/json' }, - }); -} - -export function treeDxPatternMatches(pattern: string, candidate: string) { - const normalizedPattern = pattern.replace(/\\/g, '/').replace(/^\/+/u, ''); - const normalizedCandidate = candidate.replace(/\\/g, '/').replace(/^\/+/u, ''); - const contentRelativePattern = normalizedPattern.includes('/src/content/') - ? normalizedPattern.slice(normalizedPattern.lastIndexOf('/src/content/') + '/src/content/'.length) - : normalizedPattern; - const contentRelativeCandidate = normalizedCandidate.includes('/src/content/') - ? normalizedCandidate.slice(normalizedCandidate.lastIndexOf('/src/content/') + '/src/content/'.length) - : normalizedCandidate; - if (contentRelativePattern.endsWith('/**')) { - const prefix = contentRelativePattern.slice(0, -3); - return contentRelativeCandidate.startsWith(prefix) || normalizedCandidate.includes(`/src/content/${prefix}`); - } - return contentRelativePattern === contentRelativeCandidate - || contentRelativeCandidate.startsWith(`${contentRelativePattern}/`) - || normalizedCandidate.endsWith(`/src/content/${contentRelativePattern}`); -} - -export async function readTreeDxRequest(init?: RequestInit) { - if (!init?.body) return {}; - if (typeof init.body === 'string') return JSON.parse(init.body) as Record; - if (init.body instanceof Uint8Array) return JSON.parse(Buffer.from(init.body).toString('utf8')) as Record; - return {}; -} - -export async function createFixtureTreeDxFetch(repoRoot: string): Promise { - return async (input, init) => { - const url = new URL(String(input)); - const body = await readTreeDxRequest(init); - const paths = Array.isArray(body.paths) - ? body.paths.map(String) - : typeof body.path === 'string' - ? [body.path] - : ['src/content/**']; - const files = (await walkFiles(repoRoot)) - .map((filePath) => path.relative(repoRoot, filePath).replace(/\\/g, '/')) - .filter((filePath) => /\.(md|mdx)$/iu.test(filePath)) - .filter((filePath) => paths.some((pattern) => treeDxPatternMatches(pattern, filePath))) - .sort(); - - if (url.pathname.endsWith('/paths/list')) { - return jsonResponse({ - ok: true, - entries: files.map((filePath) => ({ path: filePath, kind: 'file' })), - }); - } - - if (url.pathname.endsWith('/files/read')) { - const requested = Array.isArray(body.paths) - ? body.paths.map(String) - : typeof body.path === 'string' - ? [body.path] - : []; - const readable = requested.length ? requested : files; - const result = await Promise.all(readable.map(async (filePath) => { - const content = await readFile(path.join(repoRoot, filePath), 'utf8'); - const parsed = parseFrontmatterDocument(content); - return { - path: filePath, - content, - frontmatter: parsed.frontmatter, - body: parsed.body, - }; - })); - return jsonResponse({ ok: true, file: result[0] ?? null, files: result }); - } - - if (url.pathname.endsWith('/files/search')) { - const limit = typeof body.limit === 'number' ? body.limit : files.length; - const result = await Promise.all(files.slice(0, limit).map(async (filePath) => { - const content = await readFile(path.join(repoRoot, filePath), 'utf8'); - const parsed = parseFrontmatterDocument(content); - return { - path: filePath, - content, - frontmatter: parsed.frontmatter, - body: parsed.body, - }; - })); - return jsonResponse({ ok: true, results: result }); - } - - return jsonResponse({ - ok: false, - error: { - code: 'unhandled_fixture_treedx_route', - message: `Unhandled fixture TreeDX route ${url.pathname}`, - }, - }, 404); - }; -} diff --git a/src/agents/testing/e2e-harness/patch-fixture-agent-specs.ts b/src/agents/testing/e2e-harness/patch-fixture-agent-specs.ts deleted file mode 100644 index 3990a545..00000000 --- a/src/agents/testing/e2e-harness/patch-fixture-agent-specs.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { readFile, readdir, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import ts from 'typescript'; -import type { AgentSdk } from '@treeseed/sdk/sdk'; -import { serializeFrontmatterDocument } from '@treeseed/sdk/frontmatter'; -import { type SdkCreateMessageRequest, type SdkMessageEntity, type SdkRunEntity } from '@treeseed/sdk/types'; -import type { AgentKernel } from "../../kernel/agents/agent-kernel.ts"; -import { nowIso, resolveWranglerBin, runCommand } from './exec-file-async.ts'; - -export async function patchFixtureAgentSpecs(repoRoot: string) { - const readTools = [ - 'treedx.build_context', - 'treedx.read_repository_files', - 'treedx.search_workspace', - 'treedx.read_workspace_file', - 'treeseed.status', - ]; - const writeTools = [ - ...readTools, - 'treedx.apply_workspace_changeset', - 'treedx.commit_workspace', - ]; - const engineeringTools = [ - ...writeTools, - 'treeseed.dev_plan', - 'treeseed.changed_paths', - 'treeseed.verify', - ]; - const updates = new Map([ - ['architect.mdx', { permissionLine: ' operations: [pick, update, create]', tools: engineeringTools }], - ['engineer.mdx', { permissionLine: ' operations: [pick, update, create]', tools: engineeringTools }], - ['reporter.mdx', { tools: readTools }], - ['technical-writer.mdx', { tools: readTools }], - ['releaser.mdx', { permissionLine: ' operations: [pick, update, get, create]', tools: ['treedx.build_context', 'treedx.search_workspace', 'treedx.read_workspace_file', 'treeseed.status', 'treeseed.changed_paths', 'treeseed.verify'] }], - ['researcher.mdx', { permissionLine: ' operations: [pick, update, create]', tools: readTools }], - ['reviewer.mdx', { permissionLine: ' operations: [pick, update, get, create]', tools: ['treedx.build_context', 'treedx.search_workspace', 'treedx.read_workspace_file', 'treeseed.status', 'treeseed.changed_paths', 'treeseed.verify'] }], - ]); - - for (const [filename, update] of updates) { - const filePath = path.join(repoRoot, 'src', 'content', 'agents', filename); - const source = await readFile(filePath, 'utf8').catch(() => null); - if (!source) { - continue; - } - let next = update.permissionLine - ? source.replace( - /(\n - model: message\n) operations: \[[^\]]+\]/, - `$1${update.permissionLine}`, - ) - : source; - if (!/^tools:\n\s+allowed:/m.test(next)) { - const toolsBlock = `tools:\n allowed:\n${update.tools.map((tool) => ` - ${tool}`).join('\n')}\n`; - next = next.replace(/^outputs:\n/m, `${toolsBlock}outputs:\n`); - } - if (next !== source) { - await writeFile(filePath, next, 'utf8'); - } - } -} - -export async function transpileFixtureAgentHandlers(repoRoot: string) { - const agentsRoot = path.join(repoRoot, 'src', 'agents'); - const agentFiles = (await readdir(agentsRoot, { withFileTypes: true }).catch(() => [])) - .filter((entry) => entry.isFile() && entry.name.endsWith('.ts')) - .map((entry) => entry.name); - - for (const filename of agentFiles) { - const sourcePath = path.join(agentsRoot, filename); - const outputPath = path.join(agentsRoot, filename.replace(/\.ts$/u, '.js')); - const source = await readFile(sourcePath, 'utf8'); - const transformed = ts.transpileModule(source, { - compilerOptions: { - module: ts.ModuleKind.ESNext, - target: ts.ScriptTarget.ES2022, - }, - }).outputText.replace(/(['"`])(\.[^'"`\n]+)\.ts\1/g, '$1$2.js$1'); - await writeFile(outputPath, transformed, 'utf8'); - } -} - -export async function migrateDatabase(repoRoot: string, persistTo: string) { - const wrangler = await resolveWranglerBin(); - for (const migration of [ - '0001_subscribers.sql', - '0002_agent_runtime.sql', - '0003_agent_run_trace.sql', - '0025_agent_runtime_state.sql', - ]) { - await runCommand( - wrangler, - [ - 'd1', - 'execute', - 'docs-site-data', - '--local', - '--persist-to', - persistTo, - '--file', - path.join(repoRoot, 'migrations', migration), - ], - repoRoot, - ); - } -} - -export async function initializeSandboxRepo(repoRoot: string) { - await runCommand('git', ['init', '-b', 'main'], repoRoot); - await runCommand('git', ['config', 'user.email', 'agents-e2e@example.test'], repoRoot); - await runCommand('git', ['config', 'user.name', 'Agents E2E'], repoRoot); - await runCommand('git', ['add', '.'], repoRoot); - await runCommand('git', ['commit', '-m', 'test: baseline sandbox'], repoRoot); -} - -export function createObjectiveDocument(slug: string, date: string) { - return serializeFrontmatterDocument( - { - title: `Objective ${slug}`, - description: `Objective ${slug} description`, - date, - status: 'planned', - tags: ['agent', 'e2e'], - summary: `Summary for ${slug}`, - draft: false, - timeHorizon: 'near-term', - motivation: `Motivation for ${slug}`, - primaryContributor: 'architect', - relatedQuestions: [], - relatedBooks: [], - }, - `# Objective ${slug}\n`, - ); -} - -export function createQuestionDocument(slug: string, date: string, relatedObjectives: string[] = []) { - return serializeFrontmatterDocument( - { - title: `Question ${slug}`, - description: `Question ${slug} description`, - date, - status: 'planned', - tags: ['agent', 'e2e'], - summary: `Summary for ${slug}`, - draft: false, - questionType: 'implementation', - motivation: `Motivation for ${slug}`, - primaryContributor: 'architect', - relatedObjectives, - relatedBooks: [], - }, - `# Question ${slug}\n`, - ); -} - -export function createKnowledgeDocument(slug: string, title: string) { - return serializeFrontmatterDocument( - { - title, - slug, - updated: nowIso(), - tags: ['agent', 'e2e'], - }, - `# ${title}\n`, - ); -} - -export interface AgentTestRuntime { - rootDir: string; - repoRoot: string; - persistTo: string; - sdk: AgentSdk; - kernel: AgentKernel; - seedObjectives(entries: Array<{ slug: string; date?: string }>): Promise; - seedQuestions(entries: Array<{ slug: string; date?: string; relatedObjectives?: string[] }>): Promise; - seedKnowledge(entries: Array<{ slug: string; title?: string }>): Promise; - seedMessages(entries: Array>): Promise; - clearModelContent(model: 'objective' | 'question' | 'knowledge'): Promise; - readMessages(): Promise; - readRunLogs(): Promise; - readContentLeases(): Promise[]>; - claimMessage(messageTypes: string[], workerId?: string): Promise; - claimObjectiveLease(itemKey: string, workerId?: string): Promise; - cleanup(): Promise; -} diff --git a/src/agents/testing/engineering-agent-test-definition.ts b/src/agents/testing/engineering-agent-test-definition.ts deleted file mode 100644 index 030cbb60..00000000 --- a/src/agents/testing/engineering-agent-test-definition.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { AgentTestCatalogEntry } from './agent-test-catalog.ts'; -import { - validateEngineeringWorkflowPromotionConfig, - type EngineeringAssignmentGraphRoles, - type EngineeringWorkflowPromotionConfigV1, -} from '@treeseed/sdk/agent-capacity'; - -export const ENGINEERING_TEST_FIRST_SEQUENCE = [ - 'approved-decision-and-readiness', - 'exact-ref-worktree', - 'research-and-architecture-when-required', - 'failing-test', - 'integration-ref-captured', - 'implementation-without-test-mutation', - 'verification', - 'review-and-revision', - 'documentation', - 'release-readiness', - 'operations-runner-handoff', -] as const; - -export interface EngineeringAgentTestDefinition { - id: string; - sourcePath: string; - fixturePath: string; - coordinatorAgent: string; - workflowKind: 'engineering-test-first'; - objectiveId: string; - approvedDecisionId: string; - exactBaseRef: string; - requireRevisionCycle: boolean; - requiredAgents: string[]; - requiredSequence: string[]; - assertions: string[]; -} - -export class EngineeringAgentTestDefinitionError extends Error { - readonly code = 'engineering_agent_test_definition_invalid'; - - constructor(readonly issues: string[]) { - super(`Invalid engineering agent test definition: ${issues.join('; ')}`); - this.name = 'EngineeringAgentTestDefinitionError'; - } -} - -function text(value: unknown) { - return typeof value === 'string' && value.trim() ? value.trim() : null; -} - -function stringList(value: unknown, field: string, issues: string[]) { - if (!Array.isArray(value) || value.length === 0) { - issues.push(`${field} must be a non-empty string array`); - return []; - } - const result = value.map(text).filter((entry): entry is string => Boolean(entry)); - if (result.length !== value.length) issues.push(`${field} may contain only non-empty strings`); - if (new Set(result).size !== result.length) issues.push(`${field} may not contain duplicates`); - return result; -} - -export function compileEngineeringAgentTestDefinition(entry: AgentTestCatalogEntry): EngineeringAgentTestDefinition { - const issues: string[] = [...entry.issues]; - if (entry.status !== 'PASS') issues.push('catalog entry must pass base validation'); - if (entry.kind !== 'workday') issues.push('kind must be workday'); - if (!entry.fixture) issues.push('fixture is required'); - const workflowKind = text(entry.trigger.workflowKind); - if (workflowKind !== 'engineering-test-first') issues.push('trigger.workflowKind must be engineering-test-first'); - const objectiveId = text(entry.trigger.objective); - if (!objectiveId) issues.push('trigger.objective is required'); - const approvedDecisionId = text(entry.trigger.approvedDecision); - if (!approvedDecisionId) issues.push('trigger.approvedDecision is required'); - const exactBaseRef = text(entry.trigger.exactBaseRef); - if (!exactBaseRef) issues.push('trigger.exactBaseRef is required'); - if (typeof entry.trigger.requireRevisionCycle !== 'boolean') issues.push('trigger.requireRevisionCycle must be boolean'); - const requiredAgents = stringList(entry.expect.requiredAgents, 'expect.requiredAgents', issues); - const requiredSequence = stringList(entry.expect.requiredSequence, 'expect.requiredSequence', issues); - const assertions = stringList(entry.expect.assertions, 'expect.assertions', issues); - const missingStages = ENGINEERING_TEST_FIRST_SEQUENCE.filter((stage) => !requiredSequence.includes(stage)); - if (missingStages.length) issues.push(`expect.requiredSequence is missing canonical stages: ${missingStages.join(', ')}`); - if (issues.length) throw new EngineeringAgentTestDefinitionError([...new Set(issues)]); - return { - id: entry.id, - sourcePath: entry.sourcePath, - fixturePath: entry.fixture as string, - coordinatorAgent: entry.agent, - workflowKind: 'engineering-test-first', - objectiveId: objectiveId as string, - approvedDecisionId: approvedDecisionId as string, - exactBaseRef: exactBaseRef as string, - requireRevisionCycle: entry.trigger.requireRevisionCycle as boolean, - requiredAgents, - requiredSequence, - assertions, - }; -} - -export function compileEngineeringWorkflowPromotionConfig( - definition: EngineeringAgentTestDefinition, - input: { - projectId: string; - resolvedExactBaseRef: string; - roles: EngineeringAssignmentGraphRoles; - }, -): EngineeringWorkflowPromotionConfigV1 { - const config: EngineeringWorkflowPromotionConfigV1 = { - schemaVersion: 1, - id: definition.id, - projectId: input.projectId, - decisionId: definition.approvedDecisionId, - objectiveId: definition.objectiveId, - exactBaseRef: input.resolvedExactBaseRef, - roles: input.roles, - includeResearch: Boolean(input.roles.researcher), - includeArchitecture: Boolean(input.roles.architect), - requireLinkedProposal: true, - metadata: { - agentTestSourcePath: definition.sourcePath, - fixturePath: definition.fixturePath, - requireRevisionCycle: definition.requireRevisionCycle, - requiredSequence: definition.requiredSequence, - }, - }; - const validation = validateEngineeringWorkflowPromotionConfig(config); - if (!validation.ok) throw new EngineeringAgentTestDefinitionError(validation.diagnostics.map((entry) => `${entry.path}: ${entry.message}`)); - return config; -} diff --git a/src/agents/testing/message-chain.ts b/src/agents/testing/message-chain.ts deleted file mode 100644 index 3d2e1b2a..00000000 --- a/src/agents/testing/message-chain.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { mkdir, writeFile } from 'node:fs/promises'; -import { dirname } from 'node:path'; -import { resolveWorkspaceReportPath } from '../../services/report-paths.ts'; - -export interface MessageChainStep { - agent: string; - inputMessageType: string; - outputMessageTypes: string[]; -} - -export interface MessageChainResult { - id: string; - ok: boolean; - steps: MessageChainStep[]; - messagesEmitted: string[]; - artifactsCreated: string[]; - approvalsCreated: string[]; - mutationsAttempted: string[]; - assertions: string[]; - store: { - tasks: Array>; - messages: Array>; - artifacts: Array>; - approvals: Array>; - mutations: Array>; - }; -} - -export function runInMemoryMessageChain(input: { - id: string; - initialMessageType: string; - steps: MessageChainStep[]; - artifactsCreated?: string[]; - approvalsCreated?: string[]; - mutationsAttempted?: string[]; - requireApprovalBeforeMutation?: boolean; -}): MessageChainResult { - const store = { - tasks: [] as Array>, - messages: [{ - id: `${input.id}:message:0`, - type: input.initialMessageType, - status: 'completed', - payloadJson: JSON.stringify({ chainId: input.id }), - }], - artifacts: [] as Array>, - approvals: [] as Array>, - mutations: [] as Array>, - }; - for (const [index, step] of input.steps.entries()) { - store.tasks.push({ - id: `${input.id}:task:${index + 1}`, - agentSlug: step.agent, - inputMessageType: step.inputMessageType, - status: 'completed', - }); - for (const type of step.outputMessageTypes) { - store.messages.push({ - id: `${input.id}:message:${store.messages.length}`, - type, - status: 'pending', - payloadJson: JSON.stringify({ - chainId: input.id, - sourceMessageType: step.inputMessageType, - sourceTaskId: `${input.id}:task:${index + 1}`, - }), - }); - } - } - for (const kind of input.artifactsCreated ?? []) { - store.artifacts.push({ id: `${input.id}:artifact:${kind}`, kind, sourceMessageId: store.messages.at(-1)?.id ?? null }); - } - for (const kind of input.approvalsCreated ?? []) { - store.approvals.push({ id: `${input.id}:approval:${kind}`, kind, status: kind.includes('release') ? 'pending' : 'approved' }); - } - for (const path of input.mutationsAttempted ?? []) { - store.mutations.push({ - id: `${input.id}:mutation:${store.mutations.length + 1}`, - path, - approvalId: store.approvals[0]?.id ?? null, - verified: true, - }); - } - const messagesEmitted = store.messages.slice(1).map((message) => String(message.type)); - const assertions = [ - input.steps.every((step, index) => index === 0 || input.steps[index - 1]?.outputMessageTypes.includes(step.inputMessageType)) - ? 'message order matches declared chain' - : 'message order does not match declared chain', - store.artifacts.every((artifact) => artifact.sourceMessageId) - ? 'artifacts retain source references' - : 'artifact source references missing', - input.requireApprovalBeforeMutation - ? (store.mutations.every((mutation) => mutation.approvalId) ? 'mutation approval gate satisfied' : 'mutation approval gate missing') - : 'no mutation approval required', - ]; - return { - id: input.id, - ok: assertions.every((entry) => !entry.includes('does not') && !entry.includes('missing')), - steps: input.steps, - messagesEmitted, - artifactsCreated: store.artifacts.map((artifact) => String(artifact.kind)), - approvalsCreated: store.approvals.map((approval) => String(approval.kind)), - mutationsAttempted: store.mutations.map((mutation) => String(mutation.path)), - assertions, - store, - }; -} - -export function researchToKnowledgeNoMutationChain() { - return runInMemoryMessageChain({ - id: 'research-to-knowledge-no-mutation', - initialMessageType: 'knowledge_gap_detected', - steps: [ - { agent: 'architect', inputMessageType: 'knowledge_gap_detected', outputMessageTypes: ['research_task_requested'] }, - { agent: 'researcher', inputMessageType: 'research_task_requested', outputMessageTypes: ['research_note_created'] }, - { agent: 'technical-writer', inputMessageType: 'research_note_created', outputMessageTypes: ['knowledge_draft_created'] }, - { agent: 'reviewer', inputMessageType: 'knowledge_draft_created', outputMessageTypes: ['knowledge_optimization_completed', 'promotion_request_created'] }, - { agent: 'treeseed-governance-steward', inputMessageType: 'promotion_request_created', outputMessageTypes: ['approval_request_created'] }, - ], - artifactsCreated: ['research_note', 'knowledge_draft', 'optimization_report'], - approvalsCreated: ['promotion_request'], - mutationsAttempted: [], - }); -} - -export function governedMutationApprovalChain() { - return runInMemoryMessageChain({ - id: 'governed-mutation-approval', - initialMessageType: 'approval_request_approved', - steps: [ - { agent: 'engineer', inputMessageType: 'approval_request_approved', outputMessageTypes: ['docs_mutation_completed'] }, - { agent: 'reviewer', inputMessageType: 'docs_mutation_completed', outputMessageTypes: ['review_passed'] }, - { agent: 'reporter', inputMessageType: 'review_passed', outputMessageTypes: ['workday_report_created'] }, - { agent: 'releaser', inputMessageType: 'workday_report_created', outputMessageTypes: ['release_waiting_for_approval'] }, - ], - artifactsCreated: ['docs_mutation_snapshot', 'workday_report', 'release_readiness_summary'], - approvalsCreated: ['release_approval_request'], - mutationsAttempted: ['docs/**'], - requireApprovalBeforeMutation: true, - }); -} - -export function forbiddenPathFailureChain() { - return runInMemoryMessageChain({ - id: 'forbidden-path-failure', - initialMessageType: 'approval_request_approved', - steps: [ - { agent: 'engineer', inputMessageType: 'approval_request_approved', outputMessageTypes: ['docs_mutation_failed'] }, - { agent: 'reporter', inputMessageType: 'docs_mutation_failed', outputMessageTypes: ['workday_report_created'] }, - ], - artifactsCreated: ['verification_failure_snapshot'], - approvalsCreated: ['docs_mutation_approval'], - mutationsAttempted: ['package.json'], - requireApprovalBeforeMutation: true, - }); -} - -function renderMessageChainReport(result: { - ok: boolean; - generatedAt: string; - chains: MessageChainResult[]; -}) { - const lines = [ - '# Message Chain Report', - '', - `Generated: ${result.generatedAt}`, - `Status: ${result.ok ? 'PASS' : 'FAIL'}`, - '', - ]; - for (const chain of result.chains) { - lines.push( - `## ${chain.id}`, - '', - `Status: ${chain.ok ? 'PASS' : 'FAIL'}`, - `Messages: ${chain.messagesEmitted.join(', ') || 'none'}`, - `Artifacts: ${chain.artifactsCreated.join(', ') || 'none'}`, - `Approvals: ${chain.approvalsCreated.join(', ') || 'none'}`, - `Mutations attempted: ${chain.mutationsAttempted.join(', ') || 'none'}`, - `Assertions: ${chain.assertions.join('; ') || 'none'}`, - '', - ); - for (const step of chain.steps) { - lines.push(`- ${step.inputMessageType} -> ${step.agent} -> ${step.outputMessageTypes.join(', ')}`); - } - lines.push(''); - } - return `${lines.join('\n')}\n`; -} - -export async function runMessageChainSuite(input: { - chains?: MessageChainResult[]; - reportPath?: string; - now?: Date; -} = {}) { - const chains = input.chains ?? [ - researchToKnowledgeNoMutationChain(), - governedMutationApprovalChain(), - forbiddenPathFailureChain(), - ]; - const resultWithoutPaths = { - ok: chains.every((chain) => chain.ok) - && chains[0]?.mutationsAttempted.length === 0 - && chains[1]?.mutationsAttempted.every((path) => path.startsWith('docs/')) - && chains[2]?.messagesEmitted.includes('docs_mutation_failed'), - generatedAt: (input.now ?? new Date()).toISOString(), - chains, - }; - const reportPath = resolveWorkspaceReportPath(input.reportPath ?? '.treeseed/test-reports/message-chains.md'); - const jsonPath = resolveWorkspaceReportPath(reportPath.replace(/\.md$/u, '.json')); - await mkdir(dirname(reportPath), { recursive: true }); - await writeFile(reportPath, renderMessageChainReport(resultWithoutPaths), 'utf8'); - await writeFile(jsonPath, `${JSON.stringify(resultWithoutPaths, null, 2)}\n`, 'utf8'); - return { - ...resultWithoutPaths, - reportPath, - jsonPath, - }; -} diff --git a/src/agents/testing/research-agent-test-definition.ts b/src/agents/testing/research-agent-test-definition.ts deleted file mode 100644 index 89b443b9..00000000 --- a/src/agents/testing/research-agent-test-definition.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { AgentTestCatalogEntry } from './agent-test-catalog.ts'; - -export const RESEARCH_CITATION_REVIEW_SEQUENCE = [ - 'question-decomposition', - 'source-selection-criteria', - 'governed-source-search', - 'independent-source-fetch', - 'linked-evidence-notes', - 'claim-synthesis', - 'citation-review-rejection', - 'revision', - 'citation-review-approval', - 'cited-knowledge-publication', - 'workday-report', -] as const; - -export interface ResearchAgentTestDefinition { - id: string; - sourcePath: string; - fixturePath: string; - coordinatorAgent: string; - workflowKind: 'research-citation-review'; - questionId: string; - sourcePolicyId: string; - minimumIndependentSources: number; - maxRevisionCycles: number; - requireUnsupportedClaimRevision: boolean; - finalArtifactModel: 'knowledge'; - requiredAgents: string[]; - requiredArtifacts: string[]; - requiredSequence: string[]; - assertions: string[]; -} - -export class ResearchAgentTestDefinitionError extends Error { - readonly code = 'research_agent_test_definition_invalid'; - - constructor(readonly issues: string[]) { - super(`Invalid research agent test definition: ${issues.join('; ')}`); - this.name = 'ResearchAgentTestDefinitionError'; - } -} - -function text(value: unknown) { - return typeof value === 'string' && value.trim() ? value.trim() : null; -} - -function stringList(value: unknown, field: string, issues: string[]) { - if (!Array.isArray(value) || value.length === 0) { - issues.push(`${field} must be a non-empty string array`); - return []; - } - const result = value.map(text).filter((entry): entry is string => Boolean(entry)); - if (result.length !== value.length) issues.push(`${field} may contain only non-empty strings`); - if (new Set(result).size !== result.length) issues.push(`${field} may not contain duplicates`); - return result; -} - -export function compileResearchAgentTestDefinition(entry: AgentTestCatalogEntry): ResearchAgentTestDefinition { - const issues = [...entry.issues]; - if (entry.status !== 'PASS') issues.push('catalog entry must pass base validation'); - if (entry.kind !== 'workday') issues.push('kind must be workday'); - if (!entry.fixture) issues.push('fixture is required'); - if (text(entry.trigger.workflowKind) !== 'research-citation-review') issues.push('trigger.workflowKind must be research-citation-review'); - const questionId = text(entry.trigger.question); - if (!questionId) issues.push('trigger.question is required'); - const sourcePolicyId = text(entry.trigger.sourcePolicy); - if (!sourcePolicyId) issues.push('trigger.sourcePolicy is required'); - const minimumIndependentSources = Number(entry.trigger.minimumIndependentSources); - if (!Number.isInteger(minimumIndependentSources) || minimumIndependentSources < 2) issues.push('trigger.minimumIndependentSources must be an integer of at least two'); - const maxRevisionCycles = Number(entry.trigger.maxRevisionCycles); - if (!Number.isInteger(maxRevisionCycles) || maxRevisionCycles < 1 || maxRevisionCycles > 10) issues.push('trigger.maxRevisionCycles must be an integer from 1 through 10'); - if (entry.trigger.requireUnsupportedClaimRevision !== true) issues.push('trigger.requireUnsupportedClaimRevision must be true'); - if (entry.trigger.finalArtifactModel !== 'knowledge') issues.push('trigger.finalArtifactModel must be knowledge'); - const requiredAgents = stringList(entry.expect.requiredAgents, 'expect.requiredAgents', issues); - const requiredArtifacts = stringList(entry.expect.requiredArtifacts, 'expect.requiredArtifacts', issues); - const requiredSequence = stringList(entry.expect.requiredSequence, 'expect.requiredSequence', issues); - const assertions = stringList(entry.expect.assertions, 'expect.assertions', issues); - const missingStages = RESEARCH_CITATION_REVIEW_SEQUENCE.filter((stage) => !requiredSequence.includes(stage)); - if (missingStages.length) issues.push(`expect.requiredSequence is missing canonical stages: ${missingStages.join(', ')}`); - for (const role of ['researcher', 'reviewer', 'technical-writer', 'reporter']) { - if (!requiredAgents.includes(role)) issues.push(`expect.requiredAgents is missing ${role}`); - } - if (issues.length) throw new ResearchAgentTestDefinitionError([...new Set(issues)]); - return { - id: entry.id, - sourcePath: entry.sourcePath, - fixturePath: entry.fixture as string, - coordinatorAgent: entry.agent, - workflowKind: 'research-citation-review', - questionId: questionId as string, - sourcePolicyId: sourcePolicyId as string, - minimumIndependentSources, - maxRevisionCycles, - requireUnsupportedClaimRevision: true, - finalArtifactModel: 'knowledge', - requiredAgents, - requiredArtifacts, - requiredSequence, - assertions, - }; -} diff --git a/src/agents/tools/agent-tool-completion.ts b/src/agents/tools/agent-tool-completion.ts deleted file mode 100644 index 46009aa0..00000000 --- a/src/agents/tools/agent-tool-completion.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { readFile } from 'node:fs/promises'; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export async function readAgentToolTelemetry(path: string | null | undefined) { - if (!path) return []; - const source = await readFile(path, 'utf8').catch(() => ''); - return source.split(/\r?\n/u).filter(Boolean).flatMap((line) => { - try { return [JSON.parse(line) as Record]; } catch { return []; } - }); -} - -export function contentModelForArtifactKind(artifactKind: string) { - if (artifactKind === 'planning_question') return 'question'; - if (artifactKind === 'planning_proposal') return 'proposal'; - if (artifactKind === 'discussion_response') return 'discussion_message'; - if (artifactKind === 'knowledge_page' || artifactKind === 'knowledge_update') return 'knowledge'; - return 'note'; -} - -export function unlinkedNotePaths(telemetry: Record[]) { - const noteRelations = new Map(); - for (const entry of telemetry) { - if (entry.status !== 'completed' || !Array.isArray(entry.derivedEvents)) continue; - for (const rawEvent of entry.derivedEvents) { - const event = record(rawEvent); - if (event.type !== 'content_created' && event.type !== 'content_updated') continue; - const contentRef = record(event.contentRef); - const model = String(contentRef.model ?? '').trim().toLowerCase().replace(/s$/u, ''); - const path = String(contentRef.path ?? contentRef.contentPath ?? '').trim(); - if (model !== 'note' || !path) continue; - const linked = Boolean(String(contentRef.subjectId ?? '').trim() && String(contentRef.subjectField ?? '').trim()); - noteRelations.set(path, linked || noteRelations.get(path) === true); - } - } - return [...noteRelations].flatMap(([path, linked]) => linked ? [] : [path]); -} - -export function hasCompatibleContentArtifact(telemetry: Record[], artifactKind: string) { - const expectedModel = contentModelForArtifactKind(artifactKind); - return telemetry.some((entry) => entry.status === 'completed' && Array.isArray(entry.derivedEvents) - && entry.derivedEvents.some((rawEvent) => { - const event = record(rawEvent); - if (event.type !== 'content_created' && event.type !== 'content_updated') return false; - const contentRef = record(event.contentRef); - return String(contentRef.model ?? '').trim().toLowerCase().replace(/s$/u, '') === expectedModel; - })); -} - -export function missingPrecommitContentReceipts(telemetry: Record[], artifactKind: string) { - return [ - ...unlinkedNotePaths(telemetry).map((path) => `content_subject_linked:${path}`), - ...(hasCompatibleContentArtifact(telemetry, artifactKind) ? [] : [`content_artifact_kind:${artifactKind}`]), - ...missingOperationalCloseoutReceipts(telemetry), - ]; -} - -export function missingOperationalCloseoutReceipts(telemetry: Record[]) { - const completedInputs = (toolId: string) => telemetry.filter((entry) => entry.status === 'completed' && entry.toolId === toolId) - .map((entry) => record(entry.inputSummary)); - const terminal = new Set(['completed', 'failed', 'cancelled', 'suspended']); - const terminalStatus = completedInputs('treeseed.assignment_status_update').some((input) => terminal.has(String(input.status ?? ''))); - const terminalSummary = completedInputs('treeseed.assignment_summary').some((input) => input.action === 'write' && terminal.has(String(input.status ?? ''))); - return [ - ...(terminalStatus ? [] : ['assignment_terminal_status']), - ...(terminalSummary ? [] : ['assignment_summary']), - ]; -} - -export function missingCommunicationReadReceipts(telemetry: Record[]) { - const completed = new Set(telemetry - .filter((entry) => entry.status === 'completed') - .map((entry) => String(entry.toolId ?? ''))); - return [ - ...(completed.has('treeseed.discussion.read') ? [] : ['discussion_read']), - ...(completed.has('treeseed.discussion.follow') ? [] : ['discussion_follow']), - ]; -} diff --git a/src/agents/tools/agent-tool-copilot.ts b/src/agents/tools/agent-tool-copilot.ts deleted file mode 100644 index b407e837..00000000 --- a/src/agents/tools/agent-tool-copilot.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { CopilotTool } from '@treeseed/sdk/copilot'; -import type { ExecutionProviderToolDescriptor } from '../runtime/runtime-types.ts'; -import { agentToolMcpName } from './agent-tool-mcp-server.ts'; -import type { AgentToolRuntimeOptions } from './agent-tool-runtime.ts'; -import { callAgentToolWithTelemetry } from './agent-tool-telemetry.ts'; - -export function agentToolCopilotName(toolId: string) { - return agentToolMcpName(toolId); -} - -export function createCopilotAgentTools(options: AgentToolRuntimeOptions): CopilotTool[] { - return options.descriptors.map((descriptor): CopilotTool => { - const metadata = descriptor.metadata && typeof descriptor.metadata === 'object' && !Array.isArray(descriptor.metadata) - ? descriptor.metadata as Record - : {}; - const contentDetail = descriptor.executionTarget === 'treeseed_content' - ? ` Content action: ${String(metadata.contentAction ?? 'unknown')}; model: ${String(metadata.contentModel ?? 'generic')}; writes are staged unless an explicit commit content tool is available.` - : ''; - return { - name: agentToolCopilotName(descriptor.id), - description: `TreeSeed tool ${descriptor.id}. ${descriptor.description}${contentDetail}`, - parameters: descriptor.inputSchema, - skipPermission: true, - handler: async (input) => { - const result = await callAgentToolWithTelemetry(options, descriptor.id, input && typeof input === 'object' && !Array.isArray(input) ? input as Record : {}); - const ok = !(result && typeof result === 'object' && 'ok' in result && result.ok === false); - const message = result && typeof result === 'object' && 'message' in result && typeof result.message === 'string' - ? result.message - : JSON.stringify(result); - return { - textResultForLlm: JSON.stringify(result), - resultType: ok ? 'success' : 'failure', - error: ok ? undefined : message, - toolTelemetry: { - toolId: descriptor.id, - callName: agentToolCopilotName(descriptor.id), - executionTarget: descriptor.executionTarget, - mutability: descriptor.mutability, - }, - }; - }, - }; - }); -} diff --git a/src/agents/tools/agent-tool-mcp-server.ts b/src/agents/tools/agent-tool-mcp-server.ts deleted file mode 100644 index 55ded17c..00000000 --- a/src/agents/tools/agent-tool-mcp-server.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; -import { createRequire } from 'node:module'; -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import type { ExecutionProviderToolDescriptor } from '../runtime/runtime-types.ts'; -import type { AgentToolRuntimeOptions } from './agent-tool-runtime.ts'; -import { callAgentToolWithTelemetry } from './agent-tool-telemetry.ts'; - -export interface AgentToolMcpServerOptions extends AgentToolRuntimeOptions {} - -export function agentToolMcpName(toolId: string) { - return toolId.replace(/[^A-Za-z0-9_-]/gu, '_'); -} - -const TRANSPORT_METADATA_KEYS = [ - 'dispatch', - 'dispatchPreferredMode', - 'telemetryCategory', - 'assignmentId', - 'projectId', - 'contentRoot', - 'worktreeRoot', - 'allowedPaths', - 'forbiddenPaths', - 'researchAllowedDomains', - 'contentAction', - 'baseCommitSha', - 'baseRef', - 'contentModel', - 'contentPreset', - 'permissionSummary', - 'requiredArtifactKind', - 'requireContentArtifact', -] as const; - -export function sanitizeAgentToolDescriptorForTransport(descriptor: ExecutionProviderToolDescriptor): ExecutionProviderToolDescriptor { - const source = descriptor.metadata && typeof descriptor.metadata === 'object' && !Array.isArray(descriptor.metadata) - ? descriptor.metadata - : {}; - const metadata = Object.fromEntries(TRANSPORT_METADATA_KEYS.flatMap((key) => source[key] === undefined ? [] : [[key, source[key]]])); - return { - ...descriptor, - ...(Object.keys(metadata).length ? { metadata } : { metadata: undefined }), - }; -} - -export function createAgentToolMcpServerCommand(options: AgentToolMcpServerOptions): { - command: string; - args: string[]; - env: Record; -} { - const modulePath = fileURLToPath(import.meta.url); - const require = createRequire(import.meta.url); - const args = modulePath.endsWith('.ts') - ? ['--import', require.resolve('tsx'), modulePath] - : [modulePath]; - return { - command: process.execPath, - args, - env: { - TREESEED_API_BASE_URL: options.apiBaseUrl, - TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN: options.providerAccessToken, - TREESEED_AGENT_TOOL_ASSIGNMENT_ID: options.assignmentId, - TREESEED_AGENT_TOOL_LEASE_TOKEN: options.leaseToken ?? '', - TREESEED_AGENT_TOOL_REPO_ROOT: options.repoRoot ?? '', - TREESEED_AGENT_TOOL_TELEMETRY_PATH: options.telemetryPath ?? '', - TREESEED_AGENT_TOOL_DESCRIPTORS: JSON.stringify(options.descriptors.map(sanitizeAgentToolDescriptorForTransport)), - TREESEED_AGENT_TOOL_RESEARCH_SOURCE_POLICY: options.researchSourcePolicy ? JSON.stringify(options.researchSourcePolicy) : '', - }, - }; -} - -function descriptorsFromEnv(): ExecutionProviderToolDescriptor[] { - try { - const parsed = JSON.parse(process.env.TREESEED_AGENT_TOOL_DESCRIPTORS ?? '[]'); - return Array.isArray(parsed) ? parsed as ExecutionProviderToolDescriptor[] : []; - } catch { - return []; - } -} - -function researchSourcePolicyFromEnv() { - const value = process.env.TREESEED_AGENT_TOOL_RESEARCH_SOURCE_POLICY?.trim(); - if (!value) return undefined; - return JSON.parse(value) as AgentToolRuntimeOptions['researchSourcePolicy']; -} - -export function createAgentToolMcpServer(options?: Partial) { - const descriptors = options?.descriptors ?? descriptorsFromEnv(); - const apiBaseUrl = options?.apiBaseUrl ?? process.env.TREESEED_API_BASE_URL ?? ''; - const providerAccessToken = options?.providerAccessToken ?? process.env.TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN ?? process.env.TREESEED_PROVIDER_ACCESS_TOKEN ?? ''; - const assignmentId = options?.assignmentId ?? process.env.TREESEED_AGENT_TOOL_ASSIGNMENT_ID ?? ''; - const leaseToken = options?.leaseToken ?? process.env.TREESEED_AGENT_TOOL_LEASE_TOKEN ?? null; - const repoRoot = options?.repoRoot ?? process.env.TREESEED_AGENT_TOOL_REPO_ROOT ?? undefined; - const telemetryPath = options?.telemetryPath ?? process.env.TREESEED_AGENT_TOOL_TELEMETRY_PATH ?? null; - const researchSourcePolicy = options?.researchSourcePolicy ?? researchSourcePolicyFromEnv(); - - const server = new Server( - { name: 'treeseed-tools', version: '1.0.0' }, - { - capabilities: { tools: {} }, - instructions: 'Assignment-scoped TreeSeed tools for a single provider assignment.', - }, - ); - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: descriptors.map((descriptor) => ({ - name: agentToolMcpName(descriptor.id), - description: `TreeSeed tool ${descriptor.id}. ${descriptor.description}`, - inputSchema: descriptor.inputSchema as { - type: 'object'; - properties?: Record; - required?: string[]; - }, - })), - })); - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const requestedName = request.params.name; - const matchingDescriptor = descriptors.find((descriptor) => - descriptor.id === requestedName || agentToolMcpName(descriptor.id) === requestedName, - ); - const toolId = matchingDescriptor?.id ?? requestedName; - const input = request.params.arguments ?? {}; - try { - const result = await callAgentToolWithTelemetry({ - apiBaseUrl, - providerAccessToken, - assignmentId, - leaseToken, - descriptors, - sdk: options?.sdk, - fetchImpl: options?.fetchImpl, - repoRoot, - telemetryPath, - researchSourcePolicy, - onTelemetry: options?.onTelemetry, - }, toolId, input); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result) }], - structuredContent: result, - isError: result && typeof result === 'object' && 'ok' in result && result.ok === false ? true : undefined, - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }], - isError: true, - }; - } - }); - return server; -} - -export async function startAgentToolMcpServer(options?: Partial) { - const configPath = process.env.TREESEED_AGENT_TOOL_CONFIG_PATH?.trim(); - if (configPath) { - const parsed = JSON.parse(await readFile(configPath, 'utf8')) as Record; - for (const [key, value] of Object.entries(parsed)) { - if (typeof value === 'string') process.env[key] = value; - } - } - const server = createAgentToolMcpServer(options); - await server.connect(new StdioServerTransport()); -} - -if (process.argv[1] === fileURLToPath(import.meta.url)) { - void startAgentToolMcpServer(); -} diff --git a/src/agents/tools/agent-tool-runtime.ts b/src/agents/tools/agent-tool-runtime.ts deleted file mode 100644 index 99dbafba..00000000 --- a/src/agents/tools/agent-tool-runtime.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { execFile } from 'node:child_process'; -import { readFile, realpath, stat } from 'node:fs/promises'; -import { isAbsolute, relative, resolve } from 'node:path'; -import { promisify } from 'node:util'; -import type { SdkDispatchConfig, SdkDispatchPolicy, SdkDispatchResult } from '@treeseed/sdk'; -import { findAgentToolDefinition } from '@treeseed/sdk'; -import { checkpointAgentWorktree } from '@treeseed/sdk/operations/agent-tools'; -import { AgentSdk } from '@treeseed/sdk/sdk'; -import { parseFrontmatterDocument } from '@treeseed/sdk/frontmatter'; -import type { ExecutionProviderToolDescriptor, TreeDxProxyExecutionToolDescriptor } from '../runtime/runtime-types.ts'; -import { callTreeDxProxyTool } from './treedx-proxy-client.ts'; -import type { TreeDxProxyToolName } from './treedx-proxy-tool.ts'; -import { validateAgentToolInput } from './agent-tool-schema.ts'; -import { readAssignmentStatus } from './status/assignment-status-tool.ts'; -import { matchesScopedPath } from './path-scope/path-matcher.ts'; -import { readAssignmentActivity } from './status/assignment-activity-tool.ts'; -import { callAssignmentOperationalContentTool,hasAssignmentPlan } from './assignment/operational-content-tool.ts'; -import { prepareAssignmentOperationHandoff,readAssignmentDiscussion,requestAssignmentClientAction,requestAssignmentDiscussionHandoff } from './assignment/discussion-tool.ts'; -import { runAssignmentDiscussionResponse } from './assignment/communication-response-tool.ts'; -import { callContentTool } from './content-tool-runtime.ts'; -import { fetchGovernedResearchSource, searchGovernedResearchSources } from './governed-research-tools.ts'; -import type { AgentToolRuntimeOptions } from './runtime/agent-tool-runtime-types.ts'; -export type { AgentToolCallTelemetry, AgentToolDerivedEvent, AgentToolRuntimeOptions } from './runtime/agent-tool-runtime-types.ts'; -const execFileAsync = promisify(execFile); - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function text(value: unknown) { - return typeof value === 'string' ? value : ''; -} - -function normalizePath(value: string) { - return value.replace(/\\/gu, '/').replace(/^\.?\//u, '').replace(/\/+/gu, '/'); -} - -function descriptorFor(options: AgentToolRuntimeOptions, toolId: string) { - return options.descriptors.find((descriptor) => descriptor.id === toolId) ?? null; -} - -function structuredError(code: string, message: string, metadata: Record = {}) { - return { ok: false, code, message, metadata }; -} - -function defaultSdk(options: AgentToolRuntimeOptions) { - if (options.sdk) return options.sdk; - const projectId = text(record(options.descriptors[0]?.metadata).projectId); - const dispatch = options.apiBaseUrl && options.providerAccessToken && projectId - ? { - projectId, - marketBaseUrl: options.apiBaseUrl, - policy: 'prefer_local', - credentialSource: { type: 'bearer', token: options.providerAccessToken }, - fetchImpl: options.fetchImpl, - } satisfies SdkDispatchConfig - : undefined; - return AgentSdk.createLocal({ - repoRoot: options.repoRoot ?? process.cwd(), - databaseName: ':memory:', - dispatch, - }); -} - -function dispatchInputFor(toolId: string, input: Record, descriptor: ExecutionProviderToolDescriptor) { - if (toolId === 'treeseed.dev_plan') { - return { ...input, plan: true, json: true }; - } - if (toolId === 'treeseed.status') { - return { ...input, json: true }; - } - return input; -} - -async function callSdkDispatchTool(options: AgentToolRuntimeOptions, descriptor: ExecutionProviderToolDescriptor, input: Record) { - if (descriptor.id === 'treeseed.status' && !options.sdk && options.apiBaseUrl && options.providerAccessToken && options.assignmentId) { - return readAssignmentStatus(options); - } - const definition = findAgentToolDefinition(descriptor.id); - if (!definition?.dispatch) { - return structuredError('dispatch_mapping_missing', `${descriptor.id} does not declare an SDK dispatch mapping.`); - } - const dispatchInput = dispatchInputFor(descriptor.id, input, descriptor); - if (record(dispatchInput).ok === false) return dispatchInput; - const result = await defaultSdk(options).dispatch({ - namespace: definition.dispatch.namespace, - operation: definition.dispatch.operation, - input: dispatchInput, - preferredMode: (typeof descriptor.metadata?.dispatchPreferredMode === 'string' - ? descriptor.metadata.dispatchPreferredMode - : definition.dispatch.assignmentPreferredMode ?? definition.dispatch.preferredMode ?? 'auto') as SdkDispatchPolicy, - }) as SdkDispatchResult; - return { ok: true, payload: result }; -} - -function assertPathScope(descriptor: ExecutionProviderToolDescriptor, path: string) { - const metadata = record(descriptor.metadata); - const allowedPaths = Array.isArray(metadata.allowedPaths) ? metadata.allowedPaths.map(String) : []; - const forbiddenPaths = Array.isArray(metadata.forbiddenPaths) ? metadata.forbiddenPaths.map(String) : []; - if (forbiddenPaths.some((pattern) => matchesScopedPath(path, pattern))) { - return structuredError('path_forbidden', `${path} is forbidden for this assignment.`, { path, forbiddenPaths }); - } - if (allowedPaths.length && !allowedPaths.some((pattern) => matchesScopedPath(path, pattern))) { - return structuredError('path_not_allowed', `${path} is outside the assignment path scope.`, { path, allowedPaths }); - } - return null; -} - -async function scopedRepositoryPath( - options: AgentToolRuntimeOptions, - descriptor: ExecutionProviderToolDescriptor, - path: string, -) { - const normalized = normalizePath(path); - if (!normalized || isAbsolute(path) || normalized === '..' || normalized.startsWith('../')) { - return { error: structuredError('repository_path_invalid', 'Repository paths must be relative and cannot escape the assignment repository.', { path }) }; - } - const scopeError = assertPathScope(descriptor, normalized); - if (scopeError) return { error: scopeError }; - const root = await realpath(options.repoRoot ?? process.cwd()); - const target = await realpath(resolve(root, normalized)).catch(() => null); - if (!target) return { error: structuredError('repository_path_missing', `${normalized} does not exist in the assignment repository.`, { path: normalized }) }; - const targetRelative = relative(root, target).replace(/\\/gu, '/'); - if (!targetRelative || targetRelative === '..' || targetRelative.startsWith('../') || isAbsolute(targetRelative)) { - return { error: structuredError('repository_path_escape', `${normalized} resolves outside the assignment repository.`, { path: normalized }) }; - } - return { root, target, path: targetRelative }; -} - -async function callRepositoryReadFileTool( - options: AgentToolRuntimeOptions, - descriptor: ExecutionProviderToolDescriptor, - input: Record, -) { - const scoped = await scopedRepositoryPath(options, descriptor, text(input.path)); - if ('error' in scoped) return scoped.error; - const maxBytes = Math.max(1, Math.min(262_144, Number(input.maxBytes) || 131_072)); - const details = await stat(scoped.target); - if (!details.isFile()) return structuredError('repository_path_not_file', `${scoped.path} is not a file.`, { path: scoped.path }); - if (details.size > maxBytes) { - return structuredError('repository_file_too_large', `${scoped.path} exceeds the ${maxBytes}-byte assignment read limit.`, { - path: scoped.path, - size: details.size, - maxBytes, - }); - } - return { - ok: true, - payload: { - path: scoped.path, - content: await readFile(scoped.target, 'utf8'), - size: details.size, - truncated: false, - }, - }; -} - -async function callRepositorySearchTool( - options: AgentToolRuntimeOptions, - descriptor: ExecutionProviderToolDescriptor, - input: Record, -) { - const query = text(input.query); - const requestedPaths = Array.isArray(input.paths) ? input.paths.map(String) : []; - const paths = requestedPaths.length ? requestedPaths : ['.']; - for (const path of paths) { - if (path === '.') continue; - const scopeError = assertPathScope(descriptor, path); - if (scopeError) return scopeError; - } - const root = await realpath(options.repoRoot ?? process.cwd()); - const maxResults = Math.max(1, Math.min(200, Number(input.maxResults) || 50)); - try { - const { stdout } = await execFileAsync( - 'git', - ['grep', '-n', '-I', '-F', '-e', query, '--', ...paths], - { cwd: root, maxBuffer: 1_048_576 }, - ); - const matches = stdout.split('\n').filter(Boolean).slice(0, maxResults).map((line) => { - const separator = line.indexOf(':'); - return separator > 0 - ? { path: line.slice(0, separator), match: line.slice(separator + 1) } - : { path: '', match: line }; - }); - for (const match of matches) { - const scopeError = assertPathScope(descriptor, match.path); - if (scopeError) return scopeError; - } - return { ok: true, payload: { query, matches, truncated: stdout.split('\n').filter(Boolean).length > matches.length } }; - } catch (error) { - const exitCode = Number(record(error).code); - if (exitCode === 1) return { ok: true, payload: { query, matches: [], truncated: false } }; - return structuredError('repository_search_failed', error instanceof Error ? error.message : String(error), { query }); - } -} - -async function callChangedPathsTool(descriptor: ExecutionProviderToolDescriptor, input: Record) { - const metadata = record(descriptor.metadata); - const worktreeRoot = text(metadata.worktreeRoot); - if (!worktreeRoot) { - return structuredError('worktree_required', 'treeseed.changed_paths requires an assigned worktree for this assignment.'); - } - const { stdout } = await execFileAsync('git', ['status', '--porcelain'], { cwd: worktreeRoot }); - const changedPaths = stdout.split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => line.slice(3).trim()) - .filter(Boolean); - for (const path of changedPaths) { - const scoped = assertPathScope(descriptor, path); - if (scoped) return scoped; - } - const payload: Record = { changedPaths }; - if (input.includeDiffSummary === true) { - const diff = await execFileAsync('git', ['diff', '--stat'], { cwd: worktreeRoot }).catch((error) => ({ stdout: error instanceof Error ? error.message : String(error) })); - payload.diffSummary = diff.stdout; - } - return { ok: true, payload }; -} - -async function callVerifyTool(descriptor: ExecutionProviderToolDescriptor, input: Record) { - const worktreeRootValue = text(record(descriptor.metadata).worktreeRoot); - if (!worktreeRootValue) { - return structuredError('worktree_required', 'treeseed.verify requires an assigned worktree for this assignment.'); - } - const worktreeRoot = await realpath(worktreeRootValue); - const commands = Array.isArray(input.commands) ? input.commands.map(record) : []; - if (!commands.length || commands.length > 8) { - return structuredError('verification_commands_invalid', 'treeseed.verify requires between one and eight bounded commands.'); - } - const results: Record[] = []; - for (const entry of commands) { - const command = text(entry.command); - const args = Array.isArray(entry.args) ? entry.args.map(String) : []; - if (!['node', 'npm'].includes(command) || args.length > 32) { - return structuredError('verification_command_not_allowed', 'Verification commands are limited to node or npm with at most 32 arguments.', { command }); - } - const requestedCwd = text(entry.cwd) || '.'; - if (isAbsolute(requestedCwd) || normalizePath(requestedCwd) === '..' || normalizePath(requestedCwd).startsWith('../')) { - return structuredError('verification_cwd_invalid', 'Verification cwd must remain inside the assignment worktree.', { cwd: requestedCwd }); - } - const cwd = await realpath(resolve(worktreeRoot, requestedCwd)).catch(() => null); - const cwdRelative = cwd ? relative(worktreeRoot, cwd).replace(/\\/gu, '/') : '..'; - if (!cwd || cwdRelative === '..' || cwdRelative.startsWith('../') || isAbsolute(cwdRelative)) { - return structuredError('verification_cwd_escape', 'Verification cwd resolves outside the assignment worktree.', { cwd: requestedCwd }); - } - const expectedExitCode = Number.isInteger(Number(entry.expectedExitCode)) ? Number(entry.expectedExitCode) : 0; - const timeoutMs = Math.max(1_000, Math.min(300_000, Number(entry.timeoutSeconds || 120) * 1_000)); - const startedAt = Date.now(); - let exitCode = 0; - let stdout = ''; - let stderr = ''; - try { - const output = await execFileAsync(command, args, { cwd, timeout: timeoutMs, maxBuffer: 1_048_576 }); - stdout = output.stdout; - stderr = output.stderr; - } catch (error) { - const details = record(error); - exitCode = Number.isInteger(Number(details.code)) ? Number(details.code) : 1; - stdout = text(details.stdout); - stderr = text(details.stderr) || (error instanceof Error ? error.message : String(error)); - } - results.push({ - command, - args, - cwd: cwdRelative || '.', - exitCode, - expectedExitCode, - ok: exitCode === expectedExitCode, - durationMs: Date.now() - startedAt, - stdout: stdout.slice(0, 131_072), - stderr: stderr.slice(0, 131_072), - }); - if (exitCode !== expectedExitCode) { - return structuredError('verification_exit_code_mismatch', `${command} exited with ${exitCode}; expected ${expectedExitCode}.`, { results }); - } - } - return { ok: true, payload: { reason: text(input.reason) || null, results } }; -} - -export async function callCheckpointTool(options: AgentToolRuntimeOptions, descriptor: ExecutionProviderToolDescriptor, input: Record) { - const metadata = record(descriptor.metadata); - const worktreeRoot = text(metadata.worktreeRoot); - const allowedPaths = Array.isArray(metadata.allowedPaths) ? metadata.allowedPaths.map(String).filter(Boolean) : []; - const forbiddenPaths = Array.isArray(metadata.forbiddenPaths) ? metadata.forbiddenPaths.map(String).filter(Boolean) : []; - const agentSlug = text(metadata.agentSlug) || 'assignment-agent'; - const result = await checkpointAgentWorktree({ - request: { - taskId: options.assignmentId, agentSlug, agentRole: agentSlug, - projectId: text(metadata.projectId), environment: text(metadata.environment) || 'local', - repoRoot: options.repoRoot ?? process.cwd(), worktreeRoot, - allowedPaths, forbiddenPaths, message: text(input.message), input, - }, - grant: { - id: `assignment-checkpoint:${options.assignmentId}`, operations: ['save'], modes: ['mutating'], - projectIds: [text(metadata.projectId)], environments: [text(metadata.environment) || 'local'], - allowedPaths, forbiddenPaths, - }, - }); - return result.status === 'completed' - ? { ok: true, payload: result } - : structuredError(result.error?.code ?? 'operation_checkpoint_failed', result.summary, { result }); -} - -export async function callAgentTool( - options: AgentToolRuntimeOptions, - toolId: string, - input: Record = {}, -) { - const descriptor = descriptorFor(options, toolId); - if (!descriptor) { - return structuredError('tool_not_allowed', `${toolId} is not available for this assignment.`, { toolId }); - } - if (descriptor.kind !== 'agent_tool') { - return structuredError('invalid_tool_descriptor', `${toolId} is not an agent tool descriptor.`); - } - if (!descriptor.executionTarget || !descriptor.mutability || !descriptor.inputSchema) { - return structuredError('invalid_tool_descriptor', `${toolId} has an incomplete tool descriptor.`, { toolId }); - } - const inputValidation = validateAgentToolInput(descriptor.inputSchema, input); - if (!inputValidation.ok) { - return structuredError( - inputValidation.code ?? 'invalid_tool_input', - inputValidation.message ?? `Invalid input for ${toolId}.`, - inputValidation.metadata ?? {}, - ); - } - if (toolId === 'treeseed.assignment_status_update' && Number(input.sequence) > 0 && (!record(input.previousStatusRef).id || !Number.isInteger(Number(record(input.previousStatusRef).revision)))) { - return structuredError('invalid_tool_input','Append-only assignment status updates after sequence zero require an exact previousStatusRef.',{ field:'previousStatusRef',sequence:input.sequence }); - } - if (toolId === 'treeseed.assignment_plan' && input.action === 'write' && (!text(input.objective) || !Array.isArray(input.remaining) || !Number.isInteger(Number(input.expectedStateVersion)) || !text(input.idempotencyKey))) { - return structuredError('invalid_tool_input','Assignment plan writes require objective, remaining work, exact assignment state, and an idempotency key.'); - } - if (toolId === 'treeseed.assignment_summary' && input.action === 'write' && (!text(input.status) || !text(input.summary) || !text(record(input.performance).outcome) || !Number.isInteger(Number(input.expectedStateVersion)) || !text(input.idempotencyKey))) { - return structuredError('invalid_tool_input','Assignment summary writes require outcome status, summary, performance, exact assignment state, and an idempotency key.'); - } - const operationalTool = ['treeseed.assignment_plan','treeseed.assignment_status_update','treeseed.assignment_summary'].includes(toolId); - const mutation = descriptor.mutability !== 'read'; - if (mutation || toolId !== 'treeseed.status') { - const status = record((await readAssignmentStatus(options)).payload); - const expected = Number(input.expectedStateVersion); - if (Number.isInteger(expected) && expected !== Number(status.stateVersion)) return structuredError('assignment_status_stale','Assignment state changed; read treeseed.status before mutation.',{ expectedStateVersion:expected,stateVersion:status.stateVersion }); - const phase = text(record(status.time).phase); - const preparationAllowed=new Set(['treeseed.status','treeseed.assignment_plan','treeseed.discussion.read','treeseed.discussion.follow']); - if(phase==='preparation'&&!preparationAllowed.has(toolId)) return structuredError('assignment_preparation_tool_restricted','Preparation is limited to authoritative status, the mandatory initial plan, and bounded discussion reads. Productive tools unlock only after the plan starts the execution window.',{ phase,remainingSeconds:record(status.time).preparationRemainingSeconds }); - const closeoutAllowed = new Set([ - 'treeseed.assignment_plan','treeseed.assignment_status_update','treeseed.assignment_summary', - 'treeseed.content.validate','treeseed.content.commit','treeseed.publish_signal', - 'treeseed.checkpoint','treeseed.discussion.read','treeseed.discussion.follow','treeseed.discussion.respond','treeseed.discussion.request_handoff','treeseed.operation.prepare_handoff','treeseed.verify','treeseed.changed_paths', - ]); - if(phase==='expired'&&mutation) return structuredError('assignment_closeout_deadline_exhausted','The protected closeout deadline expired; no further assignment mutation is authorized.',{phase,remainingSeconds:0}); - if ((phase === 'closeout' || phase === 'expired') && !closeoutAllowed.has(toolId)) return structuredError('assignment_closeout_tool_restricted','Closeout has begun; only plan/status checkpointing, validation, commit/checkpoint, required publication, summary, and discussion handoff tools remain available.',{ phase,remainingSeconds:record(status.time).remainingSeconds }); - if (mutation && toolId !== 'treeseed.assignment_plan' && !await hasAssignmentPlan(options)) return structuredError('assignment_initial_plan_required','Create the assignment operational plan before any other mutation.'); - } - if (operationalTool) return callAssignmentOperationalContentTool(options,descriptor,input); - if (toolId === 'treeseed.discussion.read' || toolId === 'treeseed.discussion.follow') return readAssignmentDiscussion(options,input); - if (toolId === 'treeseed.discussion.respond') return runAssignmentDiscussionResponse(options,descriptor,input); - if (toolId === 'treeseed.discussion.request_handoff') return requestAssignmentDiscussionHandoff(options,input); - if (toolId === 'treeseed.operation.prepare_handoff') return prepareAssignmentOperationHandoff(options,input); - if (toolId === 'treeseed.client_session.request_action') return requestAssignmentClientAction(options,input); - if (toolId === 'treeseed.discussion.create_artifact') { - const parsed=parseFrontmatterDocument(text(input.content)); - return callContentTool({ - apiBaseUrl:options.apiBaseUrl,providerAccessToken:options.providerAccessToken,assignmentId:options.assignmentId, - descriptor:{ ...descriptor,executionTarget:'treeseed_content',metadata:{ ...descriptor.metadata,contentAction:'create' } }, - input:{ model:input.model,path:input.path,placement:{ path:input.path },title:text(parsed.frontmatter.title),body:parsed.body,fields:parsed.frontmatter },fetchImpl:options.fetchImpl, - }); - } - if (descriptor.executionTarget === 'treedx_proxy') { - const treeDxDescriptor = descriptor as TreeDxProxyExecutionToolDescriptor; - try { - return await callTreeDxProxyTool({ - apiBaseUrl: options.apiBaseUrl, - providerAccessToken: options.providerAccessToken, - assignmentId: options.assignmentId, - handleId: treeDxDescriptor.handleId, - descriptor: treeDxDescriptor, - toolName: toolId as TreeDxProxyToolName, - input, - fetchImpl: options.fetchImpl, - }); - } catch (error) { - return structuredError('treedx_proxy_request_failed', error instanceof Error ? error.message : String(error), { toolId }); - } - } - if (descriptor.executionTarget === 'sdk_dispatch') { - return await callSdkDispatchTool(options, descriptor, input); - } - if (descriptor.executionTarget === 'treeseed_content') { - return await callContentTool({ - apiBaseUrl: options.apiBaseUrl, - providerAccessToken: options.providerAccessToken, - assignmentId: options.assignmentId, - descriptor, - input, - fetchImpl: options.fetchImpl, - }); - } - if (descriptor.id === 'treeseed.changed_paths') { - return await callChangedPathsTool(descriptor, input); - } - if (descriptor.id === 'treeseed.assignment_activity') { - return await readAssignmentActivity(options, input); - } - if (descriptor.id === 'treeseed.verify') { - return await callVerifyTool(descriptor, input); - } - if (descriptor.id === 'treeseed.checkpoint') { - return await callCheckpointTool(options, descriptor, input); - } - if (descriptor.id === 'treeseed.review_decision') { - return { - ok: true, - payload: { - disposition: text(input.disposition), - summary: text(input.summary), - }, - }; - } - if (descriptor.id === 'treeseed.research_claims') { - return { ok: true, payload: { claims: Array.isArray(input.claims) ? input.claims : [] } }; - } - if (descriptor.id === 'treeseed.publish_signal') return { ok: true, payload: { ...input, requested: true } }; - if (descriptor.id === 'treeseed.repository.read_file') { - return await callRepositoryReadFileTool(options, descriptor, input); - } - if (descriptor.id === 'treeseed.repository.search') { - return await callRepositorySearchTool(options, descriptor, input); - } - if (descriptor.id === 'research.search_sources' || descriptor.id === 'research.fetch_source') { - try { - const researchAllowedDomains = Array.isArray(descriptor.metadata?.researchAllowedDomains) - ? descriptor.metadata.researchAllowedDomains.map(String).filter(Boolean) - : undefined; - return descriptor.id === 'research.search_sources' - ? await searchGovernedResearchSources(input, { fetchImpl: options.fetchImpl, policy: options.researchSourcePolicy, allowedDomains: researchAllowedDomains }) - : await fetchGovernedResearchSource(input, { fetchImpl: options.fetchImpl, policy: options.researchSourcePolicy, allowedDomains: researchAllowedDomains }); - } catch (error) { - return structuredError('research_egress_failed', error instanceof Error ? error.message : String(error), { toolId: descriptor.id }); - } - } - return structuredError('tool_not_implemented', `${toolId} is not implemented by the provider runner tool runtime.`); -} diff --git a/src/agents/tools/agent-tool-schema.ts b/src/agents/tools/agent-tool-schema.ts deleted file mode 100644 index e749a0f9..00000000 --- a/src/agents/tools/agent-tool-schema.ts +++ /dev/null @@ -1,161 +0,0 @@ -export interface AgentToolSchemaValidationResult { - ok: boolean; - code?: 'invalid_tool_descriptor' | 'invalid_tool_input'; - message?: string; - metadata?: Record; -} - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function schemaType(schema: Record) { - return typeof schema.type === 'string' ? schema.type : ''; -} - -function validatePrimitive(value: unknown, schema: Record, path: string): AgentToolSchemaValidationResult { - const type = schemaType(schema); - if (type === 'string') { - if (typeof value !== 'string') return { ok: false, code: 'invalid_tool_input', message: `${path} must be a string.`, metadata: { path, expected: type } }; - const minimum = Number(schema.minLength); - const maximum = Number(schema.maxLength); - if (Number.isFinite(minimum) && value.length < minimum) return { ok: false, code: 'invalid_tool_input', message: `${path} must contain at least ${minimum} characters.`, metadata: { path, minLength: minimum } }; - if (Number.isFinite(maximum) && value.length > maximum) return { ok: false, code: 'invalid_tool_input', message: `${path} must contain at most ${maximum} characters.`, metadata: { path, maxLength: maximum } }; - return { ok: true }; - } - if (type === 'boolean') { - return typeof value === 'boolean' - ? { ok: true } - : { ok: false, code: 'invalid_tool_input', message: `${path} must be a boolean.`, metadata: { path, expected: type } }; - } - if (type === 'number') { - return typeof value === 'number' && Number.isFinite(value) - ? { ok: true } - : { ok: false, code: 'invalid_tool_input', message: `${path} must be a finite number.`, metadata: { path, expected: type } }; - } - if (type === 'integer') { - if (typeof value !== 'number' || !Number.isSafeInteger(value)) return { ok: false, code: 'invalid_tool_input', message: `${path} must be a safe integer.`, metadata: { path, expected: type } }; - const minimum = Number(schema.minimum); - const maximum = Number(schema.maximum); - if (Number.isFinite(minimum) && value < minimum) return { ok: false, code: 'invalid_tool_input', message: `${path} must be at least ${minimum}.`, metadata: { path, minimum } }; - if (Number.isFinite(maximum) && value > maximum) return { ok: false, code: 'invalid_tool_input', message: `${path} must be at most ${maximum}.`, metadata: { path, maximum } }; - return { ok: true }; - } - return { - ok: false, - code: 'invalid_tool_descriptor', - message: `Unsupported schema type "${type}" at ${path}.`, - metadata: { path, type }, - }; -} - -function validateValue(value: unknown, schema: Record, path: string): AgentToolSchemaValidationResult { - const type = schemaType(schema); - if (type === 'object') { - if (!value || typeof value !== 'object' || Array.isArray(value)) { - return { ok: false, code: 'invalid_tool_input', message: `${path} must be an object.`, metadata: { path, expected: type } }; - } - const properties = record(schema.properties); - const required = Array.isArray(schema.required) ? schema.required.map(String) : []; - const objectValue = record(value); - for (const key of required) { - if (!(key in objectValue)) { - return { - ok: false, - code: 'invalid_tool_input', - message: `Missing required tool input "${path}.${key}".`, - metadata: { field: `${path}.${key}`, required }, - }; - } - } - if (schema.additionalProperties === false) { - for (const key of Object.keys(objectValue)) { - if (!(key in properties)) { - return { - ok: false, - code: 'invalid_tool_input', - message: `Unexpected tool input "${path}.${key}".`, - metadata: { field: `${path}.${key}` }, - }; - } - } - } - for (const [key, child] of Object.entries(objectValue)) { - if (!(key in properties)) continue; - const result = validateValue(child, record(properties[key]), `${path}.${key}`); - if (!result.ok) return result; - } - return { ok: true }; - } - if (type === 'array') { - if (!Array.isArray(value)) { - return { ok: false, code: 'invalid_tool_input', message: `${path} must be an array.`, metadata: { path, expected: type } }; - } - const items = record(schema.items); - const minimum = Number(schema.minItems); - const maximum = Number(schema.maxItems); - if (Number.isFinite(minimum) && value.length < minimum) return { ok: false, code: 'invalid_tool_input', message: `${path} requires at least ${minimum} items.`, metadata: { path, minItems: minimum } }; - if (Number.isFinite(maximum) && value.length > maximum) return { ok: false, code: 'invalid_tool_input', message: `${path} permits at most ${maximum} items.`, metadata: { path, maxItems: maximum } }; - const itemType = schemaType(items); - if (!itemType) { - return { - ok: false, - code: 'invalid_tool_descriptor', - message: `Array schema at ${path} must declare an item type.`, - metadata: { path }, - }; - } - for (let index = 0; index < value.length; index += 1) { - const result = itemType === 'object' - ? validateValue(value[index], items, `${path}[${index}]`) - : validatePrimitive(value[index], items, `${path}[${index}]`); - if (!result.ok) return result; - } - return { ok: true }; - } - return validatePrimitive(value, schema, path); -} - -export function validateAgentToolInput( - schema: Record, - input: Record, -): AgentToolSchemaValidationResult { - if (schemaType(schema) !== 'object') { - return { - ok: false, - code: 'invalid_tool_descriptor', - message: 'Tool input schema must be an object schema.', - metadata: { type: schema.type ?? null }, - }; - } - const properties = record(schema.properties); - const required = Array.isArray(schema.required) ? schema.required.map(String) : []; - for (const key of required) { - if (!(key in input)) { - return { - ok: false, - code: 'invalid_tool_input', - message: `Missing required tool input "${key}".`, - metadata: { field: key, required }, - }; - } - } - if (schema.additionalProperties === false) { - for (const key of Object.keys(input)) { - if (!(key in properties)) { - return { - ok: false, - code: 'invalid_tool_input', - message: `Unexpected tool input "${key}".`, - metadata: { field: key }, - }; - } - } - } - for (const [key, value] of Object.entries(input)) { - if (!(key in properties)) continue; - const result = validateValue(value, record(properties[key]), key); - if (!result.ok) return result; - } - return { ok: true }; -} diff --git a/src/agents/tools/agent-tool-telemetry.ts b/src/agents/tools/agent-tool-telemetry.ts deleted file mode 100644 index eb430a26..00000000 --- a/src/agents/tools/agent-tool-telemetry.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { appendFile } from 'node:fs/promises'; -import { findAgentToolDefinition } from '@treeseed/sdk'; -import type { ExecutionProviderToolDescriptor } from '../runtime/runtime-types.ts'; -import { - callAgentTool, - type AgentToolCallTelemetry, - type AgentToolDerivedEvent, - type AgentToolRuntimeOptions, -} from './agent-tool-runtime.ts'; -import { missingOperationalCloseoutReceipts, missingPrecommitContentReceipts, readAgentToolTelemetry } from './agent-tool-completion.ts'; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function text(value: unknown) { - return typeof value === 'string' ? value : ''; -} - -function normalizePath(value: string) { - return value.replace(/\\/gu, '/').replace(/^\.?\//u, '').replace(/\/+/gu, '/'); -} - -function summarize(value: Record) { - const summary: Record = {}; - for (const [key, raw] of Object.entries(value)) { - if (/token|secret|password|api[_-]?key|authorization/iu.test(key)) summary[key] = ''; - else if (typeof raw === 'string') summary[key] = raw.length > 120 ? `${raw.slice(0, 117)}...` : raw; - else if (Array.isArray(raw)) summary[key] = raw.length > 20 ? [...raw.slice(0, 20), `... ${raw.length - 20} more`] : raw; - else if (raw && typeof raw === 'object') summary[key] = ''; - else summary[key] = raw; - } - return summary; -} - -function operationForTool(toolId: string, descriptor: ExecutionProviderToolDescriptor | null | undefined) { - const definition = findAgentToolDefinition(toolId); - if (definition?.dispatch) return { namespace: definition.dispatch.namespace, name: definition.dispatch.operation }; - const metadata = record(descriptor?.metadata); - return { - namespace: text(metadata.operationNamespace) || descriptor?.executionTarget, - name: text(metadata.operationName) || toolId, - }; -} - -interface DerivedContentRef extends Record { - id?: string; - model?: string; - path?: string; - collection?: string; - slug?: string; - subjectId?: string; - subjectField?: string; -} - -function contentRefFrom(value: Record): DerivedContentRef | null { - const payload = record(value.payload); - const content = record(payload.content); - const data = record(payload.data); - const refs = Array.isArray(value.refs) ? value.refs : Array.isArray(payload.refs) ? payload.refs : []; - const ref = { ...record(payload.ref), ...record(refs[0]) }; - const changedPaths = Array.isArray(value.changedPaths) ? value.changedPaths : []; - const id = text(value.id) || text(payload.id) || text(content.id) || text(data.id) || text(ref.id) || text(ref.slug); - const model = text(value.model) || text(payload.model) || text(content.model) || text(data.model) || text(ref.model); - const path = text(value.path) || text(payload.path) || text(content.path) || text(data.path) || text(ref.path) || text(changedPaths[0]); - return id || model || path ? { - id: id || undefined, - model: model || undefined, - path: path || undefined, - collection: text(ref.collection) || undefined, - slug: text(ref.slug) || undefined, - subjectId: text(ref.subjectId) || undefined, - subjectField: text(ref.subjectField) || undefined, - } : null; -} - -function contentEvents(toolId: string, input: Record, output: Record, operationName: string) { - const inputModel = text(input.model) || text(input.contentType) || text(record(input.content).model); - const outputModel = text(output.model) || text(record(output.payload).model) || text(record(record(output.payload).content).model); - const model = inputModel || outputModel; - const action = text(input.action) || operationName || toolId; - let ref = contentRefFrom({ ...output, model: model || undefined }); - const events: AgentToolDerivedEvent[] = []; - if (model === 'question') { - const questionRef = ref ?? { model: 'question' }; - events.push(/create|add|write/iu.test(action) - ? { type: 'question_created', questionRef, answerPolicy: record(input.answerPolicy ?? record(input.frontmatter).answerPolicy ?? record(input.metadata).answerPolicy) } - : { type: 'question_updated', questionRef }); - } - if (ref && /create|add|write/iu.test(action)) events.push({ type: 'content_created', contentRef: ref }); - else if (ref && /update|link/iu.test(action)) events.push({ type: 'content_updated', contentRef: ref }); - return events; -} - -function lifecycleEvents(toolId: string, output: Record, action: string) { - const payload = record(output.payload); - const proxyPayload = record(payload.payload); - const events: AgentToolDerivedEvent[] = []; - if (toolId === 'treedx.commit_workspace' || toolId === 'treeseed.content.commit') { - const commitSha = text(output.commitSha) || text(payload.commitSha) || text(payload.sha) || text(record(payload.commit).sha) - || text(proxyPayload.commitSha) || text(proxyPayload.sha) || text(record(proxyPayload.commit).sha); - const branchRef = text(output.branchRef) || text(payload.branchRef) || text(payload.branchName) || text(payload.ref) - || text(proxyPayload.branchRef) || text(proxyPayload.branchName) || text(proxyPayload.ref); - events.push({ type: 'content_committed', ...(commitSha ? { commitSha } : {}), ...(branchRef ? { branchRef } : {}) }); - } - const branchRef = text(payload.branchRef) || text(payload.branchName) || text(output.branchRef); - if ((toolId === 'treeseed.stage' || /stage/iu.test(action)) && branchRef) { - events.push({ type: 'branch_staged', branchRef, stagedRef: text(payload.stagedRef) || undefined }); - } - if (toolId === 'treeseed.checkpoint') { - const result = record(payload); - const metadata = record(result.metadata); - const commitSha = text(metadata.commitSha); - if (commitSha) events.push({ - type: 'source_checkpoint_committed', commitSha, - branchRef: text(metadata.branchName) || undefined, - changedPaths: Array.isArray(result.changedPaths) ? result.changedPaths.map(String).filter(Boolean) : [], - }); - } - return events; -} - -function verificationEvents(toolId: string, output: Record) { - if (toolId !== 'treeseed.verify') return []; - const rawResults = record(output.payload).results; - const results: Record[] = Array.isArray(rawResults) - ? rawResults.map(record) - : []; - if (!results.length || results.some((result) => result.ok !== true)) return []; - const commands = results.map((result) => { - const command = text(result.command); - const args = Array.isArray(result.args) ? result.args.map(String) : []; - const cwd = text(result.cwd) || '.'; - return `${command}${args.length ? ` ${args.join(' ')}` : ''} (cwd: ${cwd})`; - }); - return [{ - type: 'verification_completed' as const, - status: 'passed' as const, - summary: `${results.length} verification command${results.length === 1 ? '' : 's'} met the expected exit code.`, - commands, - }]; -} - -function reviewDecisionEvents(toolId: string, output: Record) { - if (toolId !== 'treeseed.review_decision') return []; - const payload = record(output.payload); - const disposition = text(payload.disposition); - const summary = text(payload.summary); - if (!['approved', 'rejected'].includes(disposition) || !summary) return []; - return [{ - type: 'review_decision_recorded' as const, - disposition: disposition as 'approved' | 'rejected', - summary, - }]; -} - -function signalEvents(toolId: string, output: Record): AgentToolDerivedEvent[] { - if (toolId !== 'treeseed.publish_signal') return []; - const payload = record(output.payload); - return payload.requested === true ? [{ type: 'signal_requested', signal: payload }] : []; -} - -function communicationEvents(toolId:string,input:Record,output:Record):AgentToolDerivedEvent[]{ - if(toolId!=='treeseed.discussion.respond')return []; - const payload=record(output.payload);const message=record(payload.message);const changeset=record(payload.changeset); - const path=text(message.path);const commitSha=text(payload.commitSha)||text(changeset.resultCommitSha); - const checkpoint=record(output.suspensionCheckpoint??output.finalResponseCheckpoint);const checkpointSha=text(checkpoint.commitSha); - const operational=(Array.isArray(output.suspensionOperationalRefs)?output.suspensionOperationalRefs:[]).map(record).flatMap((ref)=>{ - const operationalPath=text(ref.path)||text(ref.contentPath);if(!operationalPath||!checkpointSha)return []; - return [{type:'content_created' as const,contentRef:{...ref,model:text(ref.model)||'assignment_summary',path:operationalPath, - artifactKind:'assignment_summary',commitSha:checkpointSha,ref:text(checkpoint.branchRef)||checkpointSha}}]; - }); - return [ - ...operational, - ...(checkpointSha?[{type:'content_committed' as const,commitSha:checkpointSha,branchRef:text(checkpoint.branchRef)||undefined}]:[]), - ...(!path||!commitSha?[]:[{type:'content_created' as const,contentRef:{id:text(message.id)||undefined,model:'discussion_message',path, - artifactKind:'discussion_response',subjectId:text(input.replyTo)||undefined,subjectField:'replyTo', - commitSha,ref:commitSha,baseRef:text(changeset.baseCommitSha)||undefined, - changedPaths:Array.isArray(changeset.changedPaths)?changeset.changedPaths.map(String).filter(Boolean):[path]}}]), - ]; -} - -function operationalContentEvents(toolId:string,output:Record):AgentToolDerivedEvent[]{ - const models:Record={ - 'treeseed.assignment_plan':'assignment_plan','treeseed.assignment_status_update':'assignment_status','treeseed.assignment_summary':'assignment_summary', - }; - const model=models[toolId];if(!model)return []; - return (Array.isArray(output.refs)?output.refs:[]).map(record).flatMap((ref)=>{ - const path=text(ref.path)||text(ref.contentPath);if(!path)return []; - return [{type:'content_created' as const,contentRef:{...ref,model:text(ref.model)||model,path,artifactKind:model}}]; - }); -} - -function researchEvents(toolId: string, input: Record, output: Record): AgentToolDerivedEvent[] { - const payload = record(output.payload); - if (toolId === 'research.fetch_source') { - const sourceUrl = text(payload.url); - const contentHash = text(payload.contentSha256); - const retrievedAt = text(payload.retrievedAt); - if (!sourceUrl || !contentHash || !retrievedAt) return []; - let publisher = 'unknown'; - try { publisher = new URL(sourceUrl).hostname.toLowerCase(); } catch { /* validated fetch output should always contain an absolute URL */ } - return [{ - type: 'research_citation_fetched', - citation: { - sourceUrl, - title: text(input.title) || sourceUrl, - publisher, - retrievedAt, - contentHash, - claimIds: Array.isArray(input.claimIds) ? input.claimIds.map(String).filter(Boolean) : ['research-claim'], - confidence: ['low', 'medium', 'high'].includes(text(input.confidence)) ? text(input.confidence) : 'medium', - }, - }]; - } - if (toolId === 'treeseed.research_claims') { - const claims = Array.isArray(payload.claims) ? payload.claims.map(record).filter((claim) => Object.keys(claim).length) : []; - return claims.length ? [{ type: 'research_claims_recorded', claims }] : []; - } - return []; -} - -export function deriveToolEvents(toolId: string, descriptor: ExecutionProviderToolDescriptor, input: Record, result: unknown) { - const output = record(result); - if (output.ok === false) return []; - const operation = operationForTool(toolId, descriptor); - const action = text(input.action) || operation.name || toolId; - return [ - ...contentEvents(toolId, input, output, operation.name), - ...lifecycleEvents(toolId, output, action), - ...verificationEvents(toolId, output), - ...reviewDecisionEvents(toolId, output), - ...signalEvents(toolId, output), - ...operationalContentEvents(toolId,output), - ...communicationEvents(toolId,input,output), - ...researchEvents(toolId, input, output), - ]; -} - -async function emitTelemetry(options: AgentToolRuntimeOptions, entry: AgentToolCallTelemetry) { - await options.onTelemetry?.(entry); - if (options.telemetryPath) await appendFile(options.telemetryPath, `${JSON.stringify(entry)}\n`, 'utf8'); -} - -export async function callAgentToolWithTelemetry(options: AgentToolRuntimeOptions, toolId: string, input: Record = {}) { - const descriptor = options.descriptors.find((candidate) => candidate.id === toolId) ?? null; - const metadata = record(descriptor?.metadata); - const assignmentId = text(metadata.assignmentId) || options.assignmentId; - const projectId = text(metadata.projectId); - const startedAt = new Date(); - if (descriptor?.kind === 'agent_tool') { - await emitTelemetry(options, { - assignmentId, projectId, toolId, executionTarget: descriptor.executionTarget, mutability: descriptor.mutability, - status: 'started', startedAt: startedAt.toISOString(), inputSummary: summarize(input), operation: operationForTool(toolId, descriptor), - }); - } - const commitTool = toolId === 'treedx.commit_workspace' || toolId === 'treeseed.content.commit'; - const requiredArtifactKind = text(metadata.requiredArtifactKind); - const priorTelemetry = commitTool && options.telemetryPath ? await readAgentToolTelemetry(options.telemetryPath) : []; - const missingPrecommit = !commitTool || !options.telemetryPath ? [] - : metadata.requireContentArtifact === true && requiredArtifactKind - ? missingPrecommitContentReceipts(priorTelemetry, requiredArtifactKind) - : missingOperationalCloseoutReceipts(priorTelemetry); - const result = missingPrecommit.length - ? { - ok: false, - code: 'content_completion_required_before_commit', - message: `TreeDX commit requires completion of: ${missingPrecommit.join(', ')}. Create or repair the required linked content before retrying commit.`, - metadata: { missingReceipts: missingPrecommit }, - } - : await callAgentTool(options, toolId, input); - if (descriptor?.kind === 'agent_tool') { - const completedAt = new Date(); - const resultRecord = record(result); - const failed = resultRecord.ok === false; - await emitTelemetry(options, { - assignmentId, projectId, toolId, executionTarget: descriptor.executionTarget, mutability: descriptor.mutability, - status: failed ? 'failed' : 'completed', startedAt: startedAt.toISOString(), completedAt: completedAt.toISOString(), - durationMs: completedAt.getTime() - startedAt.getTime(), inputSummary: summarize(input), - outputSummary: failed ? undefined : summarize(resultRecord), operation: operationForTool(toolId, descriptor), - derivedEvents: failed ? [] : deriveToolEvents(toolId, descriptor, input, resultRecord), - error: failed ? { code: text(resultRecord.code) || 'tool_failed', message: text(resultRecord.message) || 'Tool call failed.' } : undefined, - }); - } - return result; -} diff --git a/src/agents/tools/assignment/communication-response-tool.ts b/src/agents/tools/assignment/communication-response-tool.ts deleted file mode 100644 index bfc86d69..00000000 --- a/src/agents/tools/assignment/communication-response-tool.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ExecutionProviderToolDescriptor } from '../../runtime/runtime-types.ts'; -import { callContentTool } from '../content-tool-runtime.ts'; -import { missingCommunicationReadReceipts, missingOperationalCloseoutReceipts, readAgentToolTelemetry } from '../agent-tool-completion.ts'; -import { readAssignmentStatus } from '../status/assignment-status-tool.ts'; -import type { AgentToolRuntimeOptions } from '../runtime/agent-tool-runtime-types.ts'; -import { respondToAssignmentDiscussion } from './discussion-tool.ts'; -import { callAssignmentOperationalContentTool,hasAssignmentSummary } from './operational-content-tool.ts'; - -type Row=Record; -function record(value:unknown):Row{return value&&typeof value==='object'&&!Array.isArray(value)?value as Row:{};} -function text(value:unknown){return typeof value==='string'?value:'';} -function descriptorFor(options:AgentToolRuntimeOptions,id:string){return options.descriptors.find((entry)=>entry.id===id)??null;} -function error(code:string,message:string,metadata:Row={}){return {ok:false,code,message,metadata};} -function committedRef(value:unknown){let current=record(value);for(let depth=0;depth<4&&Object.keys(current).length;depth+=1){const commitSha=text(current.commitSha)||text(current.resultCommitSha)||text(current.sha)||text(record(current.commit).sha);if(commitSha)return {commitSha,branchRef:text(current.branchRef)||text(current.branchName)||text(current.ref)||undefined};current=record(current.payload);}return null;} - -async function checkpointOperationalContent(options:AgentToolRuntimeOptions,idempotencyKey:string){ - const content=descriptorFor(options,'treeseed.content.commit'); - if(!content)return error('assignment_discussion_checkpoint_unavailable','A Discussion response requires the TreeDX operational checkpoint tool.'); - const outcome=await callContentTool({apiBaseUrl:options.apiBaseUrl,providerAccessToken:options.providerAccessToken,assignmentId:options.assignmentId,descriptor:content,input:{message:`checkpoint before final discussion response ${idempotencyKey}`},fetchImpl:options.fetchImpl}); - const ref=committedRef(outcome); - return record(outcome).ok===true&&ref?{ok:true,checkpoint:{...ref,kind:'treedx-content'}}:error('assignment_discussion_checkpoint_failed','TreeDX operational checkpoint failed before the Discussion response.',{outcome}); -} - -export async function runAssignmentDiscussionResponse(options:AgentToolRuntimeOptions,descriptor:ExecutionProviderToolDescriptor,input:Row){ - let checkpoint:Row|null=null;let operationalRefs:Row[]=[];let responseInput=input; - if(!options.telemetryPath)return error('assignment_discussion_evidence_unavailable','Discussion responses require authoritative assignment tool telemetry.'); - const communicationMissing=missingCommunicationReadReceipts(await readAgentToolTelemetry(options.telemetryPath)); - if(communicationMissing.length)return error('assignment_discussion_context_required','Read and follow the exact Discussion before publishing a response.',{missingReceipts:communicationMissing}); - if(input.requiredResponse===true){ - const summaryDescriptor=descriptorFor(options,'treeseed.assignment_summary'); - if(summaryDescriptor){ - const status=record((await readAssignmentStatus(options)).payload); - const summary=await callAssignmentOperationalContentTool(options,summaryDescriptor,{action:'write',expectedStateVersion:status.stateVersion,status:'suspended',summary:`Suspended pending a required discussion response: ${text(input.message)}`,lessons:[],blockers:['Waiting for a required discussion response.'],performance:{outcome:'suspended',metrics:{}},artifactRefs:[],verificationRefs:[],resumeState:{checkpoint:'pending-required-response-checkpoint',nextAction:'Resume from the retained assignment plan after a governed response.',contextRefs:[]},idempotencyKey:`${text(input.idempotencyKey)}:summary`}); - if(record(summary).ok!==true)return error('assignment_suspension_summary_failed','Suspension summary failed before discussion send.',{summary}); - const refs=record(summary).refs; - operationalRefs=(Array.isArray(refs)?refs:[]).map((ref:unknown)=>record(ref)).filter((ref:Row)=>Object.keys(ref).length>0); - } - const result=await checkpointOperationalContent(options,text(input.idempotencyKey)); - if(record(result).ok!==true)return result; - checkpoint=record(result).checkpoint as Row; - const current=record((await readAssignmentStatus(options)).payload);responseInput={...input,expectedStateVersion:current.stateVersion}; - }else{ - if(!await hasAssignmentSummary(options))return error('assignment_final_summary_required','Write the terminal assignment summary before publishing the final Discussion response.'); - const missingReceipts=missingOperationalCloseoutReceipts(await readAgentToolTelemetry(options.telemetryPath)); - if(missingReceipts.length)return error('assignment_final_closeout_required','Complete terminal operational records before publishing the final Discussion response.',{missingReceipts}); - } - let response=await respondToAssignmentDiscussion(options,responseInput,checkpoint); - if(input.requiredResponse!==true&&record(response).code==='assignment_discussion_checkpoint_required'){ - const result=await checkpointOperationalContent(options,text(input.idempotencyKey)); - if(record(result).ok!==true)return result; - checkpoint=record(result).checkpoint as Row; - const current=record((await readAssignmentStatus(options)).payload); - response=await respondToAssignmentDiscussion(options,{...input,expectedStateVersion:current.stateVersion},checkpoint); - } - if(record(response).ok!==true)return response; - return input.requiredResponse===true?{...record(response),suspensionCheckpoint:checkpoint,suspensionOperationalRefs:operationalRefs}:{...record(response),finalResponseCheckpoint:checkpoint}; -} diff --git a/src/agents/tools/assignment/discussion-tool.ts b/src/agents/tools/assignment/discussion-tool.ts deleted file mode 100644 index 2ebb4125..00000000 --- a/src/agents/tools/assignment/discussion-tool.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { AgentToolRuntimeOptions } from '../runtime/agent-tool-runtime-types.ts'; - -function headers(options: AgentToolRuntimeOptions) { return { authorization:`Bearer ${options.providerAccessToken}`,accept:'application/json','content-type':'application/json' }; } -async function responsePayload(response: Response) { - const envelope=await response.json().catch(()=>null) as { payload?:unknown;error?:string;code?:string }|null; - if(!response.ok) return { ok:false,code:envelope?.code??'assignment_discussion_request_failed',message:envelope?.error??`Assignment discussion request failed with HTTP ${response.status}.` }; - return { ok:true,payload:envelope?.payload }; -} - -export async function readAssignmentDiscussion(options:AgentToolRuntimeOptions,input:Record){ - const query=new URLSearchParams(); - for(const key of ['discussionId','query','after','limit'] as const){ const value=input[key]; if(value!==undefined&&value!==null&&value!=='') query.set(key,String(value)); } - const response=await(options.fetchImpl??fetch)(`${options.apiBaseUrl.replace(/\/+$/u,'')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}/discussions?${query}`,{ headers:headers(options) }); - return responsePayload(response); -} - -export async function respondToAssignmentDiscussion(options:AgentToolRuntimeOptions,input:Record,checkpoint:Record|null){ - const body={ ...input,leaseToken:options.leaseToken??null,checkpoint }; - const response=await(options.fetchImpl??fetch)(`${options.apiBaseUrl.replace(/\/+$/u,'')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}/discussions/responses`,{ method:'POST',headers:headers(options),body:JSON.stringify(body) }); - const result=await responsePayload(response); - if(result.ok&&input.requiredResponse===true) return { ...result,suspended:true }; - return result; -} - -export async function requestAssignmentDiscussionHandoff(options:AgentToolRuntimeOptions,input:Record){ - const response=await(options.fetchImpl??fetch)(`${options.apiBaseUrl.replace(/\/+$/u,'')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}/discussions/handoffs`,{ method:'POST',headers:headers(options),body:JSON.stringify({ ...input,leaseToken:options.leaseToken??null }) }); - return responsePayload(response); -} - -export async function prepareAssignmentOperationHandoff(options:AgentToolRuntimeOptions,input:Record){ - const response=await(options.fetchImpl??fetch)(`${options.apiBaseUrl.replace(/\/+$/u,'')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}/operation-handoffs`,{ method:'POST',headers:headers(options),body:JSON.stringify({ ...input,leaseToken:options.leaseToken??null }) }); - return responsePayload(response); -} - -export async function requestAssignmentClientAction(options:AgentToolRuntimeOptions,input:Record){ - const response=await(options.fetchImpl??fetch)(`${options.apiBaseUrl.replace(/\/+$/u,'')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}/client-actions`,{ method:'POST',headers:headers(options),body:JSON.stringify({ ...input,leaseToken:options.leaseToken??null }) }); - return responsePayload(response); -} diff --git a/src/agents/tools/assignment/operational-content-tool.ts b/src/agents/tools/assignment/operational-content-tool.ts deleted file mode 100644 index ac2cb2c3..00000000 --- a/src/agents/tools/assignment/operational-content-tool.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { ExecutionProviderToolDescriptor,TreeDxProxyExecutionToolDescriptor } from '../../runtime/runtime-types.ts'; -import { parseFrontmatterDocument } from '@treeseed/sdk/frontmatter'; -import { callContentTool } from '../content-tool-runtime.ts'; -import { readAssignmentStatus } from '../status/assignment-status-tool.ts'; -import type { AgentToolRuntimeOptions } from '../runtime/agent-tool-runtime-types.ts'; - -type Row = Record; -function record(value: unknown): Row { return value && typeof value === 'object' && !Array.isArray(value) ? value as Row : {}; } -function text(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } -export function operationalResponseContent(value: unknown, seen = new WeakSet()): string { - if (!value || typeof value !== 'object' || Array.isArray(value) || seen.has(value)) return ''; - seen.add(value); - const source=record(value); - for(const candidate of [source.content,source.text,source.body]) if(typeof candidate==='string') return candidate; - for(const candidate of [source.payload,source.data,source.file]) { const nested=operationalResponseContent(candidate,seen); if(nested) return nested; } - return ''; -} -function frontmatter(value: unknown) { const source=operationalResponseContent(value); if(!source) return {}; try { return record(parseFrontmatterDocument(source).frontmatter); } catch { return {}; } } - -function proxyDescriptor(options: AgentToolRuntimeOptions, descriptor: ExecutionProviderToolDescriptor) { - const source = options.descriptors.find((candidate) => Boolean((candidate as TreeDxProxyExecutionToolDescriptor).routes)); - if (!source) return null; - return { ...source, id: descriptor.id, name: descriptor.name, description: descriptor.description, - inputSchema: descriptor.inputSchema, outputSchema: descriptor.outputSchema, executionTarget: 'treeseed_content' as const, - mutability: descriptor.mutability, metadata: { - ...record(source.metadata), ...record(descriptor.metadata), - // These exact-assignment records are mandatory lifecycle evidence, not - // profile-authored domain content. Their authority is the isolated - // assignment path and tool contract, while TreeDX still enforces both. - permissionSummary: {}, - } }; -} - -function operationalContentRoot(descriptor: ExecutionProviderToolDescriptor) { - const metadataRoot = text(record(descriptor.metadata).contentRoot); - if (metadataRoot) return metadataRoot.replace(/\/+$/u, ''); - const paths = (descriptor as TreeDxProxyExecutionToolDescriptor).allowedWritePaths - ?? (descriptor as TreeDxProxyExecutionToolDescriptor).allowedPaths ?? []; - for (const path of paths) { - const candidate = path.replace(/\\/gu, '/').replace(/\/\*\*$/u, '').replace(/\/+$/u, ''); - if (candidate === 'src/content' || candidate.endsWith('/src/content')) return candidate; - } - return 'src/content'; -} - -function operationalModel(toolId: string) { - if (toolId === 'treeseed.assignment_plan') return { model: 'assignment_plan', collection: 'assignment-plans' } as const; - if (toolId === 'treeseed.assignment_status_update') return { model: 'assignment_status', collection: 'assignment-statuses' } as const; - return { model: 'assignment_summary', collection: 'assignment-summaries' } as const; -} - -function contentId(toolId:string,input:Row,status:Row){ return toolId==='treeseed.assignment_status_update'?`${optionsId(status)}-status-${Number(input.sequence)}`:optionsId(status); } -export function assignmentOperationalPath(toolId:string,input:Row,status:Row,contentRoot='src/content') { - const model=operationalModel(toolId); return `${contentRoot}/${model.collection}/${contentId(toolId,input,status)}.mdx`; -} -function contentFields(toolId: string, input: Row, status: Row, existing: Row) { - const now = new Date().toISOString(); - const common = { - id: contentId(toolId,input,status), title: `${toolId.split('.').at(-1)?.replace(/_/gu, ' ')} for ${optionsId(status)}`, - teamId: text(status.teamId), projectId: text(status.projectId), ...(text(status.workdayId) ? { workdayId: text(status.workdayId) } : {}), assignmentId: optionsId(status), - createdAt: text(input.createdAt) || now, updatedAt: now, - }; - if (toolId === 'treeseed.assignment_plan') return { ...common, createdAt:text(existing.createdAt)||common.createdAt,status: text(input.status) || 'ready', revision:Math.max(1,Number(existing.revision)||0)+(Object.keys(existing).length?1:0), objective: text(input.objective), - completed:Array.isArray(input.completed)?input.completed:[],remaining:Array.isArray(input.remaining)?input.remaining:[],risks:Array.isArray(input.risks)?input.risks:[],...(record(input.resumeState).checkpoint?{ resumeState:record(input.resumeState) }:{}), - decisionId: text(record(status.governance).decisionId) || undefined, capacityPlanId: text(record(status.governance).capacityPlanId) || undefined }; - if (toolId === 'treeseed.assignment_status_update') { const sequence=Number(input.sequence); return { ...common,status:text(input.status),sequence,...(sequence>0?{ previousStatusRef:record(input.previousStatusRef) }:{}),phase:text(input.phase),reason:text(input.reason)||undefined,progress:input.progress }; } - return { ...common, status: text(input.status), summary: text(input.summary), artifactRefs: Array.isArray(input.artifactRefs) ? input.artifactRefs : [], - verificationRefs: Array.isArray(input.verificationRefs) ? input.verificationRefs : [],lessons:Array.isArray(input.lessons)?input.lessons:[],blockers:Array.isArray(input.blockers)?input.blockers:[],performance:record(input.performance), - ...(record(input.resumeState).checkpoint?{ resumeState:record(input.resumeState) }:{}) }; -} - -function optionsId(status: Row) { return text(status.assignmentId); } - -async function startExecutionAfterInitialPlan(options:AgentToolRuntimeOptions,status:Row,result:Row,input:Row){ - if(text(record(status.time).phase)!=='preparation') return null; - const ref=Array.isArray(result.refs)?record(result.refs[0]):{}; - const response=await (options.fetchImpl??fetch)(`${options.apiBaseUrl.replace(/\/+$/u,'')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}/execution-start`,{ - method:'POST',headers:{ authorization:`Bearer ${options.providerAccessToken}`,accept:'application/json','content-type':'application/json' }, - body:JSON.stringify({ leaseToken:options.leaseToken,expectedStateVersion:status.stateVersion,idempotencyKey:`${text(input.idempotencyKey)}:execution-start`,planRef:{ id:text(ref.id)||options.assignmentId,path:text(ref.path),revision:Number(frontmatter(result.postcondition).revision??1) } }), - }); - const payload=await response.json().catch(()=>null); - if(!response.ok) return { ok:false,code:text(record(payload).code)||'assignment_execution_start_failed',message:text(record(payload).error)||`Execution start failed with HTTP ${response.status}.`,metadata:{ payload } }; - return record(payload); -} - -async function startCloseoutBeforeSummary(options:AgentToolRuntimeOptions,status:Row,input:Row){ - const phase=text(record(status.time).phase); - if(phase==='expired') return { status,transition:{ ok:false,code:'assignment_closeout_deadline_exhausted',message:'The protected closeout deadline has expired; no further TreeDX mutation is authorized.' } }; - if(phase==='closeout') return { status,transition:null }; - const response=await (options.fetchImpl??fetch)(`${options.apiBaseUrl.replace(/\/+$/u,'')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}/closeout-start`,{ - method:'POST',headers:{ authorization:`Bearer ${options.providerAccessToken}`,accept:'application/json','content-type':'application/json' }, - body:JSON.stringify({ leaseToken:options.leaseToken,expectedStateVersion:status.stateVersion,idempotencyKey:`${text(input.idempotencyKey)}:closeout-start` }), - }); - const payload=await response.json().catch(()=>null); const assignment=record(record(payload).payload); - if(!response.ok) return { status,transition:{ ok:false,code:text(record(payload).code)||'assignment_closeout_start_failed',message:text(record(payload).error)||`Closeout start failed with HTTP ${response.status}.`,metadata:{ payload } } }; - return { status:{ ...status,...assignment,assignmentId:text(assignment.id)||text(status.assignmentId),workdayId:text(assignment.workDayId)||text(status.workdayId) },transition:assignment }; -} - -async function readRecord(options: AgentToolRuntimeOptions, descriptor: ExecutionProviderToolDescriptor, toolId: string,input:Row={}) { - const proxy = proxyDescriptor(options, descriptor); - if (!proxy) return { ok: false, code: 'assignment_treedx_proxy_required', message: 'Assignment operational content requires an assignment-scoped TreeDX workspace.' }; - const model = operationalModel(toolId); - return callContentTool({ ...options, descriptor: { ...proxy, metadata: { ...record(proxy.metadata), contentAction: 'read', contentModel: model.model } }, - input: { model: model.model, id: contentId(toolId,input,{ assignmentId:options.assignmentId }), path: assignmentOperationalPath(toolId,input,{ assignmentId:options.assignmentId },operationalContentRoot(proxy)) } }); -} - -export async function hasAssignmentPlan(options: AgentToolRuntimeOptions) { - const descriptor = options.descriptors.find((candidate) => candidate.id === 'treeseed.assignment_plan'); - if (!descriptor) return false; - const result = await readRecord(options, descriptor, descriptor.id); - return record(result).ok === true; -} - -export async function hasAssignmentSummary(options: AgentToolRuntimeOptions) { - const descriptor = options.descriptors.find((candidate) => candidate.id === 'treeseed.assignment_summary'); - if (!descriptor) return false; - const result = await readRecord(options, descriptor, descriptor.id); - return record(result).ok === true; -} - -export async function callAssignmentOperationalContentTool(options: AgentToolRuntimeOptions, descriptor: ExecutionProviderToolDescriptor, input: Row) { - const action = descriptor.id === 'treeseed.assignment_status_update' ? 'write' : text(input.action) || 'read'; - if (action === 'read') return readRecord(options, descriptor, descriptor.id); - let authoritative = record((await readAssignmentStatus(options)).payload); - if (Number(input.expectedStateVersion) !== Number(authoritative.stateVersion)) return { ok: false, code: 'assignment_status_stale', message: 'Assignment state changed; read treeseed.status and retry with its exact stateVersion.', metadata: { expectedStateVersion: input.expectedStateVersion, stateVersion: authoritative.stateVersion } }; - let closeoutTransition:Row|null=null; - if(descriptor.id==='treeseed.assignment_summary'){ - const closeout=await startCloseoutBeforeSummary(options,authoritative,input); - if(record(closeout.transition).ok===false) return closeout.transition; - authoritative=closeout.status; closeoutTransition=closeout.transition; - } - const proxy = proxyDescriptor(options, descriptor); - if (!proxy) return { ok: false, code: 'assignment_treedx_proxy_required', message: 'Assignment operational content requires an assignment-scoped TreeDX workspace.' }; - const model = operationalModel(descriptor.id); - const existing = descriptor.id==='treeseed.assignment_status_update'?{ ok:false }:await readRecord(options, descriptor, descriptor.id,input); - const contentAction = descriptor.id==='treeseed.assignment_status_update'?'create':record(existing).ok === true ? 'update' : 'create'; - const existingFields=frontmatter(existing); - const exactId = contentId(descriptor.id,input,authoritative); - const exactPath = assignmentOperationalPath(descriptor.id,input,authoritative,operationalContentRoot(proxy)); - const result = await callContentTool({ ...options, descriptor: { ...proxy, metadata: { ...record(proxy.metadata), contentAction, contentModel: model.model } }, - input: { model: model.model, id: exactId, slug: exactId, placement: { path: exactPath }, title: `${model.model.replace(/_/gu, ' ')} ${options.assignmentId}`, - fields: contentFields(descriptor.id, input, authoritative,existingFields), body: text(input.body) || text(input.summary) || text(input.objective) } }); - if (record(result).ok !== true) return result; - const postcondition = await readRecord(options, descriptor, descriptor.id,input); - if(record(postcondition).ok!==true) return { ok: false, code: 'assignment_operational_readback_failed', message: 'Operational content mutation did not pass authoritative TreeDX readback.', metadata: { result, postcondition } }; - const completed={ ...record(result),postcondition:record(postcondition).payload }; - if(descriptor.id==='treeseed.assignment_plan'){ - const transition=await startExecutionAfterInitialPlan(options,authoritative,completed,input); - if(record(transition).ok===false) return transition; - if(transition) { - const executionTransition=record(record(transition).payload); - return { ...completed,assignmentStateVersion:executionTransition.stateVersion,executionTransition }; - } - } - const assignmentStateVersion=closeoutTransition?.stateVersion??authoritative.stateVersion; - return closeoutTransition?{ ...completed,assignmentStateVersion,closeoutTransition }:{ ...completed,assignmentStateVersion }; -} diff --git a/src/agents/tools/content-tool-runtime.ts b/src/agents/tools/content-tool-runtime.ts deleted file mode 100644 index d37eb86b..00000000 --- a/src/agents/tools/content-tool-runtime.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { createHash } from 'node:crypto'; -import { - findContentToolPreset, - renderContentRecord, - validateContentRecord, - validateProposalContentForSubmission, - type ContentAction, - type ContentModel, -} from '@treeseed/sdk/content-operations'; -import { describeContentFrontmatterContract, isPortableContentModel } from '@treeseed/sdk/content-validation'; -import { parseFrontmatterDocument } from '@treeseed/sdk/frontmatter'; -import { createUnifiedChangeset } from '@treeseed/sdk/treedx'; -import type { ExecutionProviderToolDescriptor, TreeDxProxyExecutionToolDescriptor } from '../runtime/runtime-types.ts'; -import { callTreeDxProxyTool } from './treedx-proxy-client.ts'; -import { validateContentRelationTargets } from './content/relation-validation.ts'; - -export interface ContentToolCallOptions { - apiBaseUrl: string; - providerAccessToken: string; - assignmentId: string; - descriptor: ExecutionProviderToolDescriptor; - input?: Record; - fetchImpl?: typeof fetch; -} - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function text(value: unknown) { - return typeof value === 'string' ? value.trim() : ''; -} - -function structuredError(code: string, message: string, metadata: Record = {}) { - return { ok: false, code, message, metadata }; -} - -function contentPermissionDenial( - descriptor: TreeDxProxyExecutionToolDescriptor, - action: ContentAction, - model: ContentModel | undefined, -) { - const summary = record(descriptor.metadata?.permissionSummary); - if (!Object.keys(summary).length) return null; - if (action === 'commit') { - return summary.commitAllowed === true ? null : structuredError( - 'content_model_operation_denied', - 'Content commit is outside the immutable assignment permission snapshot.', - { action, model: null }, - ); - } - const readAction = ['describe', 'query', 'read'].includes(action); - const allowedActions = Array.isArray(summary[readAction ? 'readActions' : 'writeActions']) - ? (summary[readAction ? 'readActions' : 'writeActions'] as unknown[]).map(String) - : []; - const allowedModels = Array.isArray(summary[readAction ? 'readModels' : 'writeModels']) - ? (summary[readAction ? 'readModels' : 'writeModels'] as unknown[]).map(String) - : []; - if (allowedActions.includes(action) && model && (allowedModels.includes('*') || allowedModels.includes(model))) return null; - return structuredError( - 'content_model_operation_denied', - `Content ${action} for ${model || '(missing model)'} is outside the immutable assignment permission snapshot.`, - { action, model: model ?? null, allowedActions, allowedModels }, - ); -} - -function stableValue(value: unknown): unknown { - if (Array.isArray(value)) return value.map(stableValue); - if (!value || typeof value !== 'object') return value; - return Object.fromEntries(Object.entries(value as Record) - .filter(([key]) => key !== 'updated_at' && key !== 'updatedAt') - .sort(([left], [right]) => left.localeCompare(right)) - .map(([key, entry]) => [key, stableValue(entry)])); -} - -function sameLogicalContent(existing: string, requested: string) { - const current = parseFrontmatterDocument(existing); - const desired = parseFrontmatterDocument(requested); - return JSON.stringify(stableValue(current.frontmatter)) === JSON.stringify(stableValue(desired.frontmatter)) - && current.body.trim() === desired.body.trim(); -} - -function proposalTypePreflight( - descriptor: TreeDxProxyExecutionToolDescriptor, - frontmatter: Record, -) { - const allowed = Array.isArray(descriptor.metadata?.allowedProposalTypes) - ? [...new Set(descriptor.metadata.allowedProposalTypes.map(String).filter(Boolean))] - : []; - if (!allowed.length) return null; - const declared = [ - ...(Array.isArray(frontmatter.proposalTypes) ? frontmatter.proposalTypes : []), - ...(Array.isArray(frontmatter.proposal_types) ? frontmatter.proposal_types : []), - frontmatter.proposalType, - frontmatter.proposal_type, - ].filter((value) => value !== undefined && value !== null && value !== '').map(String); - const unsupported = [...new Set(declared)].filter((value) => !allowed.includes(value)); - return unsupported.length ? structuredError( - 'proposal_type_not_allowed', - 'Proposal types are outside the immutable assignment contract.', - { field: 'proposalTypes', unsupported, allowedProposalTypes: allowed }, - ) : null; -} - -function proxyDescriptor(descriptor: ExecutionProviderToolDescriptor): TreeDxProxyExecutionToolDescriptor | null { - const candidate = descriptor as TreeDxProxyExecutionToolDescriptor; - if (!candidate.handleId || !candidate.routes) return null; - return { - ...candidate, - executionTarget: 'treedx_proxy', - }; -} - -function resolveContentCall(descriptor: ExecutionProviderToolDescriptor, input: Record) { - const metadata = record(descriptor.metadata); - const preset = typeof metadata.contentPreset === 'string' ? findContentToolPreset(metadata.contentPreset) : null; - const idParts = descriptor.id.split('.'); - const action = text(metadata.contentAction) - || preset?.action - || (idParts[1] === 'content' ? idParts[2] : idParts[idParts.length - 1]); - const model = text(metadata.contentModel) || preset?.model || text(input.model); - return { - action: action as ContentAction, - model: model as ContentModel | undefined, - }; -} - -async function readWorkspaceFile(options: ContentToolCallOptions, descriptor: TreeDxProxyExecutionToolDescriptor, path: string) { - return await callTreeDxProxyTool({ - apiBaseUrl: options.apiBaseUrl, - providerAccessToken: options.providerAccessToken, - assignmentId: options.assignmentId, - handleId: descriptor.handleId, - descriptor, - toolName: 'treedx.read_workspace_file', - input: { path }, - fetchImpl: options.fetchImpl, - }); -} - -async function readModelContentFile( - options: ContentToolCallOptions, - descriptor: TreeDxProxyExecutionToolDescriptor, - canonicalPath: string, -) { - const paths = canonicalPath.endsWith('.mdx') - ? [canonicalPath, canonicalPath.replace(/\.mdx$/u, '.md')] - : [canonicalPath]; - let firstError: unknown; - for (const path of paths) { - try { - return { path, result: await readWorkspaceFile(options, descriptor, path) }; - } catch (error) { - firstError ??= error; - } - } - throw firstError; -} - -function collectContentPaths(value: unknown, paths = new Set()): Set { - if (Array.isArray(value)) { - for (const entry of value) collectContentPaths(entry, paths); - return paths; - } - if (!value || typeof value !== 'object') return paths; - for (const [key, entry] of Object.entries(value as Record)) { - if (typeof entry === 'string' && ['path','contentPath','filePath'].includes(key)) { - const normalized = entry.replace(/\\/gu, '/').replace(/^\.\//u, ''); - if (/\.mdx?$/iu.test(normalized)) paths.add(normalized); - } else collectContentPaths(entry, paths); - } - return paths; -} - -async function resolveModelContentPath( - options: ContentToolCallOptions, - descriptor: TreeDxProxyExecutionToolDescriptor, - canonicalPath: string, - slug: string, -) { - const result = await callTreeDxProxyTool({ - apiBaseUrl: options.apiBaseUrl, - providerAccessToken: options.providerAccessToken, - assignmentId: options.assignmentId, - handleId: descriptor.handleId, - descriptor, - toolName: 'treedx.search_workspace', - input: { query: slug }, - fetchImpl: options.fetchImpl, - }); - const collectionRoot = canonicalPath.slice(0, canonicalPath.lastIndexOf('/')); - const matches = [...collectContentPaths(result)].filter((path) => path.startsWith(`${collectionRoot}/`) - && path.replace(/\.mdx?$/iu, '').split('/').at(-1) === slug); - if (matches.length === 1) return matches[0]!; - if (matches.length > 1) throw Object.assign(new Error(`Content identity ${slug} resolves to multiple current repository paths.`), { - code: 'content_identity_ambiguous', paths: matches, - }); - return null; -} - -async function relationDiagnostics(options: ContentToolCallOptions, descriptor: TreeDxProxyExecutionToolDescriptor, frontmatter: Record, contentRoot: string) { - return validateContentRelationTargets(frontmatter, async ({ targetModel, targetId }) => { - if (!descriptor.routes.searchWorkspace) throw Object.assign(new Error('Content relation validation requires TreeDX workspace search.'), { code: 'content_relation_verification_unavailable' }); - const slug = targetId.includes(':') ? targetId.split(':').at(-1)! : targetId; - if (!targetModel || !isPortableContentModel(targetModel)) throw Object.assign(new Error(`Content relation target model ${targetModel || '(missing)'} is not portable.`), { code: 'content_relation_model_invalid' }); - const query = targetId.includes(':') ? targetId : `${targetModel}:${slug}`; - const result = await callTreeDxProxyTool({ - apiBaseUrl: options.apiBaseUrl, providerAccessToken: options.providerAccessToken, assignmentId: options.assignmentId, - handleId: descriptor.handleId, descriptor, toolName: 'treedx.search_workspace', input: { query }, fetchImpl: options.fetchImpl, - }); - const targetPath = renderContentRecord({ model: targetModel, slug, title: slug, contentRoot }).path; - const targetCollectionRoot = targetPath.slice(0, targetPath.lastIndexOf('/')); - const paths = [...collectContentPaths(result)].filter((path) => path.startsWith(`${targetCollectionRoot}/`)); - const records = await Promise.all(paths.map(async (path) => parseFrontmatterDocument(responseContent(await readWorkspaceFile(options, descriptor, path))).frontmatter)); - return { - ids: records.flatMap((entry) => typeof entry.id === 'string' && entry.id.trim() ? [entry.id.trim()] : []), - slugs: records.flatMap((entry, index) => typeof entry.slug === 'string' && entry.slug.trim() ? [entry.slug.trim()] : [paths[index]!.replace(/\.mdx?$/iu, '').split('/').at(-1)!]), - }; - }); -} - -async function applyWorkspaceFileChange(options: ContentToolCallOptions, descriptor: TreeDxProxyExecutionToolDescriptor, path: string, before: string | null, content: string) { - const baseCommitSha = text(descriptor.metadata?.baseCommitSha); - const baseRef = text(descriptor.metadata?.baseRef); - if (!baseCommitSha || !baseRef) throw new Error('TreeDX changeset requires the assignment workspace base commit and ref.'); - const patch = createUnifiedChangeset([{ path, before, after: content }]); - const patchSha256 = createHash('sha256').update(patch).digest('hex'); - return await callTreeDxProxyTool({ - apiBaseUrl: options.apiBaseUrl, - providerAccessToken: options.providerAccessToken, - assignmentId: options.assignmentId, - handleId: descriptor.handleId, - descriptor, - toolName: 'treedx.apply_workspace_changeset', - input: { - contract: 'treedx.changeset/v1', baseCommitSha, baseRef, patch, patchSha256, - idempotencyKey: createHash('sha256').update(`${options.assignmentId}:${patchSha256}`).digest('hex'), - expectedDestinationRefHead: baseCommitSha, - }, - fetchImpl: options.fetchImpl, - }); -} - -function responseContent(value: unknown): string { - if (!value || typeof value !== 'object' || Array.isArray(value)) return ''; - const source = record(value); - if (typeof source.content === 'string') return source.content; - if (typeof source.text === 'string') return source.text; - if (typeof source.body === 'string') return source.body; - for (const nested of [source.payload, source.data, source.file]) { - const content = responseContent(nested); - if (content) return content; - } - return ''; -} - -function descriptorContentRoot(descriptor: TreeDxProxyExecutionToolDescriptor) { - const configured = typeof descriptor.metadata?.contentRoot === 'string' ? descriptor.metadata.contentRoot.trim() : ''; - if (configured) return configured.replace(/\\/gu, '/').replace(/\/+$/u, ''); - const scopedPaths = descriptor.allowedWritePaths?.length - ? descriptor.allowedWritePaths - : descriptor.allowedReadPaths?.length - ? descriptor.allowedReadPaths - : descriptor.allowedPaths; - for (const allowed of scopedPaths) { - const candidate = allowed.replace(/\\/gu, '/').replace(/\/\*\*$/u, '').replace(/\/+$/u, ''); - if (candidate.endsWith('/src/content') || candidate === 'src/content') return candidate; - } - return 'src/content'; -} - -function contentSlug(input: Record) { - const candidate = text(input.slug) || text(input.id) || text(record(input.placement).path) || text(input.path); - if (!candidate.includes('/')) return candidate; - return candidate.replace(/\\/gu, '/').split('/').filter(Boolean).at(-1)?.replace(/\.mdx?$/iu, '') ?? candidate; -} - -export function modelRelativePlacement(input: Record, model: ContentModel, contentRoot: string) { - const placement = record(input.placement); - const configuredPath = text(placement.path).replace(/\\/gu, '/').replace(/^\.\//u, '').replace(/\.mdx?$/iu, ''); - if (!configuredPath) return placement; - return { ...placement, path: configuredPath }; -} - -export async function callContentTool(options: ContentToolCallOptions) { - const descriptor = proxyDescriptor(options.descriptor); - if (!descriptor) { - return structuredError('invalid_tool_descriptor', `${options.descriptor.id} is missing TreeDX proxy descriptor metadata.`); - } - const input = options.input ?? {}; - const { action, model } = resolveContentCall(options.descriptor, input); - const contentModel = model ?? text(input.model) as ContentModel; - const contentRoot = descriptorContentRoot(descriptor); - const relations = Array.isArray(input.relations) ? input.relations.filter((entry) => { - const relation = record(entry); - return Boolean(text(relation.field) && text(relation.targetSlug)); - }) : []; - try { - const permissionDenial = contentPermissionDenial(descriptor, action, contentModel); - if (permissionDenial) return permissionDenial; - if (action === 'describe') { - if (!contentModel || !isPortableContentModel(contentModel)) { - return structuredError('content_model_unknown', 'Describe requires a supported content model.', { model: contentModel || null }); - } - return { - ok: true, - action, - refs: [], - diagnostics: [], - payload: { - ...describeContentFrontmatterContract(contentModel), - message: 'This contract is derived from the canonical SDK validation schema. Use its exact fields and enum values for create and update operations.', - }, - }; - } - if (action === 'query') { - const result = await callTreeDxProxyTool({ - apiBaseUrl: options.apiBaseUrl, - providerAccessToken: options.providerAccessToken, - assignmentId: options.assignmentId, - handleId: descriptor.handleId, - descriptor, - toolName: 'treedx.search_workspace', - input: { query: text(input.query) || contentModel || '*' }, - fetchImpl: options.fetchImpl, - }); - return { ok: true, action, refs: [], diagnostics: [], payload: record(result) }; - } - if (action === 'commit') { - const message = text(input.message) || text(record(input.commit).message); - if (!message) return structuredError('content_validation_failed', 'A commit message is required.', { field: 'message' }); - const result = await callTreeDxProxyTool({ - apiBaseUrl: options.apiBaseUrl, - providerAccessToken: options.providerAccessToken, - assignmentId: options.assignmentId, - handleId: descriptor.handleId, - descriptor, - toolName: 'treedx.commit_workspace', - input: { message }, - fetchImpl: options.fetchImpl, - }); - return { ok: true, action, refs: [], diagnostics: [], payload: record(result) }; - } - if (!contentModel) return structuredError('content_model_unknown', 'A content model is required.', { toolId: options.descriptor.id }); - if (action === 'link' && !relations.length) return structuredError('content_relation_required', 'Content link requires at least one relation with field and targetSlug.', { toolId: options.descriptor.id }); - if (action === 'read') { - const rendered = renderContentRecord({ - model: contentModel, - slug: contentSlug(input), - title: text(input.title) || contentSlug(input), - placement: modelRelativePlacement(input, contentModel, contentRoot), - contentRoot, - }); - const descriptorMetadata = record(descriptor.metadata); - const assignedAgentPath = contentModel === 'agent' - && contentSlug(input) === text(descriptorMetadata.agentSlug) - ? text(descriptorMetadata.agentContentPath) - : ''; - const explicitPath = text(input.path) || assignedAgentPath; - let resolved: { path: string; result: unknown }; - if (explicitPath) resolved = { path: explicitPath, result: await readWorkspaceFile(options, descriptor, explicitPath) }; - else { - try { - resolved = await readModelContentFile(options, descriptor, rendered.path); - } catch (canonicalError) { - const resolvedPath = await resolveModelContentPath(options, descriptor, rendered.path, contentSlug(input)).catch((error) => { - if (record(error).code === 'content_identity_ambiguous') throw error; - return null; - }); - if (!resolvedPath) throw canonicalError; - resolved = { path: resolvedPath, result: await readWorkspaceFile(options, descriptor, resolvedPath) }; - } - } - const { path, result } = resolved; - return { - ok: true, - action, - refs: [{ ...rendered.ref, path }], - diagnostics: [], - payload: record(result), - }; - } - const initial = renderContentRecord({ - model: contentModel, - slug: contentSlug(input) || undefined, - title: text(input.title) || undefined, - fields: record(input.fields), - body: typeof input.body === 'string' ? input.body : undefined, - relations: relations as never, - placement: modelRelativePlacement(input, contentModel, contentRoot), - contentRoot, - }); - if (contentModel === 'proposal') { - const semantic = proposalTypePreflight(descriptor, record(input.fields)); - if (semantic) return semantic; - } - let rendered = initial; - let originalContent: string | null = null; - if (action === 'update' || action === 'link') { - const existing = await readModelContentFile(options, descriptor, initial.path); - originalContent = responseContent(existing.result); - rendered = renderContentRecord({ - model: contentModel, - slug: contentSlug(input) || undefined, - title: text(input.title) || undefined, - fields: record(input.fields), - body: typeof input.body === 'string' ? input.body : undefined, - relations: relations as never, - placement: modelRelativePlacement(input, contentModel, contentRoot), - contentRoot, - existingContent: originalContent, - }); - } - if (action === 'validate') { - const placement = record(input.placement); - const proposedRecord = typeof input.body === 'string' - || Boolean(text(input.title)) - || Object.keys(record(input.fields)).length > 0; - const existing = text(placement.path) && !proposedRecord - ? await readModelContentFile(options, descriptor, rendered.path) - : null; - const validation = validateContentRecord(contentModel, existing ? responseContent(existing.result) : rendered.content); - if (!validation.ok) { - return structuredError('content_validation_failed', 'Content validation failed.', { diagnostics: validation.diagnostics }); - } - const relationValidation = await relationDiagnostics(options, descriptor, parseFrontmatterDocument(existing ? responseContent(existing.result) : rendered.content).frontmatter, contentRoot); - if (relationValidation.length) return structuredError('content_relation_invalid', 'Content relationship validation failed against the current assignment workspace.', { diagnostics: relationValidation }); - if (contentModel === 'proposal') { - const semantic = proposalTypePreflight(descriptor, rendered.frontmatter); - if (semantic) return semantic; - const proposal = validateProposalContentForSubmission(existing ? responseContent(existing.result) : rendered.content); - if (!proposal.ok) return structuredError('proposal_plan_incomplete', 'Agent proposal is missing required planning evidence.', { diagnostics: proposal.diagnostics }); - } - return { ok: true, action, refs: [{ ...rendered.ref, ...(existing ? { path: existing.path } : {}) }], diagnostics: validation.diagnostics }; - } - if (action === 'create' || action === 'update' || action === 'link') { - const preflight = validateContentRecord(contentModel, rendered.content); - if (!preflight.ok) { - return structuredError('content_validation_failed', 'Content validation failed before any workspace mutation.', { diagnostics: preflight.diagnostics }); - } - const relationValidation = await relationDiagnostics(options, descriptor, rendered.frontmatter, contentRoot); - if (relationValidation.length) return structuredError('content_relation_invalid', 'Content relationship validation failed before any workspace mutation.', { diagnostics: relationValidation }); - if (contentModel === 'proposal') { - const semantic = proposalTypePreflight(descriptor, rendered.frontmatter); - if (semantic) return semantic; - const proposal = validateProposalContentForSubmission(rendered.content); - if (!proposal.ok) return structuredError('proposal_plan_incomplete', 'Agent proposal is missing required planning evidence.', { diagnostics: proposal.diagnostics }); - } - if (action === 'create') { - const existing = await readModelContentFile(options, descriptor, rendered.path).catch(() => null); - const existingContent = existing ? responseContent(existing.result) : ''; - if (existingContent) { - if (!sameLogicalContent(existingContent, rendered.content)) { - return structuredError('content_create_conflict', 'Create target already exists with different content.', { path: rendered.path }); - } - return { - ok: true, action, refs: [rendered.ref], changedPaths: [rendered.path], - idempotentReplay: true, diagnostics: rendered.diagnostics, - }; - } - } - const changeset = await applyWorkspaceFileChange(options, descriptor, rendered.path, originalContent, rendered.content); - const readback = await readWorkspaceFile(options, descriptor, rendered.path).catch(() => null); - if (readback) { - const content = responseContent(readback) || rendered.content; - const validation = validateContentRecord(contentModel, content); - if (!validation.ok) { - return structuredError('content_readback_failed', 'Content readback validation failed.', { diagnostics: validation.diagnostics }); - } - if (contentModel === 'proposal') { - const proposal = validateProposalContentForSubmission(content); - if (!proposal.ok) return structuredError('proposal_plan_incomplete', 'Agent proposal is missing required planning evidence.', { diagnostics: proposal.diagnostics }); - } - } - return { - ok: true, - action, - refs: [rendered.ref], - changedPaths: [rendered.path], - changeset, - diagnostics: rendered.diagnostics, - }; - } - return structuredError('tool_not_implemented', `${options.descriptor.id} content action is not implemented.`, { action }); - } catch (error) { - const reportedCode = text(record(error).code); - const code = reportedCode || (action === 'query' || action === 'read' ? 'content_readback_failed' : 'content_write_failed'); - return structuredError(code, error instanceof Error ? error.message : String(error), { - action, model: contentModel, ...(Array.isArray(record(error).paths) ? { paths: record(error).paths } : {}), - }); - } -} diff --git a/src/agents/tools/content/relation-validation.ts b/src/agents/tools/content/relation-validation.ts deleted file mode 100644 index 4806a760..00000000 --- a/src/agents/tools/content/relation-validation.ts +++ /dev/null @@ -1,46 +0,0 @@ -type JsonRecord = Record; - -export interface ContentRelationDiagnostic { - code: 'content_relation_target_missing' | 'content_relation_target_mismatch'; - field: string; - targetModel: string; - targetId: string; - message: string; -} - -const relationModels: Record = { - about: '', - relatedObjectives: 'objective', related_objectives: 'objective', - relatedQuestions: 'question', related_questions: 'question', - relatedProposals: 'proposal', related_proposals: 'proposal', - relatedDecisions: 'decision', related_decisions: 'decision', - relatedNotes: 'note', related_notes: 'note', - relatedBooks: 'book', related_books: 'book', -}; - -function targets(value: unknown) { - return (Array.isArray(value) ? value : [value]).filter((entry): entry is string => typeof entry === 'string' && Boolean(entry.trim())).map((entry) => entry.trim()); -} - -export async function validateContentRelationTargets( - frontmatter: JsonRecord, - resolve: (input: { field: string; targetModel: string; targetId: string }) => Promise<{ ids: string[]; slugs: string[] }>, -) { - const diagnostics: ContentRelationDiagnostic[] = []; - for (const [field, targetModel] of Object.entries(relationModels)) { - for (const targetId of targets(frontmatter[field])) { - const inferredModel = targetModel || targetId.split(':')[0] || ''; - const observed = await resolve({ field, targetModel: inferredModel, targetId }); - const requestedSlug = targetId.includes(':') ? targetId.split(':').at(-1)! : targetId; - if (observed.ids.includes(targetId) || (!targetId.includes(':') && observed.slugs.includes(requestedSlug))) continue; - diagnostics.push({ - code: observed.ids.length ? 'content_relation_target_mismatch' : 'content_relation_target_missing', - field, targetModel: inferredModel, targetId, - message: observed.ids.length - ? `Relation ${field} identifies ${targetId}, but the current repository record has identity ${observed.ids.join(', ')}.` - : `Relation ${field} target ${targetId} does not exist in the current assignment workspace.`, - }); - } - } - return diagnostics; -} diff --git a/src/agents/tools/governed-research-tools.ts b/src/agents/tools/governed-research-tools.ts deleted file mode 100644 index cdab697e..00000000 --- a/src/agents/tools/governed-research-tools.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { createHash } from 'node:crypto'; -import { lookup } from 'node:dns/promises'; -import { isIP } from 'node:net'; -import { assertResearchSourcePolicy, type ResearchSourcePolicy } from '@treeseed/sdk/agent-capacity'; - -const MAX_SOURCE_BYTES = 1_000_000; -const DEFAULT_ALLOWED_CONTENT_TYPES = ['text/*', 'application/json', 'application/xml', 'application/xhtml+xml']; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function privateIpv4(address: string) { - const parts = address.split('.').map(Number); - if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) return false; - return parts[0] === 10 - || parts[0] === 127 - || (parts[0] === 169 && parts[1] === 254) - || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) - || (parts[0] === 192 && parts[1] === 168) - || parts[0] === 0; -} - -function privateIp(address: string) { - if (isIP(address) === 4) return privateIpv4(address); - const normalized = address.toLowerCase(); - return normalized === '::1' - || normalized === '::' - || normalized.startsWith('fc') - || normalized.startsWith('fd') - || normalized.startsWith('fe8') - || normalized.startsWith('fe9') - || normalized.startsWith('fea') - || normalized.startsWith('feb') - || normalized.startsWith('::ffff:127.') - || normalized.startsWith('::ffff:10.') - || normalized.startsWith('::ffff:192.168.'); -} - -function sourcePolicy(env: NodeJS.ProcessEnv, configured?: ResearchSourcePolicy, allowedDomains?: string[]): ResearchSourcePolicy { - const providerPolicy = configured ? assertResearchSourcePolicy(configured) : assertResearchSourcePolicy({ - schemaVersion: 1, - allowedDomains: String(env.TREESEED_RESEARCH_ALLOWED_DOMAINS ?? '').split(',').map((value) => value.trim().toLowerCase()).filter(Boolean), - requestTimeoutMs: Number(env.TREESEED_RESEARCH_REQUEST_TIMEOUT_MS ?? 20_000), - maxResponseBytes: Number(env.TREESEED_RESEARCH_MAX_RESPONSE_BYTES ?? MAX_SOURCE_BYTES), - maxRedirects: Number(env.TREESEED_RESEARCH_MAX_REDIRECTS ?? 3), - allowedContentTypes: String(env.TREESEED_RESEARCH_ALLOWED_CONTENT_TYPES ?? DEFAULT_ALLOWED_CONTENT_TYPES.join(',')).split(',').map((value) => value.trim().toLowerCase()).filter(Boolean), - }); - if (!allowedDomains) return providerPolicy; - const projectDomains = allowedDomains.map((domain) => domain.trim().toLowerCase()).filter(Boolean); - return assertResearchSourcePolicy({ - ...providerPolicy, - allowedDomains: providerPolicy.allowedDomains.filter((domain) => projectDomains.some((projectDomain) => domain === projectDomain || domain.endsWith(`.${projectDomain}`) || projectDomain.endsWith(`.${domain}`))), - }); -} - -function domainAllowed(hostname: string, policy: ResearchSourcePolicy) { - const candidate = hostname.toLowerCase().replace(/\.$/u, ''); - return policy.allowedDomains.some((allowed) => candidate === allowed || candidate.endsWith(`.${allowed}`)); -} - -async function governedUrl(value: unknown, env: NodeJS.ProcessEnv, policy: ResearchSourcePolicy) { - let url: URL; - try { url = new URL(String(value ?? '')); } catch { throw new Error('Research source URL is invalid.'); } - if (url.protocol !== 'https:') throw new Error('Research egress permits HTTPS URLs only.'); - if (url.username || url.password) throw new Error('Research source URLs cannot contain credentials.'); - if (url.port && url.port !== '443') throw new Error('Research source URLs must use the standard HTTPS port.'); - if (!domainAllowed(url.hostname, policy)) throw new Error(`Research source domain ${url.hostname} is not allowed by policy.`); - const addresses = await lookup(url.hostname, { all: true, verbatim: true }); - if (!addresses.length) throw new Error('Research source hostname did not resolve.'); - if (env.TREESEED_RESEARCH_ALLOW_PRIVATE_EGRESS !== 'true' && addresses.some((entry) => privateIp(entry.address))) { - throw new Error('Research egress denied a private, loopback, or link-local destination.'); - } - return url; -} - -async function boundedBody(response: Response, maxBytes: number) { - const declared = Number(response.headers.get('content-length')); - if (Number.isFinite(declared) && declared > maxBytes) throw new Error(`Research source exceeds the ${maxBytes}-byte response limit.`); - if (!response.body) return new Uint8Array(); - const reader = response.body.getReader(); - const chunks: Uint8Array[] = []; - let length = 0; - while (true) { - const item = await reader.read(); - if (item.done) break; - length += item.value.byteLength; - if (length > maxBytes) { - await reader.cancel(); - throw new Error(`Research source exceeds the ${maxBytes}-byte response limit.`); - } - chunks.push(item.value); - } - const result = new Uint8Array(length); - let offset = 0; - for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.byteLength; } - return result; -} - -async function fetchGoverned(url: URL, env: NodeJS.ProcessEnv, policy: ResearchSourcePolicy, fetchImpl: typeof fetch, headers: Record = {}) { - let current = url; - for (let redirects = 0; redirects <= policy.maxRedirects; redirects += 1) { - current = await governedUrl(current.toString(), env, policy); - const response = await fetchImpl(current, { - headers: { accept: 'text/html, text/plain, application/json, application/xml;q=0.8', 'user-agent': 'TreeseedResearch/1.0', ...headers }, - redirect: 'manual', - signal: AbortSignal.timeout(policy.requestTimeoutMs), - }); - if (response.status >= 300 && response.status < 400) { - const location = response.headers.get('location'); - if (!location || redirects === policy.maxRedirects) throw new Error('Research source redirect limit was exceeded.'); - current = new URL(location, current); - continue; - } - return { response, url: current }; - } - throw new Error('Research source redirect limit was exceeded.'); -} - -export async function fetchGovernedResearchSource(input: Record, options: { env?: NodeJS.ProcessEnv; fetchImpl?: typeof fetch; policy?: ResearchSourcePolicy; allowedDomains?: string[] } = {}) { - const env = options.env ?? process.env; - const fetchImpl = options.fetchImpl ?? fetch; - const policy = sourcePolicy(env, options.policy, options.allowedDomains); - const requested = await governedUrl(input.url, env, policy); - const maxBytes = Math.max(1_024, Math.min(Number(input.maxBytes ?? policy.maxResponseBytes), policy.maxResponseBytes)); - const { response, url } = await fetchGoverned(requested, env, policy, fetchImpl); - if (!response.ok) throw new Error(`Research source returned HTTP ${response.status}.`); - const mediaType = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() ?? 'application/octet-stream'; - if (!policy.allowedContentTypes.some((allowed) => allowed === mediaType || (allowed.endsWith('/*') && mediaType.startsWith(allowed.slice(0, -1))))) { - throw new Error(`Research source media type ${mediaType} is not supported.`); - } - const bytes = await boundedBody(response, maxBytes); - const text = new TextDecoder().decode(bytes); - return { - ok: true, - payload: { - url: url.toString(), - retrievedAt: new Date().toISOString(), - mediaType, - contentLength: bytes.byteLength, - contentSha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}`, - content: text, - }, - }; -} - -export async function searchGovernedResearchSources(input: Record, options: { env?: NodeJS.ProcessEnv; fetchImpl?: typeof fetch; policy?: ResearchSourcePolicy; allowedDomains?: string[] } = {}) { - const env = options.env ?? process.env; - const policy = sourcePolicy(env, options.policy, options.allowedDomains); - const endpoint = env.TREESEED_RESEARCH_SEARCH_ENDPOINT?.trim(); - if (!endpoint) throw new Error('TREESEED_RESEARCH_SEARCH_ENDPOINT is required for governed source discovery.'); - const url = await governedUrl(endpoint, env, policy); - url.searchParams.set('q', String(input.query ?? '')); - url.searchParams.set('limit', String(Math.max(1, Math.min(Number(input.maxResults ?? 10), 20)))); - const headers: Record = env.TREESEED_RESEARCH_SEARCH_TOKEN ? { authorization: `Bearer ${env.TREESEED_RESEARCH_SEARCH_TOKEN}` } : {}; - const { response, url: finalUrl } = await fetchGoverned(url, env, policy, options.fetchImpl ?? fetch, headers); - if (!response.ok) throw new Error(`Research search adapter returned HTTP ${response.status}.`); - const bytes = await boundedBody(response, policy.maxResponseBytes); - const parsed = JSON.parse(new TextDecoder().decode(bytes)) as unknown; - const root = record(parsed); - const candidates = [root.items, root.results, root.sources].find(Array.isArray) ?? []; - const items = (candidates as unknown[]).slice(0, 20).map((entry) => { - const source = record(entry); - return { - url: String(source.url ?? source.link ?? ''), - title: String(source.title ?? ''), - publisher: String(source.publisher ?? source.siteName ?? ''), - publishedAt: source.publishedAt ?? source.published_at ?? null, - summary: String(source.summary ?? source.snippet ?? '').slice(0, 2_000), - }; - }).filter((entry) => { - try { return entry.url.startsWith('https://') && domainAllowed(new URL(entry.url).hostname, policy); } catch { return false; } - }); - return { ok: true, payload: { query: String(input.query ?? ''), adapter: finalUrl.origin, retrievedAt: new Date().toISOString(), items } }; -} diff --git a/src/agents/tools/path-scope/path-matcher.ts b/src/agents/tools/path-scope/path-matcher.ts deleted file mode 100644 index b140995f..00000000 --- a/src/agents/tools/path-scope/path-matcher.ts +++ /dev/null @@ -1,15 +0,0 @@ -function normalizePath(value: string) { - return value.replace(/\\/gu, '/').replace(/^\.?\//u, '').replace(/\/+/gu, '/'); -} - -export function matchesScopedPath(path: string, pattern: string) { - const normalizedPath = normalizePath(path); - const normalizedPattern = normalizePath(pattern); - if (!normalizedPattern || normalizedPattern === '**' || normalizedPattern === '*') return true; - if (normalizedPattern.endsWith('/**')) { - const prefix = normalizedPattern.slice(0, -3); - return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`); - } - if (normalizedPattern.endsWith('*')) return normalizedPath.startsWith(normalizedPattern.slice(0, -1)); - return normalizedPath === normalizedPattern || normalizedPath.startsWith(`${normalizedPattern}/`); -} diff --git a/src/agents/tools/runtime/agent-tool-runtime-types.ts b/src/agents/tools/runtime/agent-tool-runtime-types.ts deleted file mode 100644 index d4bed59e..00000000 --- a/src/agents/tools/runtime/agent-tool-runtime-types.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { - AgentToolExecutionTarget, - AgentToolMutability, - ResearchSourcePolicy, -} from '@treeseed/sdk'; -import type { AgentSdk } from '@treeseed/sdk/sdk'; -import type { ExecutionProviderToolDescriptor } from '../../runtime/runtime-types.ts'; - -export interface AgentToolRuntimeOptions { - apiBaseUrl: string; - providerAccessToken: string; - assignmentId: string; - leaseToken?: string | null; - descriptors: ExecutionProviderToolDescriptor[]; - sdk?: Pick; - fetchImpl?: typeof fetch; - repoRoot?: string; - telemetryPath?: string | null; - onTelemetry?: (entry: AgentToolCallTelemetry) => void | Promise; - researchSourcePolicy?: ResearchSourcePolicy; -} - -export interface AgentToolCallTelemetry { - assignmentId: string; - projectId: string; - toolId: string; - executionTarget: AgentToolExecutionTarget; - mutability: AgentToolMutability; - status: 'started' | 'completed' | 'failed'; - startedAt: string; - completedAt?: string; - durationMs?: number; - inputSummary: Record; - outputSummary?: Record; - operation?: { namespace?: string; name?: string }; - capturedInputRef?: string; - capturedOutputRef?: string; - derivedEvents?: AgentToolDerivedEvent[]; - error?: { code: string; message: string }; -} - -export type AgentToolDerivedEvent = - | { type: 'question_created'; questionRef: Record; answerPolicy?: Record } - | { type: 'question_updated'; questionRef: Record } - | { type: 'content_created'; contentRef: Record; requiresCommit?: boolean } - | { type: 'content_updated'; contentRef: Record } - | { type: 'verification_completed'; status: 'passed'; summary: string; commands: string[] } - | { type: 'branch_staged'; branchRef: string; stagedRef?: string } - | { type: 'content_committed'; commitSha?: string; branchRef?: string } - | { type: 'source_checkpoint_committed'; commitSha: string; branchRef?: string; changedPaths: string[] } - | { type: 'review_decision_recorded'; disposition: 'approved' | 'rejected'; summary: string } - | { type: 'research_citation_fetched'; citation: Record } - | { type: 'research_claims_recorded'; claims: Record[] } - | { type: 'signal_requested'; signal: Record }; diff --git a/src/agents/tools/status/assignment-activity-tool.ts b/src/agents/tools/status/assignment-activity-tool.ts deleted file mode 100644 index 126eace4..00000000 --- a/src/agents/tools/status/assignment-activity-tool.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { agentActivityPageSchema,agentActivityQuerySchema } from '@treeseed/sdk/agent-capacity'; - -type RuntimeOptions = { - apiBaseUrl: string; - providerAccessToken: string; - assignmentId: string; - fetchImpl?: typeof fetch; -}; - -export async function readAssignmentActivity(options: RuntimeOptions, input: Record) { - const query = agentActivityQuerySchema.parse(input); - const parameters = new URLSearchParams({ after: String(query.after), limit: String(query.limit) }); - for (const key of ['type', 'severity'] as const) { - const value = query[key]; - if (Array.isArray(value)) parameters.set(key, value.join(',')); - else if (value) parameters.set(key, value); - } - const response = await (options.fetchImpl ?? fetch)( - `${options.apiBaseUrl.replace(/\/+$/u, '')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}/activity?${parameters}`, - { headers: { authorization: `Bearer ${options.providerAccessToken}`, accept: 'application/json' } }, - ); - const envelope = await response.json().catch(() => null) as { payload?: unknown; error?: string } | null; - if (!response.ok) throw new Error(envelope?.error ?? `Assignment activity request failed with HTTP ${response.status}.`); - return { ok: true, payload: agentActivityPageSchema.parse(envelope?.payload) }; -} diff --git a/src/agents/tools/status/assignment-status-tool.ts b/src/agents/tools/status/assignment-status-tool.ts deleted file mode 100644 index c9d78987..00000000 --- a/src/agents/tools/status/assignment-status-tool.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { assignmentTimeWindow } from '@treeseed/sdk/agent-capacity'; - -type RuntimeOptions = { - apiBaseUrl: string; - providerAccessToken: string; - assignmentId: string; - fetchImpl?: typeof fetch; - now?: () => Date; -}; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function date(value: unknown) { - if (typeof value !== 'string' || !value.trim()) return null; - const parsed = Date.parse(value); - return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null; -} - -function positiveInteger(value: unknown) { - const parsed = Number(value); - return Number.isInteger(parsed) && parsed > 0 ? parsed : null; -} - -function projection(value: unknown, now: Date) { - const assignment = record(record(value).payload); - const decisionInput = record(assignment.decisionInput); - const envelope = record(assignment.capacityEnvelope); - const budget = record(envelope.budget); - const budgetTime = record(budget.time); - const startedAt = date(assignment.claimedAt ?? assignment.assignedAt ?? envelope.startedAt); - const closeoutWarningSeconds = positiveInteger(budgetTime.closeoutWarningSeconds) ?? 180; - const window=assignmentTimeWindow(budgetTime,now.getTime()); const phase=window.phase; - const deadlineAt=date(window.deadlineAt ?? budget.deadline); - const remainingSeconds=phase==='preparation'?window.preparationRemainingSeconds:phase==='working'?window.executionRemainingSeconds:window.closeoutRemainingSeconds; - return { - assignmentId: assignment.id, teamId: assignment.teamId, stateVersion: assignment.stateVersion, - updatedAt: assignment.updatedAt, workdayId: assignment.workDayId, - workdayRunId: record(assignment.metadata).workdayRunId, projectId: assignment.projectId, - agentId: assignment.agentId, agentClassId: assignment.projectAgentClassId, - activityType: record(decisionInput.input).activityType ?? record(decisionInput.metadata).activityType, - handlerId: assignment.handlerId, mode: assignment.mode, status: assignment.status, - lease: { state: assignment.leaseState, expiresAt: assignment.leaseExpiresAt, renewedAt: assignment.leaseRenewedAt }, - time: { - ...window, now: now.toISOString(), startedAt, deadlineAt, - allocatedSeconds: positiveInteger(envelope.reservedSeconds ?? envelope.requestedSeconds), - remainingSeconds, closeoutWarningSeconds, - reservationId: assignment.reservationId, - }, - governance: { decisionId: assignment.decisionId, proposalId: assignment.proposalId, allocationSetId: assignment.allocationSetId }, - allowedOutputs: assignment.allowedOutputs, - lifecycle: { code: assignment.lifecycleCode, reason: assignment.lifecycleReason, - completedAt: assignment.completedAt, failedAt: assignment.failedAt }, - operationalState: record(assignment.metadata).operationalState ?? null, - }; -} - -export async function readAssignmentStatus(options: RuntimeOptions) { - const response = await (options.fetchImpl ?? fetch)( - `${options.apiBaseUrl.replace(/\/+$/u, '')}/v1/provider/assignments/${encodeURIComponent(options.assignmentId)}`, - { headers: { authorization: `Bearer ${options.providerAccessToken}`, accept: 'application/json' } }, - ); - const payload = await response.json().catch(() => null); - if (!response.ok) throw new Error(`Assignment status request failed with HTTP ${response.status}.`); - return { ok: true, payload: projection(payload, options.now?.() ?? new Date()) }; -} diff --git a/src/agents/tools/treedx-proxy-client.ts b/src/agents/tools/treedx-proxy-client.ts deleted file mode 100644 index ed08fe77..00000000 --- a/src/agents/tools/treedx-proxy-client.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type { TreeDxProxyExecutionToolDescriptor } from '../runtime/runtime-types.ts'; -import { - TREE_DX_PROXY_TOOL_REQUIRED_OPERATIONS, - type TreeDxProxyToolName, -} from './treedx-proxy-tool.ts'; -import { matchesScopedPath } from './path-scope/path-matcher.ts'; - -export interface TreeDxProxyToolCallOptions { - apiBaseUrl: string; - providerAccessToken: string; - assignmentId: string; - handleId: string; - descriptor: TreeDxProxyExecutionToolDescriptor; - toolName: TreeDxProxyToolName; - input?: Record; - fetchImpl?: typeof fetch; -} - -const RETRYABLE_READ_TOOLS = new Set([ - 'treedx.build_context', - 'treedx.read_repository_files', - 'treedx.search_workspace', - 'treedx.read_workspace_file', -]); - -function transientProxyFailure(error: unknown) { - return /fetch failed|timed out|econnreset|econnrefused|socket|temporarily unavailable/iu.test(error instanceof Error ? error.message : String(error)); -} - -function assertAllowedOperation(descriptor: TreeDxProxyExecutionToolDescriptor, toolName: TreeDxProxyToolName) { - const missing = TREE_DX_PROXY_TOOL_REQUIRED_OPERATIONS[toolName].filter((operation) => !descriptor.allowedOperations.includes(operation)); - if (missing.length) { - throw new Error(`TreeDX proxy operation denied: ${missing.join(', ')}.`); - } -} - -function assertAllowedPath(descriptor: TreeDxProxyExecutionToolDescriptor, toolName: TreeDxProxyToolName, input: Record) { - const path = typeof input.path === 'string' ? input.path : null; - const paths = Array.isArray(input.paths) ? input.paths.filter((value): value is string => typeof value === 'string') : []; - const candidates = path ? [path] : paths; - const operations = TREE_DX_PROXY_TOOL_REQUIRED_OPERATIONS[toolName] ?? []; - const writeOperation = operations.some((operation) => operation === 'files:write' || operation === 'git:commit'); - const scopedPaths = writeOperation - ? (descriptor.allowedWritePaths?.length ? descriptor.allowedWritePaths : descriptor.allowedPaths) - : (descriptor.allowedReadPaths?.length ? descriptor.allowedReadPaths : descriptor.allowedPaths); - if (!candidates.length || !scopedPaths.length) return; - for (const candidate of candidates) { - if (!scopedPaths.some((pattern) => matchesScopedPath(candidate, pattern))) { - throw new Error(`TreeDX proxy path denied: ${candidate}.`); - } - } -} - -function resolveRoute(descriptor: TreeDxProxyExecutionToolDescriptor, toolName: TreeDxProxyToolName, input: Record) { - const repoId = encodeURIComponent(String(input.repoId ?? descriptor.repositoryId ?? '')); - const workspaceId = encodeURIComponent(String(input.workspaceId ?? descriptor.workspaceId ?? '')); - const path = encodeURIComponent(String(input.path ?? '')); - const route = (() => { - switch (toolName) { - case 'treedx.build_context': - return descriptor.routes.buildContext; - case 'treedx.read_repository_files': - return descriptor.routes.readRepositoryFiles; - case 'treedx.search_workspace': - return descriptor.routes.searchWorkspace; - case 'treedx.read_workspace_file': - return descriptor.routes.readWorkspaceFile; - case 'treedx.apply_workspace_changeset': - return descriptor.routes.applyWorkspaceChangeset; - case 'treedx.commit_workspace': - return descriptor.routes.commitWorkspace; - default: - throw new Error(`Unsupported TreeDX tool: ${toolName}`); - } - })().replace(/:repoId/gu, repoId).replace(/:workspaceId/gu, workspaceId).replace(/:path/gu, path); - const separator = route.indexOf(' '); - if (separator < 0) throw new Error(`Invalid TreeDX route template for ${toolName}.`); - return { - method: route.slice(0, separator), - path: route.slice(separator + 1), - }; -} - -function requestBody(toolName: TreeDxProxyToolName, input: Record) { - switch (toolName) { - case 'treedx.build_context': - return { - query: input.query, - paths: input.paths, - budget: { - maxNodes: Math.max(1, Math.min(64, Number(input.maxNodes) || 16)), - maxTokens: Math.max(256, Math.min(12_000, Number(input.maxTokens) || 4_000)), - }, - }; - case 'treedx.read_repository_files': - return { - paths: input.paths, - ref: input.ref, - maxBytes: Math.max(1, Math.min(196_608, Number(input.maxBytes) || 131_072)), - offsetBytes: Math.max(0, Number(input.offsetBytes) || 0), - }; - case 'treedx.search_workspace': - return { query: input.query }; - case 'treedx.apply_workspace_changeset': - return input; - case 'treedx.commit_workspace': - return { message: input.message }; - case 'treedx.read_workspace_file': - return null; - default: - return null; - } -} - -export async function callTreeDxProxyTool(options: TreeDxProxyToolCallOptions): Promise> { - if (!options.apiBaseUrl.trim()) throw new Error('TreeSeed API base URL is required for TreeDX proxy tools.'); - if (!options.providerAccessToken.trim()) throw new Error('Capacity provider access token is required for TreeDX proxy tools.'); - if (!options.assignmentId.trim()) throw new Error('Assignment id is required for TreeDX proxy tools.'); - if (!options.handleId.trim()) throw new Error('TreeDX proxy handle id is required for TreeDX proxy tools.'); - const input = options.input ?? {}; - assertAllowedOperation(options.descriptor, options.toolName); - assertAllowedPath(options.descriptor, options.toolName, input); - const paths = Array.isArray(input.paths) ? input.paths.filter((value): value is string => typeof value === 'string') : []; - if (options.toolName === 'treedx.read_repository_files' && paths.length > 1) { - const responses = await Promise.all(paths.map((path) => callTreeDxProxyTool({ - ...options, - input: { ...input, paths: [path] }, - }))); - const first = responses[0] as Record; - const payload = first.payload && typeof first.payload === 'object' && !Array.isArray(first.payload) - ? first.payload as Record - : {}; - const files = responses.flatMap((response) => { - const entry = response as Record; - const body = entry.payload && typeof entry.payload === 'object' && !Array.isArray(entry.payload) - ? entry.payload as Record - : {}; - return Array.isArray(body.files) ? body.files : body.file ? [body.file] : []; - }); - return { ...first, payload: { ...payload, files, file: files[0] ?? null } }; - } - const route = resolveRoute(options.descriptor, options.toolName, input); - const body = requestBody(options.toolName, input); - const maxAttempts = RETRYABLE_READ_TOOLS.has(options.toolName) ? 3 : 1; - let lastError: unknown = new Error('TreeDX proxy request was not attempted.'); - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - try { - const response = await (options.fetchImpl ?? fetch)(`${options.apiBaseUrl.replace(/\/$/u, '')}${route.path}`, { - method: route.method, - headers: { - accept: 'application/json', - 'content-type': 'application/json', - authorization: `Bearer ${options.providerAccessToken}`, - 'x-treeseed-assignment-id': options.assignmentId, - 'x-treeseed-treedx-proxy-handle-id': options.handleId, - }, - body: body == null ? undefined : JSON.stringify(body), - }); - const responseText = await response.text(); - if (!response.ok) { - const error = Object.assign(new Error(responseText || `TreeDX proxy request failed with ${response.status}.`), { status: response.status, operation: options.toolName, path: route.path }); - if (attempt < maxAttempts && (response.status === 429 || response.status >= 500)) throw error; - return Promise.reject(error); - } - return responseText ? JSON.parse(responseText) : {}; - } catch (error) { - lastError = error; - const status = Number((error as { status?: unknown })?.status); - const transient = transientProxyFailure(error) || status === 429 || status >= 500; - if (!transient || attempt === maxAttempts) throw error; - await new Promise((resolve) => setTimeout(resolve, 100 * attempt)); - } - } - throw lastError; -} diff --git a/src/agents/tools/treedx-proxy-tool.ts b/src/agents/tools/treedx-proxy-tool.ts deleted file mode 100644 index 9770e3a1..00000000 --- a/src/agents/tools/treedx-proxy-tool.ts +++ /dev/null @@ -1,87 +0,0 @@ -export const TREE_DX_PROXY_TOOL_NAMES = [ - 'treedx.build_context', - 'treedx.read_repository_files', - 'treedx.search_workspace', - 'treedx.read_workspace_file', - 'treedx.apply_workspace_changeset', - 'treedx.commit_workspace', -] as const; - -export type TreeDxProxyToolName = typeof TREE_DX_PROXY_TOOL_NAMES[number]; - -export const TREE_DX_PROXY_TOOL_REQUIRED_OPERATIONS: Record = { - 'treedx.build_context': ['files:read'], - 'treedx.read_repository_files': ['files:read'], - 'treedx.search_workspace': ['files:search'], - 'treedx.read_workspace_file': ['files:read'], - 'treedx.apply_workspace_changeset': ['files:write'], - 'treedx.commit_workspace': ['git:commit'], -}; - -export const TREE_DX_PROXY_TOOL_INPUT_SCHEMAS: Record> = { - 'treedx.build_context': { - type: 'object', - properties: { - repoId: { type: 'string' }, - query: { type: 'string' }, - paths: { type: 'array', items: { type: 'string' } }, - maxNodes: { type: 'integer', minimum: 1, maximum: 64 }, - maxTokens: { type: 'integer', minimum: 256, maximum: 12000 }, - }, - additionalProperties: false, - }, - 'treedx.read_repository_files': { - type: 'object', - properties: { - repoId: { type: 'string' }, - paths: { type: 'array', minItems: 1, items: { type: 'string' } }, - ref: { type: 'string' }, - maxBytes: { type: 'integer', minimum: 1, maximum: 196608 }, - offsetBytes: { type: 'integer', minimum: 0 }, - }, - required: ['paths'], - additionalProperties: false, - }, - 'treedx.search_workspace': { - type: 'object', - properties: { - workspaceId: { type: 'string' }, - query: { type: 'string' }, - }, - required: ['query'], - additionalProperties: false, - }, - 'treedx.read_workspace_file': { - type: 'object', - properties: { - workspaceId: { type: 'string' }, - path: { type: 'string' }, - }, - required: ['path'], - additionalProperties: false, - }, - 'treedx.apply_workspace_changeset': { - type: 'object', - properties: { - workspaceId: { type: 'string' }, - contract: { type: 'string' }, - baseCommitSha: { type: 'string' }, - baseRef: { type: 'string' }, - patch: { type: 'string' }, - patchSha256: { type: 'string' }, - idempotencyKey: { type: 'string' }, - expectedDestinationRefHead: { type: 'string' }, - }, - required: ['contract', 'baseCommitSha', 'baseRef', 'patch', 'patchSha256', 'idempotencyKey'], - additionalProperties: false, - }, - 'treedx.commit_workspace': { - type: 'object', - properties: { - workspaceId: { type: 'string' }, - message: { type: 'string' }, - }, - required: ['message'], - additionalProperties: false, - }, -}; diff --git a/src/api/agents/agent-routes.ts b/src/api/agents/agent-routes.ts deleted file mode 100644 index 12e5c017..00000000 --- a/src/api/agents/agent-routes.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { Hono } from 'hono'; -import type { - AgentSdk, - SdkContextPack, - SdkGraphQueryResult, -} from '@treeseed/sdk'; -import { listRegisteredAgentHandlers as listCoreRegisteredAgentHandlers } from '../../agents/support/registry.ts'; -import type { ApiContext } from '../support/http.ts'; -import { jsonError, requireScope } from '../support/http.ts'; - -interface RegisterAgentRoutesOptions { - sdk: AgentSdk; - prefix?: string; - scope?: string | null; - projectId?: string; - authorize?: (c: ApiContext) => Response | null; -} - -async function listRegisteredHandlers() { - return listCoreRegisteredAgentHandlers(); -} - -async function safeListRegisteredHandlers() { - try { - return { - handlers: await listRegisteredHandlers(), - error: null, - }; - } catch (error) { - return { - handlers: [], - error: error instanceof Error ? error.message : String(error), - }; - } -} - -function withPrefix(prefix: string, path: string) { - return `${prefix}${path}`.replace(/\/{2,}/g, '/'); -} - -function routeParam(c: { req: { param: (name: string) => string | undefined } }, name: string) { - const value = c.req.param(name); - if (!value) { - throw new Error(`Missing route parameter "${name}".`); - } - return value; -} - -function authorizeRequest(c: ApiContext, options: RegisterAgentRoutesOptions) { - const routeUnauthorized = options.authorize?.(c); - if (routeUnauthorized) { - return routeUnauthorized; - } - if (options.scope) { - return requireScope(c, options.scope); - } - return null; -} - -export function registerAgentRoutes( - app: Hono, - options: RegisterAgentRoutesOptions, -) { - const prefix = options.prefix ?? '/agent'; - app.get(withPrefix(prefix, '/healthz'), async (c) => { - const registration = await safeListRegisteredHandlers(); - - return c.json({ - ok: true, - service: 'treeseed-agent-api', - handlerCount: registration.handlers.length, - registrationError: registration.error, - }); - }); - - app.get(withPrefix(prefix, '/specs'), async (c) => { - const unauthorized = authorizeRequest(c as ApiContext, options); - if (unauthorized) return unauthorized; - const payload = await options.sdk.listAgentSpecs({ enabled: true }); - return c.json({ - ok: true, - payload, - handlers: await listRegisteredHandlers(), - }); - }); - - app.post(withPrefix(prefix, '/graph/search'), async (c) => { - const unauthorized = authorizeRequest(c as ApiContext, options); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const query = String(body.query ?? ''); - const scope = String(body.scope ?? 'sections'); - const payload = - scope === 'files' - ? await options.sdk.searchFiles(query, body.options as Record | undefined) - : scope === 'entities' - ? await options.sdk.searchEntities(query, body.options as Record | undefined) - : await options.sdk.searchSections(query, body.options as Record | undefined); - return c.json({ ok: true, payload }); - }); - - app.post(withPrefix(prefix, '/graph/subgraph'), async (c) => { - const unauthorized = authorizeRequest(c as ApiContext, options); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const payload = await options.sdk.getSubgraph( - Array.isArray(body.seedIds) ? body.seedIds.map(String) : [], - body.options as Record | undefined, - ); - return c.json({ ok: true, payload }); - }); - - app.post(withPrefix(prefix, '/graph/query'), async (c) => { - const unauthorized = authorizeRequest(c as ApiContext, options); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const payload = await options.sdk.queryGraph(body as never) as SdkGraphQueryResult; - return c.json({ ok: true, payload }); - }); - - app.post(withPrefix(prefix, '/graph/context-pack'), async (c) => { - const unauthorized = authorizeRequest(c as ApiContext, options); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const payload = await options.sdk.buildContextPack(body as never) as SdkContextPack; - return c.json({ ok: true, payload }); - }); - - app.post(withPrefix(prefix, '/graph/parse-dsl'), async (c) => { - const unauthorized = authorizeRequest(c as ApiContext, options); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const payload = await options.sdk.parseGraphDsl(String(body.source ?? body.query ?? '')); - return c.json({ ok: true, payload }); - }); - - app.get(withPrefix(prefix, '/graph/node/:id'), async (c) => { - const unauthorized = authorizeRequest(c as ApiContext, options); - if (unauthorized) return unauthorized; - const payload = await options.sdk.getGraphNode(routeParam(c, 'id')); - return payload ? c.json({ ok: true, payload }) : jsonError(c, 404, 'Unknown graph node.'); - }); -} diff --git a/src/api/auth/d1-database.ts b/src/api/auth/d1-database.ts deleted file mode 100644 index bd554c43..00000000 --- a/src/api/auth/d1-database.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CloudflareHttpD1Database } from '@treeseed/sdk'; -import { NodeSqliteD1Database } from '@treeseed/sdk/db/node-sqlite'; -import type { D1DatabaseLike } from '@treeseed/sdk/types/cloudflare'; -import type { ApiConfig } from '../types.ts'; - -export function resolveApiD1Database(config: ApiConfig): D1DatabaseLike { - if (config.cloudflareAccountId && config.cloudflareApiToken && config.d1DatabaseId) { - return new CloudflareHttpD1Database({ - accountId: config.cloudflareAccountId, - apiToken: config.cloudflareApiToken, - databaseId: config.d1DatabaseId, - }); - } - - if (config.d1LocalPersistTo || config.d1DatabaseName) { - return new NodeSqliteD1Database(config.d1LocalPersistTo); - } - - throw new Error( - 'Treeseed API auth requires either CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN + TREESEED_API_D1_DATABASE_ID for remote D1 access, or TREESEED_API_D1_LOCAL_PERSIST_TO for local SQLite-backed D1-compatible access.', - ); -} diff --git a/src/api/auth/d1-provider.ts b/src/api/auth/d1-provider.ts deleted file mode 100644 index 66423a60..00000000 --- a/src/api/auth/d1-provider.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { createHmac, timingSafeEqual } from 'node:crypto'; -import type { D1DatabaseLike } from '@treeseed/sdk/types/cloudflare'; -import type { - ApiAuthProvider, - ApiConfig, - ApiCredential, - ApiPrincipal, - DeviceCodeApproveRequest, - DeviceCodePollRequest, - DeviceCodePollResponse, - DeviceCodeStartRequest, - DeviceCodeStartResponse, - TokenRefreshRequest, - TokenRefreshResponse, - TrustedUserAssertionClaims, - UserIdentityProfileInput, -} from '../types.ts'; -import { D1AuthStore } from './d1-store.ts'; - -function encodePayload(payload: TrustedUserAssertionClaims) { - return Buffer.from(JSON.stringify(payload)).toString('base64url'); -} - -function decodePayload(value: string) { - return JSON.parse(Buffer.from(value, 'base64url').toString('utf8')) as TrustedUserAssertionClaims; -} - -function signPayload(payload: string, secret: string) { - return createHmac('sha256', secret).update(payload).digest('base64url'); -} - -function safeEqual(left: string, right: string) { - const leftBuffer = Buffer.from(left); - const rightBuffer = Buffer.from(right); - return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer); -} - -export class D1AuthProvider implements ApiAuthProvider { - readonly id = 'd1'; - private readonly store: D1AuthStore; - - constructor( - private readonly config: ApiConfig, - options: { db?: D1DatabaseLike } = {}, - ) { - if (!options.db) { - throw new Error('D1AuthProvider requires an explicit database binding or adapter.'); - } - this.store = new D1AuthStore(config, options.db); - } - - startDeviceFlow(request: DeviceCodeStartRequest): Promise { - return this.store.startDeviceFlow(request); - } - - pollDeviceFlow(request: DeviceCodePollRequest): Promise { - return this.store.pollDeviceFlow(request); - } - - refreshAccessToken(request: TokenRefreshRequest): Promise { - return this.store.refreshAccessToken(request); - } - - approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{ ok: true }> { - return this.store.approveDeviceFlow(request); - } - - authenticateBearerToken(token: string): Promise<{ principal: ApiPrincipal; credential: ApiCredential } | null> { - return this.store.authenticateBearerToken(token); - } - - authenticateServiceCredential(serviceId: string, secret: string): Promise<{ principal: ApiPrincipal; credential: ApiCredential } | null> { - return this.store.authenticateService(serviceId, secret); - } - - createPersonalAccessToken(userId: string, input: { name: string; scopes?: string[]; expiresAt?: string | null }) { - return this.store.createPersonalAccessToken(userId, input); - } - - listPersonalAccessTokens(userId: string) { - return this.store.listPersonalAccessTokens(userId); - } - - revokePersonalAccessToken(userId: string, tokenId: string) { - return this.store.revokePersonalAccessToken(userId, tokenId); - } - - syncUserIdentity(identity: UserIdentityProfileInput) { - return this.store.syncUser(identity); - } - - createUser(input: { email?: string | null; username?: string | null; displayName?: string | null; metadata?: Record }) { - return this.store.createUser(input); - } - - setUserRoles(userId: string, roles: string[]) { - return this.store.setUserRoles(userId, roles); - } - - createServiceToken(input: { serviceId: string; name: string; roles?: string[]; permissions?: string[] }) { - return this.store.createServiceCredential(input); - } - - rotateServiceToken(serviceId: string) { - return this.store.rotateServiceCredential(serviceId); - } - - createTrustedUserAssertion(claims: TrustedUserAssertionClaims) { - const payload = encodePayload(claims); - const signature = signPayload(payload, this.config.webAssertionSecret); - return `${payload}.${signature}`; - } - - verifyTrustedUserAssertion(assertion: string) { - const [payload, signature] = assertion.split('.'); - if (!payload || !signature) return null; - const expectedSignature = signPayload(payload, this.config.webAssertionSecret); - if (!safeEqual(signature, expectedSignature)) return null; - const claims = decodePayload(payload); - if (!claims.expiresAt || new Date(claims.expiresAt).getTime() <= Date.now()) { - return null; - } - return claims; - } - - exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims) { - return this.store.exchangeTrustedUserAssertion(claims); - } -} diff --git a/src/api/auth/d1-store-core.ts b/src/api/auth/d1-store-core.ts deleted file mode 100644 index ed5f7c15..00000000 --- a/src/api/auth/d1-store-core.ts +++ /dev/null @@ -1,498 +0,0 @@ -import { createHash, randomUUID, timingSafeEqual } from 'node:crypto'; -import type { D1DatabaseLike } from '@treeseed/sdk/types/cloudflare'; -import type { - ApiConfig, - ApiCredential, - ApiPrincipal, - DeviceCodeApproveRequest, - DeviceCodePollRequest, - DeviceCodePollResponse, - DeviceCodeStartRequest, - DeviceCodeStartResponse, - TokenRefreshRequest, - TokenRefreshResponse, - TrustedUserAssertionClaims, - UserIdentityProfileInput, -} from '../types.ts'; -import { DEFAULT_PERMISSIONS, DEFAULT_ROLES } from './rbac.ts'; -import { createAccessToken, nextOpaqueToken, principalFromAccessTokenPayload, verifyAccessToken } from './tokens.ts'; - -export function approvalUrl(baseUrl: string, userCode?: string | null) { - const url = new URL('/auth/device/approve', `${baseUrl.replace(/\/+$/u, '')}/`); - if (userCode) { - url.searchParams.set('user_code', userCode); - } - return url.toString(); -} - -const AUTH_SCHEMA_SQL = [ - `CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - email TEXT, - username TEXT UNIQUE, - display_name TEXT, - status TEXT NOT NULL DEFAULT 'active', - metadata_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - )`, - `CREATE TABLE IF NOT EXISTS user_identities ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - provider TEXT NOT NULL, - provider_subject TEXT NOT NULL, - email TEXT, - email_verified INTEGER NOT NULL DEFAULT 0, - profile_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - )`, - `CREATE UNIQUE INDEX IF NOT EXISTS idx_user_identities_provider_subject - ON user_identities(provider, provider_subject)`, - `CREATE TABLE IF NOT EXISTS roles ( - id TEXT PRIMARY KEY, - key TEXT NOT NULL UNIQUE, - description TEXT, - created_at TEXT NOT NULL - )`, - `CREATE TABLE IF NOT EXISTS permissions ( - id TEXT PRIMARY KEY, - key TEXT NOT NULL UNIQUE, - resource TEXT NOT NULL, - action TEXT NOT NULL, - scope TEXT NOT NULL, - description TEXT, - created_at TEXT NOT NULL - )`, - `CREATE TABLE IF NOT EXISTS role_permissions ( - role_id TEXT NOT NULL, - permission_id TEXT NOT NULL, - created_at TEXT NOT NULL, - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS user_role_bindings ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - role_id TEXT NOT NULL, - created_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE - )`, - `CREATE UNIQUE INDEX IF NOT EXISTS idx_user_role_bindings_user_role - ON user_role_bindings(user_id, role_id)`, - `CREATE TABLE IF NOT EXISTS api_tokens ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - kind TEXT NOT NULL, - name TEXT NOT NULL, - token_prefix TEXT NOT NULL, - token_hash TEXT NOT NULL, - scopes_json TEXT NOT NULL, - expires_at TEXT, - last_used_at TEXT, - revoked_at TEXT, - metadata_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id - ON api_tokens(user_id)`, - `CREATE INDEX IF NOT EXISTS idx_api_tokens_prefix - ON api_tokens(token_prefix)`, - `CREATE TABLE IF NOT EXISTS service_credentials ( - id TEXT PRIMARY KEY, - service_id TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - secret_hash TEXT NOT NULL, - roles_json TEXT NOT NULL, - permissions_json TEXT NOT NULL, - revoked_at TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - last_used_at TEXT - )`, - `CREATE TABLE IF NOT EXISTS auth_sessions ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - session_type TEXT NOT NULL, - refresh_token_hash TEXT NOT NULL, - scopes_json TEXT NOT NULL, - expires_at TEXT NOT NULL, - revoked_at TEXT, - data_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id - ON auth_sessions(user_id)`, - `CREATE TABLE IF NOT EXISTS audit_events ( - id TEXT PRIMARY KEY, - actor_type TEXT NOT NULL, - actor_id TEXT, - event_type TEXT NOT NULL, - target_type TEXT, - target_id TEXT, - data_json TEXT, - created_at TEXT NOT NULL - )`, - `CREATE INDEX IF NOT EXISTS idx_audit_events_target - ON audit_events(target_type, target_id)`, - `CREATE TABLE IF NOT EXISTS device_codes ( - id TEXT PRIMARY KEY, - device_code TEXT NOT NULL UNIQUE, - user_code TEXT NOT NULL UNIQUE, - requested_scopes_json TEXT NOT NULL, - expires_at TEXT NOT NULL, - interval_seconds INTEGER NOT NULL, - status TEXT NOT NULL, - user_id TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - )`, -]; - -export type DeviceCodeRow = { - id: string; - device_code: string; - user_code: string; - requested_scopes_json: string; - expires_at: string; - interval_seconds: number; - status: string; - user_id: string | null; -}; - -export type UserRow = { - id: string; - email: string | null; - username: string | null; - display_name: string | null; - status: string; - metadata_json: string | null; - created_at: string; - updated_at: string; -}; - -export type PrincipalRecord = { - principal: ApiPrincipal; - userId: string; -}; - -export interface PersonalAccessTokenResult { - id: string; - token: string; - prefix: string; - name: string; - expiresAt: string | null; -} - -export interface ServiceCredentialResult { - id: string; - serviceId: string; - secret: string; -} - -export function now() { - return new Date(); -} - -export function isoNow() { - return now().toISOString(); -} - -export function addSeconds(date: Date, seconds: number) { - return new Date(date.getTime() + seconds * 1000); -} - -export function parseJson(value: string | null | undefined, fallback: T): T { - if (!value) return fallback; - try { - return JSON.parse(value) as T; - } catch { - return fallback; - } -} - -export function stableHash(value: string, secret: string) { - return createHash('sha256').update(`${secret}:${value}`).digest('hex'); -} - -export function equalHash(left: string, right: string) { - const leftBuffer = Buffer.from(left); - const rightBuffer = Buffer.from(right); - return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer); -} - -export abstract class D1AuthStoreCore { - protected abstract upsertServiceCredential(input: { serviceId: string; name: string; secret: string; roles?: string[]; permissions?: string[] }): Promise; - - protected initializationPromise: Promise | null = null; - - constructor( - protected readonly config: ApiConfig, - protected readonly db: D1DatabaseLike, - ) {} - - protected async run(query: string, params: unknown[] = []) { - await this.db.prepare(query).bind(...params).run(); - } - - protected async first>(query: string, params: unknown[] = []) { - return this.db.prepare(query).bind(...params).first(); - } - - protected async all>(query: string, params: unknown[] = []) { - const result = await this.db.prepare(query).bind(...params).all(); - return result.results ?? []; - } - - protected ensureInitialized() { - if (!this.initializationPromise) { - this.initializationPromise = this.ensureAuthSchema() - .then(() => this.seedCatalog()) - .then(() => this.seedConfiguredServices()); - } - return this.initializationPromise; - } - - protected async ensureAuthSchema() { - for (const statement of AUTH_SCHEMA_SQL) await this.run(statement); - const result = await this.db.prepare('PRAGMA table_info(users)').all<{ name: string }>(); - const columns = new Set((result.results ?? []).map((row) => row.name)); - if (!columns.has('username')) { - await this.run('ALTER TABLE users ADD COLUMN username TEXT'); - } - await this.run('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username)'); - } - - protected async seedCatalog() { - const createdAt = isoNow(); - const seeded = await this.first<{ key: string }>( - `SELECT key FROM permissions WHERE key = '*:*:*' LIMIT 1`, - ); - const adminRole = await this.first<{ key: string }>( - `SELECT key FROM roles WHERE key = 'platform_admin' LIMIT 1`, - ); - if (seeded?.key && adminRole?.key) { - return; - } - for (const permission of DEFAULT_PERMISSIONS) { - await this.run( - `INSERT OR IGNORE INTO permissions (id, key, resource, action, scope, description, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - [randomUUID(), permission.key, permission.resource, permission.action, permission.scope, permission.description, createdAt], - ); - } - for (const role of DEFAULT_ROLES) { - await this.run( - `INSERT OR IGNORE INTO roles (id, key, description, created_at) - VALUES (?, ?, ?, ?)`, - [randomUUID(), role.key, role.description, createdAt], - ); - const roleRow = await this.first<{ id: string }>(`SELECT id FROM roles WHERE key = ?`, [role.key]); - if (!roleRow) continue; - for (const permissionKey of role.permissions) { - const permissionRow = await this.first<{ id: string }>(`SELECT id FROM permissions WHERE key = ?`, [permissionKey]); - if (permissionRow) { - await this.run( - `INSERT OR IGNORE INTO role_permissions (role_id, permission_id, created_at) - VALUES (?, ?, ?)`, - [roleRow.id, permissionRow.id, createdAt], - ); - } - } - } - } - - protected async seedConfiguredServices() { - if (!this.config.webServiceSecret) return; - await this.upsertServiceCredential({ - serviceId: this.config.webServiceId, - name: 'Trusted web tier', - secret: this.config.webServiceSecret, - roles: ['market_admin'], - permissions: ['services:impersonate:global'], - }); - } - - protected async loadUser(userId: string) { - return this.first(`SELECT * FROM users WHERE id = ?`, [userId]); - } - - protected async loadIdentityByProvider(provider: string, providerSubject: string) { - return this.first<{ id: string; user_id: string; email: string | null; profile_json: string | null }>( - `SELECT * FROM user_identities WHERE provider = ? AND provider_subject = ?`, - [provider, providerSubject], - ); - } - - protected async loadUserByVerifiedEmail(email: string) { - return this.first( - `SELECT * FROM users WHERE LOWER(email) = LOWER(?) AND status = 'active' LIMIT 1`, - [email], - ); - } - - protected async loadUserByUsername(username: string) { - return this.first( - `SELECT * FROM users WHERE LOWER(username) = LOWER(?) AND status = 'active' LIMIT 1`, - [username], - ); - } - - protected canAdoptUsernameMatch(identity: UserIdentityProfileInput, user: UserRow | null) { - if (!user?.id || !identity.username) return false; - const profile = identity.profile && typeof identity.profile === 'object' ? identity.profile : {}; - if (identity.provider === 'acceptance' || profile.acceptance === true) return true; - const existingEmail = typeof user.email === 'string' ? user.email.trim().toLowerCase() : ''; - const requestedEmail = typeof identity.email === 'string' ? identity.email.trim().toLowerCase() : ''; - return Boolean(requestedEmail && existingEmail && requestedEmail === existingEmail && identity.emailVerified); - } - - protected async rolesForUser(userId: string) { - const rows = await this.all<{ key: string }>( - `SELECT roles.key AS key - FROM user_role_bindings - INNER JOIN roles ON roles.id = user_role_bindings.role_id - WHERE user_role_bindings.user_id = ?`, - [userId], - ); - return rows.map((row) => row.key); - } - - protected async permissionsForUser(userId: string) { - const rows = await this.all<{ key: string }>( - `SELECT DISTINCT permissions.key AS key - FROM user_role_bindings - INNER JOIN role_permissions ON role_permissions.role_id = user_role_bindings.role_id - INNER JOIN permissions ON permissions.id = role_permissions.permission_id - WHERE user_role_bindings.user_id = ?`, - [userId], - ); - return rows.map((row) => row.key); - } - - protected async permissionsForRoles(roleKeys: string[]) { - if (roleKeys.length === 0) { - return []; - } - const placeholders = roleKeys.map(() => '?').join(', '); - const rows = await this.all<{ key: string }>( - `SELECT DISTINCT permissions.key AS key - FROM roles - INNER JOIN role_permissions ON role_permissions.role_id = roles.id - INNER JOIN permissions ON permissions.id = role_permissions.permission_id - WHERE roles.key IN (${placeholders})`, - roleKeys, - ); - return rows.map((row) => row.key); - } - - protected scopesForPrincipal(permissions: string[]) { - const scopes = new Set(['auth:me']); - if (permissions.includes('*:*:*') || permissions.includes('sdk:execute:global')) scopes.add('sdk'); - if (permissions.includes('*:*:*') || permissions.includes('agent:execute:global')) scopes.add('agent'); - if (permissions.includes('*:*:*') || permissions.includes('operations:execute:global')) scopes.add('operations'); - return [...scopes]; - } - - protected async principalForUser(userId: string): Promise { - const user = await this.loadUser(userId); - if (!user) { - throw new Error(`Unknown user "${userId}".`); - } - const roles = await this.rolesForUser(userId); - const permissions = await this.permissionsForUser(userId); - return { - userId, - principal: { - id: user.id, - displayName: user.display_name ?? undefined, - roles, - permissions, - scopes: this.scopesForPrincipal(permissions), - metadata: { - ...parseJson(user.metadata_json, {}), - username: user.username ?? undefined, - }, - }, - }; - } - - protected async assignRole(userId: string, roleKey: string) { - const role = await this.first<{ id: string }>(`SELECT id FROM roles WHERE key = ?`, [roleKey]); - if (!role) return; - await this.run( - `INSERT OR IGNORE INTO user_role_bindings (id, user_id, role_id, created_at) - VALUES (?, ?, ?, ?)`, - [randomUUID(), userId, role.id, isoNow()], - ); - } - - protected async replaceRoles(userId: string, roleKeys: string[]) { - await this.run(`DELETE FROM user_role_bindings WHERE user_id = ?`, [userId]); - for (const roleKey of roleKeys) { - await this.assignRole(userId, roleKey); - } - } - - protected async bootstrapRolesForUser(userId: string, identity: UserIdentityProfileInput) { - await this.assignRole(userId, 'member'); - if ((await this.rolesForUser(userId)).includes('platform_admin')) return; - const allowlist = this.config.bootstrapAdminAllowlist; - const email = identity.email?.trim().toLowerCase() ?? ''; - const providerSubject = `${identity.provider}:${identity.providerSubject}`; - if (allowlist.includes(email) || allowlist.includes(providerSubject)) { - await this.assignRole(userId, 'platform_admin'); - await this.writeAuditEvent({ - actorType: 'system', - actorId: null, - eventType: 'auth.bootstrap_admin', - targetType: 'user', - targetId: userId, - data: { matched: allowlist.includes(providerSubject) ? providerSubject : email }, - }); - } - } - - protected async writeAuditEvent(input: { - actorType: string; - actorId: string | null; - eventType: string; - targetType: string | null; - targetId: string | null; - data?: Record; - }) { - await this.run( - `INSERT INTO audit_events (id, actor_type, actor_id, event_type, target_type, target_id, data_json, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [ - randomUUID(), - input.actorType, - input.actorId, - input.eventType, - input.targetType, - input.targetId, - JSON.stringify(input.data ?? {}), - isoNow(), - ], - ); - } - - protected userMetadata(identity: UserIdentityProfileInput, existingUsername: string | null = null) { - const profile = identity.profile ?? {}; - return { - emailVerified: identity.emailVerified ?? false, - authProvider: identity.provider, - username: identity.username ?? existingUsername, - firstName: typeof profile.firstName === 'string' ? profile.firstName : null, - lastName: typeof profile.lastName === 'string' ? profile.lastName : null, - }; - } -} - diff --git a/src/api/auth/d1-store.ts b/src/api/auth/d1-store.ts deleted file mode 100644 index 1d24f626..00000000 --- a/src/api/auth/d1-store.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import type { ApiCredential, ApiPrincipal, DeviceCodeApproveRequest, DeviceCodePollRequest, DeviceCodePollResponse, DeviceCodeStartRequest, DeviceCodeStartResponse, TokenRefreshRequest, TokenRefreshResponse, TrustedUserAssertionClaims } from '../types.ts'; -import { createAccessToken, nextOpaqueToken, principalFromAccessTokenPayload, verifyAccessToken } from './tokens.ts'; -import { - addSeconds, - approvalUrl, - equalHash, - isoNow, - now, - parseJson, - stableHash, - type DeviceCodeRow, - type PersonalAccessTokenResult, - type ServiceCredentialResult, -} from './d1-store-core.ts'; -import { D1AuthUserStore } from './d1-user-store.ts'; - -export type { PersonalAccessTokenResult, ServiceCredentialResult } from './d1-store-core.ts'; - -export class D1AuthStore extends D1AuthUserStore { - async startDeviceFlow(request: DeviceCodeStartRequest): Promise { - await this.ensureInitialized(); - const current = now(); - const expiresAt = addSeconds(current, this.config.deviceCodeTtlSeconds); - const deviceCode = nextOpaqueToken('device'); - const userCode = `${Math.random().toString(36).slice(2, 6).toUpperCase()}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`; - await this.run( - `INSERT INTO device_codes (id, device_code, user_code, requested_scopes_json, expires_at, interval_seconds, status, user_id, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, ?, ?)`, - [ - randomUUID(), - deviceCode, - userCode, - JSON.stringify(request.scopes?.length ? request.scopes : ['auth:me']), - expiresAt.toISOString(), - this.config.deviceCodePollIntervalSeconds, - current.toISOString(), - current.toISOString(), - ], - ); - return { - ok: true, - deviceCode, - userCode, - verificationUri: approvalUrl(this.config.baseUrl), - verificationUriComplete: approvalUrl(this.config.baseUrl, userCode), - intervalSeconds: this.config.deviceCodePollIntervalSeconds, - expiresAt: expiresAt.toISOString(), - expiresInSeconds: this.config.deviceCodeTtlSeconds, - }; - } - - async approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{ ok: true }> { - await this.ensureInitialized(); - const row = await this.first(`SELECT * FROM device_codes WHERE user_code = ?`, [request.userCode]); - if (!row || new Date(row.expires_at).getTime() <= Date.now()) { - throw new Error('Device code approval failed because the user code is unknown or expired.'); - } - let userId = request.principalId; - if (!(await this.loadUser(userId))) { - const createdAt = isoNow(); - await this.run( - `INSERT INTO users (id, email, display_name, status, metadata_json, created_at, updated_at) - VALUES (?, NULL, ?, 'active', ?, ?, ?)`, - [userId, request.displayName ?? null, JSON.stringify(request.metadata ?? {}), createdAt, createdAt], - ); - await this.assignRole(userId, 'member'); - } - await this.run(`UPDATE device_codes SET status = 'approved', user_id = ?, updated_at = ? WHERE id = ?`, [userId, isoNow(), row.id]); - await this.writeAuditEvent({ - actorType: 'user', - actorId: userId, - eventType: 'auth.device_approved', - targetType: 'device_code', - targetId: row.id, - }); - return { ok: true }; - } - - async pollDeviceFlow(request: DeviceCodePollRequest): Promise { - await this.ensureInitialized(); - const row = await this.first(`SELECT * FROM device_codes WHERE device_code = ?`, [request.deviceCode]); - if (!row) { - return { ok: false, status: 'invalid', error: 'Unknown device code.' }; - } - if (new Date(row.expires_at).getTime() <= Date.now()) { - return { ok: false, status: 'expired', error: 'Device code expired.' }; - } - if (row.status === 'pending' || !row.user_id) { - return { ok: true, status: 'pending', intervalSeconds: row.interval_seconds }; - } - if (row.status === 'used') { - return { ok: false, status: 'already_used', error: 'Device code already used.' }; - } - - await this.run(`UPDATE device_codes SET status = 'used', updated_at = ? WHERE id = ?`, [isoNow(), row.id]); - const principalRecord = await this.principalForUser(row.user_id); - const refreshToken = nextOpaqueToken('refresh'); - const sessionId = randomUUID(); - const refreshTokenHash = stableHash(refreshToken, this.config.authSecret); - const expiresAt = addSeconds(now(), this.config.accessTokenTtlSeconds); - const refreshExpiresAt = addSeconds(now(), this.config.refreshTokenTtlSeconds); - await this.run( - `INSERT INTO auth_sessions (id, user_id, session_type, refresh_token_hash, scopes_json, expires_at, revoked_at, data_json, created_at, updated_at) - VALUES (?, ?, 'device', ?, ?, ?, NULL, ?, ?, ?)`, - [ - sessionId, - row.user_id, - refreshTokenHash, - row.requested_scopes_json, - refreshExpiresAt.toISOString(), - JSON.stringify({ deviceCodeId: row.id }), - isoNow(), - isoNow(), - ], - ); - const requestedScopes = parseJson(row.requested_scopes_json, principalRecord.principal.scopes); - const accessToken = createAccessToken({ - sub: principalRecord.principal.id, - displayName: principalRecord.principal.displayName, - scopes: requestedScopes, - roles: principalRecord.principal.roles, - permissions: principalRecord.principal.permissions, - metadata: principalRecord.principal.metadata, - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(expiresAt.getTime() / 1000), - iss: this.config.issuer, - jti: randomUUID(), - tokenType: 'access', - }, this.config.authSecret); - return { - ok: true, - status: 'approved', - accessToken, - refreshToken, - tokenType: 'Bearer', - expiresAt: expiresAt.toISOString(), - expiresInSeconds: this.config.accessTokenTtlSeconds, - principal: { - ...principalRecord.principal, - scopes: requestedScopes, - }, - }; - } - - async refreshAccessToken(request: TokenRefreshRequest): Promise { - await this.ensureInitialized(); - const refreshHash = stableHash(request.refreshToken, this.config.authSecret); - const row = await this.first<{ id: string; user_id: string; scopes_json: string; expires_at: string }>( - `SELECT * FROM auth_sessions WHERE refresh_token_hash = ? AND revoked_at IS NULL`, - [refreshHash], - ); - if (!row || new Date(row.expires_at).getTime() <= Date.now()) { - throw new Error('Refresh token is invalid or expired.'); - } - const principalRecord = await this.principalForUser(row.user_id); - const nextRefreshToken = nextOpaqueToken('refresh'); - const nextRefreshHash = stableHash(nextRefreshToken, this.config.authSecret); - const nextRefreshExpiresAt = addSeconds(now(), this.config.refreshTokenTtlSeconds); - await this.run( - `UPDATE auth_sessions SET refresh_token_hash = ?, expires_at = ?, updated_at = ? WHERE id = ?`, - [nextRefreshHash, nextRefreshExpiresAt.toISOString(), isoNow(), row.id], - ); - const requestedScopes = parseJson(row.scopes_json, principalRecord.principal.scopes); - const expiresAt = addSeconds(now(), this.config.accessTokenTtlSeconds); - const accessToken = createAccessToken({ - sub: principalRecord.principal.id, - displayName: principalRecord.principal.displayName, - scopes: requestedScopes, - roles: principalRecord.principal.roles, - permissions: principalRecord.principal.permissions, - metadata: principalRecord.principal.metadata, - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(expiresAt.getTime() / 1000), - iss: this.config.issuer, - jti: randomUUID(), - tokenType: 'access', - }, this.config.authSecret); - return { - ok: true, - accessToken, - refreshToken: nextRefreshToken, - tokenType: 'Bearer', - expiresAt: expiresAt.toISOString(), - expiresInSeconds: this.config.accessTokenTtlSeconds, - principal: { - ...principalRecord.principal, - scopes: requestedScopes, - }, - }; - } - - async createPersonalAccessToken(userId: string, input: { name: string; scopes?: string[]; expiresAt?: string | null }) { - await this.ensureInitialized(); - const nowIso = isoNow(); - const token = nextOpaqueToken('pat'); - const id = randomUUID(); - const tokenHash = stableHash(token, this.config.authSecret); - const prefix = token.slice(0, 12); - await this.run( - `INSERT INTO api_tokens (id, user_id, kind, name, token_prefix, token_hash, scopes_json, expires_at, last_used_at, revoked_at, metadata_json, created_at, updated_at) - VALUES (?, ?, 'personal_access_token', ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?)`, - [ - id, - userId, - input.name, - prefix, - tokenHash, - JSON.stringify(input.scopes?.length ? input.scopes : ['auth:me']), - input.expiresAt ?? null, - JSON.stringify({}), - nowIso, - nowIso, - ], - ); - await this.writeAuditEvent({ - actorType: 'user', - actorId: userId, - eventType: 'auth.pat_created', - targetType: 'api_token', - targetId: id, - data: { name: input.name }, - }); - return { id, token, prefix, name: input.name, expiresAt: input.expiresAt ?? null } satisfies PersonalAccessTokenResult; - } - - async listPersonalAccessTokens(userId: string) { - await this.ensureInitialized(); - return this.all<{ - id: string; - name: string; - token_prefix: string; - expires_at: string | null; - last_used_at: string | null; - revoked_at: string | null; - created_at: string; - }>( - `SELECT id, name, token_prefix, expires_at, last_used_at, revoked_at, created_at - FROM api_tokens - WHERE user_id = ? - ORDER BY created_at DESC`, - [userId], - ); - } - - async revokePersonalAccessToken(userId: string, tokenId: string) { - await this.ensureInitialized(); - await this.run(`UPDATE api_tokens SET revoked_at = ? WHERE id = ? AND user_id = ?`, [isoNow(), tokenId, userId]); - await this.writeAuditEvent({ - actorType: 'user', - actorId: userId, - eventType: 'auth.pat_revoked', - targetType: 'api_token', - targetId: tokenId, - }); - } - - async upsertServiceCredential(input: { serviceId: string; name: string; secret: string; roles?: string[]; permissions?: string[] }) { - const nowIso = isoNow(); - const existing = await this.first<{ id: string }>(`SELECT id FROM service_credentials WHERE service_id = ?`, [input.serviceId]); - const secretHash = stableHash(input.secret, this.config.authSecret); - if (existing) { - await this.run( - `UPDATE service_credentials - SET name = ?, secret_hash = ?, roles_json = ?, permissions_json = ?, revoked_at = NULL, updated_at = ? - WHERE id = ?`, - [input.name, secretHash, JSON.stringify(input.roles ?? []), JSON.stringify(input.permissions ?? []), nowIso, existing.id], - ); - return existing.id; - } - const id = randomUUID(); - await this.run( - `INSERT INTO service_credentials (id, service_id, name, secret_hash, roles_json, permissions_json, revoked_at, last_used_at, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)`, - [id, input.serviceId, input.name, secretHash, JSON.stringify(input.roles ?? []), JSON.stringify(input.permissions ?? []), nowIso, nowIso], - ); - return id; - } - - async createServiceCredential(input: { serviceId: string; name: string; roles?: string[]; permissions?: string[] }): Promise { - await this.ensureInitialized(); - const secret = nextOpaqueToken('svc'); - const id = await this.upsertServiceCredential({ ...input, secret }); - return { id, serviceId: input.serviceId, secret }; - } - - async rotateServiceCredential(serviceId: string) { - await this.ensureInitialized(); - const row = await this.first<{ name: string; roles_json: string; permissions_json: string }>( - `SELECT name, roles_json, permissions_json FROM service_credentials WHERE service_id = ? AND revoked_at IS NULL`, - [serviceId], - ); - if (!row) { - throw new Error(`Unknown active service credential "${serviceId}".`); - } - return this.createServiceCredential({ - serviceId, - name: row.name, - roles: parseJson(row.roles_json, []), - permissions: parseJson(row.permissions_json, []), - }); - } - - async authenticateBearerToken(token: string): Promise<{ principal: ApiPrincipal; credential: ApiCredential } | null> { - await this.ensureInitialized(); - const patHash = stableHash(token, this.config.authSecret); - const pat = await this.first<{ - id: string; - user_id: string; - name: string; - scopes_json: string; - expires_at: string | null; - revoked_at: string | null; - }>( - `SELECT id, user_id, name, scopes_json, expires_at, revoked_at - FROM api_tokens - WHERE token_hash = ?`, - [patHash], - ); - if (pat && !pat.revoked_at && (!pat.expires_at || new Date(pat.expires_at).getTime() > Date.now())) { - await this.run(`UPDATE api_tokens SET last_used_at = ? WHERE id = ?`, [isoNow(), pat.id]); - const principal = (await this.principalForUser(pat.user_id)).principal; - return { - principal: { ...principal, scopes: parseJson(pat.scopes_json, principal.scopes) }, - credential: { type: 'personal_access_token', id: pat.id, label: pat.name }, - }; - } - const payload = verifyAccessToken(token, this.config.authSecret); - if (!payload) return null; - return { - principal: principalFromAccessTokenPayload(payload), - credential: { - type: payload.tokenType === 'service' ? 'service_token' : 'access_token', - id: payload.jti, - label: payload.tokenType, - }, - }; - } - - async authenticateService(serviceId: string, secret: string): Promise<{ principal: ApiPrincipal; credential: ApiCredential } | null> { - await this.ensureInitialized(); - const row = await this.first<{ - id: string; - name: string; - secret_hash: string; - roles_json: string; - permissions_json: string; - revoked_at: string | null; - }>( - `SELECT id, name, secret_hash, roles_json, permissions_json, revoked_at - FROM service_credentials - WHERE service_id = ?`, - [serviceId], - ); - if (!row || row.revoked_at) return null; - const incomingHash = stableHash(secret, this.config.authSecret); - if (!equalHash(row.secret_hash, incomingHash)) return null; - await this.run(`UPDATE service_credentials SET last_used_at = ?, updated_at = ? WHERE id = ?`, [isoNow(), isoNow(), row.id]); - const roles = parseJson(row.roles_json, []); - const permissions = [ - ...new Set([ - ...await this.permissionsForRoles(roles), - ...parseJson(row.permissions_json, []), - ]), - ]; - return { - principal: { - id: serviceId, - displayName: row.name, - roles, - permissions, - scopes: this.scopesForPrincipal(permissions), - metadata: { serviceId }, - }, - credential: { type: 'service_secret', id: row.id, label: row.name }, - }; - } - - async exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims) { - await this.ensureInitialized(); - const principalRecord = await this.principalForUser(claims.userId); - const expiresAt = addSeconds(now(), this.config.webExchangeTtlSeconds); - const accessToken = createAccessToken({ - sub: principalRecord.principal.id, - displayName: principalRecord.principal.displayName, - scopes: principalRecord.principal.scopes, - roles: principalRecord.principal.roles, - permissions: principalRecord.principal.permissions, - metadata: { - ...principalRecord.principal.metadata, - actingSessionId: claims.sessionId, - identityId: claims.identityId, - teamId: claims.teamId ?? null, - projectId: claims.projectId ?? null, - membershipId: claims.membershipId ?? null, - teamRoles: [...new Set((claims.teamRoles ?? []).filter((entry) => typeof entry === 'string' && entry.trim()))], - teamCapabilities: [...new Set((claims.teamCapabilities ?? []).filter((entry) => typeof entry === 'string' && entry.trim()))], - authTime: claims.authTime, - }, - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(expiresAt.getTime() / 1000), - iss: this.config.issuer, - jti: randomUUID(), - tokenType: 'access', - }, this.config.authSecret); - await this.writeAuditEvent({ - actorType: 'service', - actorId: this.config.webServiceId, - eventType: 'auth.web_exchange', - targetType: 'user', - targetId: claims.userId, - data: { sessionId: claims.sessionId }, - }); - return { - ok: true as const, - accessToken, - tokenType: 'Bearer' as const, - expiresAt: expiresAt.toISOString(), - expiresInSeconds: this.config.webExchangeTtlSeconds, - principal: principalRecord.principal, - }; - } -} - diff --git a/src/api/auth/d1-user-store.ts b/src/api/auth/d1-user-store.ts deleted file mode 100644 index fe1003cb..00000000 --- a/src/api/auth/d1-user-store.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import type { UserIdentityProfileInput } from '../types.ts'; -import { D1AuthStoreCore, isoNow } from './d1-store-core.ts'; - -export abstract class D1AuthUserStore extends D1AuthStoreCore { - async syncUser(identity: UserIdentityProfileInput) { - await this.ensureInitialized(); - const nowIso = isoNow(); - const existingIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject); - let userId = existingIdentity?.user_id; - if (!userId) { - const emailLinkedUser = identity.email && identity.emailVerified ? await this.loadUserByVerifiedEmail(identity.email) : null; - const usernameLinkedUser = !emailLinkedUser && identity.username ? await this.loadUserByUsername(identity.username) : null; - const linkedUser = emailLinkedUser ?? (this.canAdoptUsernameMatch(identity, usernameLinkedUser) ? usernameLinkedUser : null); - userId = linkedUser?.id ?? randomUUID(); - if (linkedUser) { - await this.run( - `UPDATE users - SET email = COALESCE(?, email), - username = COALESCE(username, ?), - display_name = COALESCE(?, display_name), - metadata_json = ?, - updated_at = ? - WHERE id = ?`, - [ - identity.email ?? null, - identity.username ?? null, - identity.displayName ?? null, - JSON.stringify(this.userMetadata(identity, linkedUser.username ?? null)), - nowIso, - userId, - ], - ); - } else { - await this.run( - `INSERT INTO users (id, email, username, display_name, status, metadata_json, created_at, updated_at) - VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`, - [ - userId, - identity.email ?? null, - identity.username ?? null, - identity.displayName ?? null, - JSON.stringify(this.userMetadata(identity)), - nowIso, - nowIso, - ], - ); - } - await this.run( - `INSERT INTO user_identities (id, user_id, provider, provider_subject, email, email_verified, profile_json, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - randomUUID(), - userId, - identity.provider, - identity.providerSubject, - identity.email ?? null, - identity.emailVerified ? 1 : 0, - JSON.stringify(identity.profile ?? {}), - nowIso, - nowIso, - ], - ); - } else { - await this.run( - `UPDATE users - SET email = COALESCE(?, email), - username = COALESCE(username, ?), - display_name = COALESCE(?, display_name), - metadata_json = ?, - updated_at = ? - WHERE id = ?`, - [ - identity.email ?? null, - identity.username ?? null, - identity.displayName ?? null, - JSON.stringify(this.userMetadata(identity)), - nowIso, - userId, - ], - ); - await this.run( - `UPDATE user_identities - SET email = ?, email_verified = ?, profile_json = ?, updated_at = ? - WHERE provider = ? AND provider_subject = ?`, - [ - identity.email ?? null, - identity.emailVerified ? 1 : 0, - JSON.stringify(identity.profile ?? {}), - nowIso, - identity.provider, - identity.providerSubject, - ], - ); - } - await this.bootstrapRolesForUser(userId, identity); - await this.writeAuditEvent({ - actorType: 'service', - actorId: this.config.webServiceId, - eventType: 'auth.user_synced', - targetType: 'user', - targetId: userId, - data: { provider: identity.provider }, - }); - const principal = await this.principalForUser(userId); - const syncedIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject); - return { - ...principal, - identityId: syncedIdentity?.id ?? null, - }; - } - - async createUser(input: { email?: string | null; username?: string | null; displayName?: string | null; metadata?: Record }) { - await this.ensureInitialized(); - const timestamp = isoNow(); - const userId = randomUUID(); - await this.run( - `INSERT INTO users (id, email, username, display_name, status, metadata_json, created_at, updated_at) - VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`, - [ - userId, - input.email?.trim() || null, - input.username?.trim().toLowerCase() || null, - input.displayName?.trim() || null, - JSON.stringify(input.metadata ?? {}), - timestamp, - timestamp, - ], - ); - await this.assignRole(userId, 'member'); - await this.writeAuditEvent({ - actorType: 'service', - actorId: this.config.webServiceId, - eventType: 'auth.user_created', - targetType: 'user', - targetId: userId, - data: { source: 'admin' }, - }); - return this.principalForUser(userId); - } - - async setUserRoles(userId: string, roles: string[]) { - await this.ensureInitialized(); - const requestedRoles = [...new Set(roles.map((role) => role.trim()).filter(Boolean))]; - await this.replaceRoles(userId, requestedRoles.length > 0 ? requestedRoles : ['member']); - await this.writeAuditEvent({ - actorType: 'service', - actorId: this.config.webServiceId, - eventType: 'auth.user_roles_set', - targetType: 'user', - targetId: userId, - data: { roles: requestedRoles }, - }); - return this.principalForUser(userId); - } -} - diff --git a/src/api/auth/memory-provider.ts b/src/api/auth/memory-provider.ts deleted file mode 100644 index 920e95cd..00000000 --- a/src/api/auth/memory-provider.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import type { - ApiAuthProvider, - ApiConfig, - ApiCredential, - ApiPrincipal, - DeviceCodeApproveRequest, - DeviceCodePollRequest, - DeviceCodePollResponse, - DeviceCodeStartRequest, - DeviceCodeStartResponse, - TokenRefreshRequest, - TokenRefreshResponse, - TrustedUserAssertionClaims, - UserIdentityProfileInput, -} from '../types.ts'; -import { - createAccessToken, - nextOpaqueToken, - principalFromAccessTokenPayload, - verifyAccessToken, -} from './tokens.ts'; - -type DeviceFlowRecord = { - deviceCode: string; - userCode: string; - requestedScopes: string[]; - expiresAt: number; - intervalSeconds: number; - status: 'pending' | 'approved' | 'used'; - principal: ApiPrincipal | null; -}; - -type RefreshSessionRecord = { - principal: ApiPrincipal; - expiresAt: number; -}; - -function nowSeconds() { - return Math.floor(Date.now() / 1000); -} - -function formatExpiry(epochSeconds: number) { - return new Date(epochSeconds * 1000).toISOString(); -} - -function nextUserCode() { - return Math.random().toString(36).slice(2, 6).toUpperCase() - + '-' - + Math.random().toString(36).slice(2, 6).toUpperCase(); -} - -function approvalUrl(baseUrl: string, userCode?: string | null) { - const url = new URL('/auth/device/approve', `${baseUrl.replace(/\/+$/u, '')}/`); - if (userCode) { - url.searchParams.set('user_code', userCode); - } - return url.toString(); -} - -export class MemoryDeviceCodeAuthProvider implements ApiAuthProvider { - readonly id = 'memory'; - private readonly devices = new Map(); - private readonly refreshSessions = new Map(); - - constructor(private readonly config: ApiConfig) {} - - async startDeviceFlow(request: DeviceCodeStartRequest): Promise { - const expiresAt = nowSeconds() + this.config.deviceCodeTtlSeconds; - const deviceCode = nextOpaqueToken('device'); - const userCode = nextUserCode(); - this.devices.set(deviceCode, { - deviceCode, - userCode, - requestedScopes: request.scopes?.length ? [...request.scopes] : ['templates:read', 'auth:me', 'sdk', 'operations'], - expiresAt, - intervalSeconds: this.config.deviceCodePollIntervalSeconds, - status: 'pending', - principal: null, - }); - - return { - ok: true, - deviceCode, - userCode, - verificationUri: approvalUrl(this.config.baseUrl), - verificationUriComplete: approvalUrl(this.config.baseUrl, userCode), - intervalSeconds: this.config.deviceCodePollIntervalSeconds, - expiresAt: formatExpiry(expiresAt), - expiresInSeconds: this.config.deviceCodeTtlSeconds, - }; - } - - async approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{ ok: true }> { - const record = [...this.devices.values()].find((entry) => entry.userCode === request.userCode); - if (!record || record.expiresAt <= nowSeconds()) { - throw new Error('Device code approval failed because the user code is unknown or expired.'); - } - - record.status = 'approved'; - record.principal = { - id: request.principalId, - displayName: request.displayName, - scopes: request.scopes?.length ? [...request.scopes] : [...record.requestedScopes], - roles: ['member'], - permissions: ['auth:read:self'], - metadata: request.metadata, - }; - return { ok: true }; - } - - async pollDeviceFlow(request: DeviceCodePollRequest): Promise { - const record = this.devices.get(request.deviceCode); - if (!record) { - return { ok: false, status: 'invalid', error: 'Unknown device code.' }; - } - if (record.expiresAt <= nowSeconds()) { - this.devices.delete(request.deviceCode); - return { ok: false, status: 'expired', error: 'Device code expired.' }; - } - if (record.status === 'pending' || !record.principal) { - return { - ok: true, - status: 'pending', - intervalSeconds: record.intervalSeconds, - }; - } - if (record.status === 'used') { - return { ok: false, status: 'already_used', error: 'Device code already used.' }; - } - - record.status = 'used'; - const refreshToken = nextOpaqueToken('refresh'); - const expiresAt = nowSeconds() + this.config.accessTokenTtlSeconds; - const accessToken = createAccessToken({ - sub: record.principal.id, - displayName: record.principal.displayName, - scopes: record.principal.scopes, - roles: record.principal.roles, - permissions: record.principal.permissions, - metadata: record.principal.metadata, - iat: nowSeconds(), - exp: expiresAt, - iss: this.config.issuer, - jti: randomUUID(), - tokenType: 'access', - }, this.config.authSecret); - - this.refreshSessions.set(refreshToken, { - principal: record.principal, - expiresAt: nowSeconds() + this.config.refreshTokenTtlSeconds, - }); - - return { - ok: true, - status: 'approved', - accessToken, - refreshToken, - tokenType: 'Bearer', - expiresAt: formatExpiry(expiresAt), - expiresInSeconds: this.config.accessTokenTtlSeconds, - principal: record.principal, - }; - } - - async refreshAccessToken(request: TokenRefreshRequest): Promise { - const session = this.refreshSessions.get(request.refreshToken); - if (!session || session.expiresAt <= nowSeconds()) { - throw new Error('Refresh token is invalid or expired.'); - } - - const nextRefreshToken = nextOpaqueToken('refresh'); - this.refreshSessions.delete(request.refreshToken); - this.refreshSessions.set(nextRefreshToken, { - principal: session.principal, - expiresAt: nowSeconds() + this.config.refreshTokenTtlSeconds, - }); - - const expiresAt = nowSeconds() + this.config.accessTokenTtlSeconds; - const accessToken = createAccessToken({ - sub: session.principal.id, - displayName: session.principal.displayName, - scopes: session.principal.scopes, - roles: session.principal.roles, - permissions: session.principal.permissions, - metadata: session.principal.metadata, - iat: nowSeconds(), - exp: expiresAt, - iss: this.config.issuer, - jti: randomUUID(), - tokenType: 'access', - }, this.config.authSecret); - - return { - ok: true, - accessToken, - refreshToken: nextRefreshToken, - tokenType: 'Bearer', - expiresAt: formatExpiry(expiresAt), - expiresInSeconds: this.config.accessTokenTtlSeconds, - principal: session.principal, - }; - } - - async authenticateBearerToken(token: string) { - const payload = verifyAccessToken(token, this.config.authSecret); - return payload - ? { - principal: principalFromAccessTokenPayload(payload), - credential: { - type: 'access_token', - id: payload.jti, - label: payload.tokenType, - } satisfies ApiCredential, - } - : null; - } - - async authenticateServiceCredential(_serviceId: string, _secret: string) { - return null; - } - - async createPersonalAccessToken( - _userId: string, - _input: { name: string; scopes?: string[]; expiresAt?: string | null }, - ): Promise<{ id: string; token: string; prefix: string; name: string; expiresAt: string | null }> { - throw new Error('Personal access tokens are unavailable in the memory auth provider.'); - } - - async listPersonalAccessTokens() { - return []; - } - - async revokePersonalAccessToken() {} - - async syncUserIdentity(identity: UserIdentityProfileInput) { - return { - userId: identity.providerSubject, - identityId: identity.providerSubject, - principal: { - id: identity.providerSubject, - displayName: identity.displayName ?? undefined, - scopes: ['auth:me'], - roles: ['member'], - permissions: ['auth:read:self'], - metadata: { - ...(identity.profile ?? {}), - username: identity.username ?? undefined, - firstName: typeof identity.profile?.firstName === 'string' ? identity.profile.firstName : undefined, - lastName: typeof identity.profile?.lastName === 'string' ? identity.profile.lastName : undefined, - }, - }, - }; - } - - async createServiceToken( - _input: { serviceId: string; name: string; roles?: string[]; permissions?: string[] }, - ): Promise<{ id: string; serviceId: string; secret: string }> { - throw new Error('Service credentials are unavailable in the memory auth provider.'); - } - - async rotateServiceToken(_serviceId: string): Promise<{ id: string; serviceId: string; secret: string }> { - throw new Error('Service credentials are unavailable in the memory auth provider.'); - } - - createTrustedUserAssertion(claims: TrustedUserAssertionClaims) { - return Buffer.from(JSON.stringify(claims)).toString('base64url'); - } - - verifyTrustedUserAssertion(assertion: string) { - try { - return JSON.parse(Buffer.from(assertion, 'base64url').toString('utf8')) as TrustedUserAssertionClaims; - } catch { - return null; - } - } - - async exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims) { - const principal: ApiPrincipal = { - id: claims.userId, - displayName: claims.userId, - scopes: ['auth:me'], - roles: ['member'], - permissions: ['auth:read:self'], - metadata: { - sessionId: claims.sessionId, - identityId: claims.identityId, - teamId: claims.teamId ?? null, - projectId: claims.projectId ?? null, - membershipId: claims.membershipId ?? null, - teamRoles: claims.teamRoles ?? [], - teamCapabilities: claims.teamCapabilities ?? [], - }, - }; - const expiresAt = nowSeconds() + this.config.accessTokenTtlSeconds; - return { - ok: true as const, - accessToken: createAccessToken({ - sub: principal.id, - displayName: principal.displayName, - scopes: principal.scopes, - roles: principal.roles, - permissions: principal.permissions, - metadata: principal.metadata, - iat: nowSeconds(), - exp: expiresAt, - iss: this.config.issuer, - jti: randomUUID(), - tokenType: 'access', - }, this.config.authSecret), - tokenType: 'Bearer' as const, - expiresAt: formatExpiry(expiresAt), - expiresInSeconds: this.config.accessTokenTtlSeconds, - principal, - }; - } -} diff --git a/src/api/auth/rbac.ts b/src/api/auth/rbac.ts deleted file mode 100644 index 6c000305..00000000 --- a/src/api/auth/rbac.ts +++ /dev/null @@ -1,180 +0,0 @@ -export const CONTENT_RESOURCES = [ - 'pages', - 'notes', - 'questions', - 'objectives', - 'proposals', - 'decisions', - 'people', - 'agents', - 'books', - 'templates', - 'workdays', -] as const; - -export const PLATFORM_RESOURCES = [ - 'users', - 'roles', - 'api_tokens', - 'services', - 'jobs', - 'audit', - 'auth', - 'sdk', - 'agent', - 'operations', -] as const; - -export const ALL_PERMISSION_RESOURCES = [...CONTENT_RESOURCES, ...PLATFORM_RESOURCES] as const; - -export type PermissionResource = (typeof ALL_PERMISSION_RESOURCES)[number]; -export type PermissionAction = 'read' | 'create' | 'update' | 'delete' | 'manage' | 'execute' | 'impersonate'; -export type PermissionScope = 'self' | 'global'; - -export interface PermissionDefinition { - key: string; - resource: PermissionResource | '*'; - action: PermissionAction | '*'; - scope: PermissionScope | '*'; - description: string; -} - -export interface RoleDefinition { - key: string; - description: string; - permissions: string[]; -} - -export function permissionKey(resource: PermissionDefinition['resource'], action: PermissionDefinition['action'], scope: PermissionDefinition['scope']) { - return `${resource}:${action}:${scope}`; -} - -function contentPermission(resource: PermissionResource, action: PermissionAction, scope: PermissionScope, description: string): PermissionDefinition { - return { - key: permissionKey(resource, action, scope), - resource, - action, - scope, - description, - }; -} - -const permissionDefinitions: PermissionDefinition[] = [ - { - key: permissionKey('*', '*', '*'), - resource: '*', - action: '*', - scope: '*', - description: 'Full platform access.', - }, - contentPermission('auth', 'read', 'self', 'Read the authenticated principal.'), - contentPermission('api_tokens', 'read', 'self', 'List personal API tokens.'), - contentPermission('api_tokens', 'create', 'self', 'Create personal API tokens.'), - contentPermission('api_tokens', 'delete', 'self', 'Revoke personal API tokens.'), - contentPermission('services', 'impersonate', 'global', 'Allow trusted web and service impersonation flows.'), - contentPermission('services', 'manage', 'global', 'Manage service credentials and internal service auth.'), - contentPermission('users', 'read', 'global', 'Read user records.'), - contentPermission('users', 'manage', 'global', 'Manage user records.'), - contentPermission('roles', 'manage', 'global', 'Manage role assignments.'), - contentPermission('audit', 'read', 'global', 'Read audit events.'), - contentPermission('jobs', 'manage', 'global', 'Manage internal job and worker control surfaces.'), - contentPermission('sdk', 'execute', 'global', 'Execute SDK routes.'), - contentPermission('agent', 'execute', 'global', 'Execute agent routes.'), - contentPermission('operations', 'execute', 'global', 'Execute workflow operation routes.'), -]; - -for (const resource of CONTENT_RESOURCES) { - permissionDefinitions.push( - contentPermission(resource, 'read', 'global', `Read ${resource}.`), - contentPermission(resource, 'create', 'global', `Create ${resource}.`), - contentPermission(resource, 'update', 'global', `Update ${resource}.`), - contentPermission(resource, 'delete', 'global', `Delete ${resource}.`), - ); -} - -export const DEFAULT_PERMISSIONS = permissionDefinitions; - -export const DEFAULT_ROLES: RoleDefinition[] = [ - { - key: 'platform_admin', - description: 'Full platform administration.', - permissions: [permissionKey('*', '*', '*')], - }, - { - key: 'market_admin', - description: 'Manage market content and core operational surfaces.', - permissions: [ - permissionKey('auth', 'read', 'self'), - permissionKey('api_tokens', 'read', 'self'), - permissionKey('api_tokens', 'create', 'self'), - permissionKey('api_tokens', 'delete', 'self'), - permissionKey('sdk', 'execute', 'global'), - permissionKey('agent', 'execute', 'global'), - permissionKey('operations', 'execute', 'global'), - permissionKey('users', 'read', 'global'), - permissionKey('audit', 'read', 'global'), - permissionKey('jobs', 'manage', 'global'), - ], - }, - { - key: 'content_admin', - description: 'Manage all content resources.', - permissions: [ - permissionKey('auth', 'read', 'self'), - permissionKey('api_tokens', 'read', 'self'), - permissionKey('api_tokens', 'create', 'self'), - permissionKey('api_tokens', 'delete', 'self'), - ], - }, - { - key: 'content_editor', - description: 'Edit marketplace content.', - permissions: [ - permissionKey('auth', 'read', 'self'), - permissionKey('api_tokens', 'read', 'self'), - permissionKey('api_tokens', 'create', 'self'), - permissionKey('api_tokens', 'delete', 'self'), - ], - }, - { - key: 'member', - description: 'Authenticated member with personal API tokens.', - permissions: [ - permissionKey('auth', 'read', 'self'), - permissionKey('api_tokens', 'read', 'self'), - permissionKey('api_tokens', 'create', 'self'), - permissionKey('api_tokens', 'delete', 'self'), - ], - }, - { - key: 'viewer', - description: 'Read-only marketplace viewer.', - permissions: [permissionKey('auth', 'read', 'self')], - }, -]; - -for (const role of DEFAULT_ROLES) { - if (role.key === 'content_admin') { - for (const resource of CONTENT_RESOURCES) { - role.permissions.push( - permissionKey(resource, 'read', 'global'), - permissionKey(resource, 'create', 'global'), - permissionKey(resource, 'update', 'global'), - permissionKey(resource, 'delete', 'global'), - ); - } - } - if (role.key === 'content_editor') { - for (const resource of CONTENT_RESOURCES) { - role.permissions.push( - permissionKey(resource, 'read', 'global'), - permissionKey(resource, 'create', 'global'), - permissionKey(resource, 'update', 'global'), - ); - } - } -} - -export function permissionGranted(granted: string[], required: string) { - return granted.includes(permissionKey('*', '*', '*')) || granted.includes(required); -} diff --git a/src/api/auth/tokens.ts b/src/api/auth/tokens.ts deleted file mode 100644 index 01d70e4a..00000000 --- a/src/api/auth/tokens.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { createHmac, randomBytes } from 'node:crypto'; -import type { ApiPrincipal } from '../types.ts'; - -export interface AccessTokenPayload { - sub: string; - displayName?: string; - scopes: string[]; - roles: string[]; - permissions: string[]; - iat: number; - exp: number; - iss: string; - jti: string; - tokenType: 'access' | 'service'; - metadata?: Record; -} - -function encodeBase64Url(value: string | Buffer) { - return Buffer.from(value).toString('base64url'); -} - -function decodeBase64Url(value: string) { - return Buffer.from(value, 'base64url').toString('utf8'); -} - -function sign(input: string, secret: string) { - return createHmac('sha256', secret).update(input).digest('base64url'); -} - -export function nextOpaqueToken(prefix: string) { - return `${prefix}_${randomBytes(24).toString('base64url')}`; -} - -export function createAccessToken(payload: AccessTokenPayload, secret: string) { - const encodedPayload = encodeBase64Url(JSON.stringify(payload)); - const encodedSignature = sign(encodedPayload, secret); - return `${encodedPayload}.${encodedSignature}`; -} - -export function verifyAccessToken(token: string, secret: string): AccessTokenPayload | null { - const [encodedPayload, encodedSignature] = token.split('.'); - if (!encodedPayload || !encodedSignature) { - return null; - } - - const expected = sign(encodedPayload, secret); - if (expected !== encodedSignature) { - return null; - } - - try { - const payload = JSON.parse(decodeBase64Url(encodedPayload)) as AccessTokenPayload; - if (!payload.sub || !Array.isArray(payload.scopes) || !payload.exp) { - return null; - } - if (payload.exp <= Math.floor(Date.now() / 1000)) { - return null; - } - return payload; - } catch { - return null; - } -} - -export function principalFromAccessTokenPayload(payload: AccessTokenPayload): ApiPrincipal { - return { - id: payload.sub, - displayName: payload.displayName, - scopes: [...payload.scopes], - roles: [...payload.roles], - permissions: [...payload.permissions], - metadata: payload.metadata, - }; -} diff --git a/src/api/capacity/providers/providers.ts b/src/api/capacity/providers/providers.ts deleted file mode 100644 index 1b5da3f1..00000000 --- a/src/api/capacity/providers/providers.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { MemoryDeviceCodeAuthProvider } from '../../auth/memory-provider.ts'; -import { D1AuthProvider } from '../../auth/d1-provider.ts'; -import { resolveApiD1Database } from '../../auth/d1-database.ts'; -import type { - ApiAuthProvider, - ApiConfig, - ApiProviderFactory, - ApiRuntimeProviders, - ResolvedApiRuntimeProviders, -} from '../../types.ts'; -import { BUILT_IN_AGENT_EXECUTION_PROVIDER_IDS } from '@treeseed/sdk/types/agents'; - -function addProviders(target: Map, incoming: Record | undefined, label: string) { - for (const [id, value] of Object.entries(incoming ?? {})) { - if (target.has(id)) { - throw new Error(`Treeseed API runtime found duplicate ${label} provider "${id}".`); - } - target.set(id, value); - } -} - -function resolveSelectedProvider(registry: Map, selectedId: string, label: string) { - const selected = registry.get(selectedId); - if (!selected) { - throw new Error(`Treeseed API runtime could not resolve ${label} provider "${selectedId}".`); - } - return selected; -} - -export function resolveApiRuntimeProviders(config: ApiConfig, overrides: ApiRuntimeProviders = {}): ResolvedApiRuntimeProviders { - const authRegistry = new Map>(); - const agentExecution = new Map(); - const agentQueue = new Map(); - const agentNotification = new Map(); - const agentRepository = new Map(); - const agentVerification = new Map(); - - addProviders(authRegistry, { - memory: ({ config: runtimeConfig }) => new MemoryDeviceCodeAuthProvider({ - ...runtimeConfig, - baseUrl: runtimeConfig.authApprovalBaseUrl ?? runtimeConfig.baseUrl, - }), - d1: ({ config: runtimeConfig }) => new D1AuthProvider({ - ...runtimeConfig, - baseUrl: runtimeConfig.authApprovalBaseUrl ?? runtimeConfig.baseUrl, - }, { db: resolveApiD1Database(runtimeConfig) }), - }, 'auth'); - addProviders(authRegistry, overrides.auth, 'auth'); - - addProviders(agentExecution, Object.fromEntries(BUILT_IN_AGENT_EXECUTION_PROVIDER_IDS.map((id) => [id, { id }])), 'agent execution'); - addProviders(agentQueue, { memory: { id: 'memory' } }, 'agent queue'); - addProviders(agentNotification, { sdk_message: { id: 'sdk_message' } }, 'agent notification'); - addProviders(agentRepository, { git: { id: 'git' } }, 'agent repository'); - addProviders(agentVerification, { local: { id: 'local' } }, 'agent verification'); - - addProviders(agentExecution, overrides.agentExecution, 'agent execution'); - addProviders(agentQueue, overrides.agentQueue, 'agent queue'); - addProviders(agentNotification, overrides.agentNotification, 'agent notification'); - addProviders(agentRepository, overrides.agentRepository, 'agent repository'); - addProviders(agentVerification, overrides.agentVerification, 'agent verification'); - - const authFactory = resolveSelectedProvider(authRegistry, config.providers.auth, 'auth'); - resolveSelectedProvider(agentExecution, config.providers.agents.execution, 'agent execution'); - resolveSelectedProvider(agentQueue, config.providers.agents.queue, 'agent queue'); - resolveSelectedProvider(agentNotification, config.providers.agents.notification, 'agent notification'); - resolveSelectedProvider(agentRepository, config.providers.agents.repository, 'agent repository'); - resolveSelectedProvider(agentVerification, config.providers.agents.verification, 'agent verification'); - - return { - auth: authFactory({ config }), - registries: { - auth: authRegistry, - agentExecution, - agentQueue, - agentNotification, - agentRepository, - agentVerification, - }, - selections: config.providers, - }; -} diff --git a/src/api/configuration/config.ts b/src/api/configuration/config.ts deleted file mode 100644 index e35bcb01..00000000 --- a/src/api/configuration/config.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { existsSync } from 'node:fs'; -import { resolve } from 'node:path'; -import type { ApiConfig } from '../types.ts'; - -const LOCAL_DEV_AUTH_TTL_SECONDS = 365 * 24 * 60 * 60; -const DEFAULT_AUTH_TTL_SECONDS = 15 * 60; -const DEFAULT_REFRESH_TTL_SECONDS = 7 * 24 * 60 * 60; - -function parseInteger(value: string | undefined, fallback: number) { - if (!value) return fallback; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : fallback; -} - -function resolveLocalWranglerConfigPath(repoRoot: string, env: NodeJS.ProcessEnv) { - const explicit = env.TREESEED_API_D1_WRANGLER_CONFIG?.trim() || env.TREESEED_LOCAL_WRANGLER_CONFIG?.trim(); - if (explicit) return resolve(repoRoot, explicit); - const generated = resolve(repoRoot, '.treeseed', 'generated', 'environments', 'local', 'wrangler.toml'); - return existsSync(generated) ? generated : undefined; -} - -function normalizeUrl(value: string) { - return value.endsWith('/') ? value.slice(0, -1) : value; -} - -function isLoopbackUrl(value: string) { - try { - const url = new URL(value); - return url.hostname === '127.0.0.1' || url.hostname === 'localhost'; - } catch { - return false; - } -} - -function parseCsv(value: string | undefined) { - return (value ?? '') - .split(',') - .map((entry) => entry.trim().toLowerCase()) - .filter(Boolean); -} - -function firstEnvValue(env: NodeJS.ProcessEnv, ...keys: string[]) { - for (const key of keys) { - const value = env[key]?.trim(); - if (value) return value; - } - return undefined; -} - -function resolveBaseUrl(env: NodeJS.ProcessEnv, host: string, port: number) { - if (env.TREESEED_API_BASE_URL?.trim()) { - return normalizeUrl(env.TREESEED_API_BASE_URL.trim()); - } - - if (env.RAILWAY_PUBLIC_DOMAIN?.trim()) { - return normalizeUrl(`https://${env.RAILWAY_PUBLIC_DOMAIN.trim()}`); - } - - return normalizeUrl(`http://${host}:${port}`); -} - -function resolveAuthApprovalBaseUrl(env: NodeJS.ProcessEnv, baseUrl: string) { - const explicit = env.TREESEED_API_AUTH_APPROVAL_BASE_URL?.trim() - || env.TREESEED_SITE_URL?.trim() - || env.TREESEED_BETTER_AUTH_URL?.trim(); - const explicitIsLoopback = explicit ? isLoopbackUrl(explicit) : false; - try { - const url = new URL(baseUrl); - const isLocalApi = url.hostname === '127.0.0.1' || url.hostname === 'localhost'; - if (!isLocalApi && explicit && explicitIsLoopback) { - throw new Error(`Refusing loopback device approval URL "${explicit}" for remote API "${baseUrl}".`); - } - if (explicit) { - return normalizeUrl(explicit); - } - if (isLocalApi && url.port === '3000') { - url.port = '4321'; - return normalizeUrl(url.toString()); - } - } catch (error) { - if (error instanceof Error && error.message.includes('Refusing loopback device approval URL')) { - throw error; - } - if (explicit && /^https?:\/\//u.test(explicit)) { - throw new Error(`Invalid device approval URL configuration for API "${baseUrl}".`); - } - } - return baseUrl; -} - -export function resolveApiConfig(env: NodeJS.ProcessEnv = process.env): ApiConfig { - const host = env.HOST?.trim() || '0.0.0.0'; - const port = parseInteger(env.PORT, 3000); - const baseUrl = resolveBaseUrl(env, host === '0.0.0.0' ? '127.0.0.1' : host, port); - const issuer = normalizeUrl(env.TREESEED_API_ISSUER?.trim() || baseUrl); - const repoRoot = resolve(env.TREESEED_API_REPO_ROOT?.trim() || process.cwd()); - const localDevAuth = env.LOCAL_DEV_MODE === 'cloudflare' || isLoopbackUrl(baseUrl); - const defaultAccessTokenTtl = localDevAuth ? LOCAL_DEV_AUTH_TTL_SECONDS : DEFAULT_AUTH_TTL_SECONDS; - const defaultRefreshTokenTtl = localDevAuth ? LOCAL_DEV_AUTH_TTL_SECONDS : DEFAULT_REFRESH_TTL_SECONDS; - - return { - name: env.TREESEED_API_NAME?.trim() || '@treeseed/agent/api', - host, - port, - baseUrl, - authApprovalBaseUrl: resolveAuthApprovalBaseUrl(env, baseUrl), - issuer, - repoRoot, - projectId: env.TREESEED_PROJECT_ID?.trim() || 'treeseed-project', - authSecret: env.TREESEED_API_AUTH_SECRET?.trim() || 'treeseed-api-dev-secret', - projectApiKey: env.TREESEED_API_PROJECT_KEY?.trim() || undefined, - projectApiLabel: env.TREESEED_API_PROJECT_LABEL?.trim() || 'Project API Key', - projectApiPermissions: parseCsv(env.TREESEED_API_PROJECT_KEY_PERMISSIONS) - .length > 0 - ? parseCsv(env.TREESEED_API_PROJECT_KEY_PERMISSIONS) - : ['sdk:execute:global', 'agent:execute:global', 'operations:execute:global'], - cloudflareAccountId: env.TREESEED_CLOUDFLARE_ACCOUNT_ID?.trim() || undefined, - cloudflareApiToken: env.TREESEED_CLOUDFLARE_API_TOKEN?.trim() || undefined, - d1DatabaseId: env.TREESEED_API_D1_DATABASE_ID?.trim() || undefined, - d1DatabaseName: env.TREESEED_API_D1_DATABASE_NAME?.trim() || env.SITE_DATA_DB?.trim() || undefined, - d1LocalPersistTo: env.TREESEED_API_D1_LOCAL_PERSIST_TO?.trim() || resolve(repoRoot, '.wrangler/state/v3/d1'), - d1WranglerConfigPath: resolveLocalWranglerConfigPath(repoRoot, env), - webServiceId: firstEnvValue(env, 'TREESEED_API_WEB_SERVICE_ID', 'TREESEED_WEB_SERVICE_ID') || 'web', - webServiceSecret: firstEnvValue(env, 'TREESEED_API_WEB_SERVICE_SECRET', 'TREESEED_WEB_SERVICE_SECRET') || 'treeseed-web-service-dev-secret', - webAssertionSecret: firstEnvValue(env, 'TREESEED_API_WEB_ASSERTION_SECRET', 'TREESEED_WEB_ASSERTION_SECRET', 'TREESEED_API_AUTH_SECRET') || 'treeseed-web-assertion-dev-secret', - webExchangeTtlSeconds: parseInteger(env.TREESEED_API_WEB_EXCHANGE_TTL, 300), - bootstrapAdminAllowlist: parseCsv(env.TREESEED_API_BOOTSTRAP_ADMIN_ALLOWLIST), - accessTokenTtlSeconds: parseInteger(env.TREESEED_API_ACCESS_TOKEN_TTL, defaultAccessTokenTtl), - refreshTokenTtlSeconds: parseInteger(env.TREESEED_API_REFRESH_TOKEN_TTL, defaultRefreshTokenTtl), - deviceCodeTtlSeconds: parseInteger(env.TREESEED_API_DEVICE_CODE_TTL, 10 * 60), - deviceCodePollIntervalSeconds: parseInteger(env.TREESEED_API_DEVICE_CODE_POLL_INTERVAL, 5), - templateCatalogPath: env.TREESEED_API_TEMPLATE_CATALOG_PATH?.trim() || undefined, - providers: { - auth: env.TREESEED_API_PROVIDER_AUTH?.trim() || 'd1', - agents: { - execution: env.TREESEED_API_PROVIDER_AGENT_EXECUTION?.trim() || 'codex', - queue: env.TREESEED_API_PROVIDER_AGENT_QUEUE?.trim() || 'memory', - notification: env.TREESEED_API_PROVIDER_AGENT_NOTIFICATION?.trim() || 'sdk_message', - repository: env.TREESEED_API_PROVIDER_AGENT_REPOSITORY?.trim() || 'git', - verification: env.TREESEED_API_PROVIDER_AGENT_VERIFICATION?.trim() || 'local', - }, - }, - }; -} diff --git a/src/api/hosting/railway.ts b/src/api/hosting/railway.ts deleted file mode 100644 index b7b25805..00000000 --- a/src/api/hosting/railway.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { createServer, type Server } from 'node:http'; -import { Readable } from 'node:stream'; -import type { AddressInfo } from 'node:net'; -import type { Hono } from 'hono'; -import type { ApiServerOptions } from '@treeseed/sdk/api'; -import type { ApiConfig } from '../types.ts'; -import { createApiApp } from '../support/app.ts'; -import { resolveApiConfig } from '../configuration/config.ts'; - -function hasRequestBody(method: string | undefined) { - return method !== 'GET' && method !== 'HEAD'; -} - -async function honoNodeHandler(app: Hono, request: Parameters[1], response: Parameters[2]) { - const req = request as any; - const res = response as any; - const origin = req.headers.host ? `http://${req.headers.host}` : 'http://127.0.0.1'; - const url = new URL(req.url ?? '/', origin); - const webRequest = new Request(url, { - method: req.method, - headers: req.headers as HeadersInit, - body: hasRequestBody(req.method) ? req : undefined, - duplex: 'half', - } as RequestInit & { duplex: 'half' }); - - const webResponse = await app.fetch(webRequest); - res.statusCode = webResponse.status; - webResponse.headers.forEach((value: string, key: string) => { - res.setHeader(key, value); - }); - - if (!webResponse.body) { - res.end(); - return; - } - - Readable.fromWeb(webResponse.body as never).pipe(res); -} - -export interface NodeServer { - app: Hono; - config: ApiConfig; - server: Server; - url: string; - close(): Promise; -} - -export async function createNodeServer(options: ApiServerOptions = {}): Promise { - const config = { - ...resolveApiConfig(), - ...(options.config ?? {}), - providers: { - ...resolveApiConfig().providers, - ...(options.config?.providers ?? {}), - agents: { - ...resolveApiConfig().providers.agents, - ...(options.config?.providers?.agents ?? {}), - }, - }, - }; - const app = createApiApp({ - ...options, - config, - }); - const server = createServer((req, res) => { - void honoNodeHandler(app, req as never, res as never); - }); - - await new Promise((resolvePromise) => { - server.listen(config.port, config.host, () => resolvePromise()); - }); - - const address = server.address() as AddressInfo | null; - const resolvedUrl = address - ? `${config.baseUrl.startsWith('http') ? config.baseUrl : `http://${address.address}:${address.port}`}` - : config.baseUrl; - - return { - app, - config, - server, - url: resolvedUrl, - async close() { - await new Promise((resolvePromise, rejectPromise) => { - server.close((error) => (error ? rejectPromise(error) : resolvePromise())); - }); - }, - }; -} - -export const createRailwayApiServer = createNodeServer; diff --git a/src/api/index.ts b/src/api/index.ts deleted file mode 100644 index 33ff8e4f..00000000 --- a/src/api/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { createApiApp, createApiRouter } from './support/app.ts'; -export { resolveApiConfig } from './configuration/config.ts'; -export { createRailwayApiServer, createNodeServer } from './hosting/railway.ts'; -export { resolveApiRuntimeProviders } from './capacity/providers/providers.ts'; -export { resolveApiD1Database } from './auth/d1-database.ts'; -export { loadTemplateCatalog } from './support/templates.ts'; -export { MemoryDeviceCodeAuthProvider } from './auth/memory-provider.ts'; -export { D1AuthProvider } from './auth/d1-provider.ts'; -export type * from './types.ts'; diff --git a/src/api/operations/operations-routes.ts b/src/api/operations/operations-routes.ts deleted file mode 100644 index bc1d83ec..00000000 --- a/src/api/operations/operations-routes.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { Hono } from 'hono'; -import type { AgentSdk } from '@treeseed/sdk'; -import { findDispatchCapability, findOperation } from '@treeseed/sdk'; -import { executeHttpWorkflowOperation, isHttpWorkflowOperationAllowed } from './operations.ts'; -import { jsonError, requireScope } from '../support/http.ts'; -import type { ApiConfig, AppVariables } from '../types.ts'; - -export function registerOperationRoutes( - app: Hono, - options: { - config: ApiConfig; - scope: string; - prefix?: string; - sdk?: AgentSdk; - executeOperation?: typeof executeHttpWorkflowOperation; - }, -) { - const executeOperation = options.executeOperation ?? executeHttpWorkflowOperation; - const prefix = options.prefix ?? ''; - - function withPrefix(path: string) { - if (!prefix) return path; - return `${prefix}${path}`.replace(/\/{2,}/g, '/'); - } - - app.post(withPrefix('/operations/:operation'), async (c) => { - const unauthorized = requireScope(c, options.scope); - if (unauthorized) return unauthorized; - - const requestedOperation = c.req.param('operation'); - const resolvedOperation = findOperation(requestedOperation); - if (!resolvedOperation) { - return jsonError(c, 400, `Unknown Treeseed operation "${requestedOperation}".`, { - operation: requestedOperation, - }); - } - if (!isHttpWorkflowOperationAllowed(resolvedOperation.name)) { - return jsonError(c, 400, `Workflow operation "${resolvedOperation.name}" is not supported over HTTP.`, { - operation: resolvedOperation.name, - }); - } - - const body = await c.req.json().catch(() => ({})); - try { - const capability = findDispatchCapability('workflow', resolvedOperation.name); - if (capability?.executionClass === 'remote_job' && options.sdk) { - return jsonError(c, 410, `Remote workflow "${resolvedOperation.name}" used the legacy task queue and is disabled. Use provider assignments for remote execution.`, { - operation: resolvedOperation.name, - }); - } - const result = await executeOperation(resolvedOperation.name, body); - return c.json(result, { status: result.ok ? 200 : 400 }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - const status = /Unknown Treeseed operation|not supported over HTTP|confirmation required/i.test(message) ? 400 : 500; - return jsonError(c, status, message, { operation: resolvedOperation.name }); - } - }); -} diff --git a/src/api/operations/operations.ts b/src/api/operations/operations.ts deleted file mode 100644 index a0f6822c..00000000 --- a/src/api/operations/operations.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { OperationsSdk } from '@treeseed/sdk'; -import type { ApiWorkflowOperationResponse, WorkflowHttpOperationRequest } from '../types.ts'; - -const HTTP_BLOCKED_WORKFLOW_OPERATIONS = new Set(['dev', 'dev:watch']); - -export function isHttpWorkflowOperationAllowed(operation: string) { - return !HTTP_BLOCKED_WORKFLOW_OPERATIONS.has(operation); -} - -export async function executeHttpWorkflowOperation( - operation: string, - request: WorkflowHttpOperationRequest, -): Promise { - if (!isHttpWorkflowOperationAllowed(operation)) { - throw new Error(`Workflow operation "${operation}" is not supported over HTTP.`); - } - - const operations = new OperationsSdk(); - return operations.execute({ - operationName: operation, - input: (request.input ?? {}) as Record, - }, { - cwd: request.cwd ?? process.cwd(), - env: { - ...process.env, - ...(request.env ?? {}), - }, - transport: 'api', - }); -} diff --git a/src/api/projects/projects-core/project-route-helpers.ts b/src/api/projects/projects-core/project-route-helpers.ts deleted file mode 100644 index 1016c75a..00000000 --- a/src/api/projects/projects-core/project-route-helpers.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { AgentMessageKind } from '@treeseed/sdk'; - -export function withPrefix(prefix: string, path: string) { - if (!prefix) return path; - return `${prefix}${path}`.replace(/\/{2,}/g, '/'); -} - -export function slugify(value: string) { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .slice(0, 64) || 'change'; -} - -export function nowIso() { - return new Date().toISOString(); -} - -export function asRecords(value: unknown) { - return Array.isArray(value) ? value as Record[] : []; -} - -export function readString(record: Record, ...keys: string[]) { - for (const key of keys) { - const value = record[key]; - if (typeof value === 'string' && value.trim()) return value.trim(); - } - return ''; -} - -export function readOptionalString(record: Record, ...keys: string[]) { - const value = readString(record, ...keys); - return value || null; -} - -export function routeParam(c: { req: { param: (name: string) => string | undefined } }, name: string) { - const value = c.req.param(name); - if (!value) { - throw new Error(`Missing route parameter "${name}".`); - } - return value; -} - -export function inferMessageKind(type: string, status: string): AgentMessageKind { - if (status === 'failed' || type.includes('failed')) return 'warning'; - if (type.includes('waiting') || type.includes('review') || type.includes('release')) return 'action_requested'; - if (type.includes('release_completed') || type.includes('task_verified')) return 'release_readiness'; - return 'informational'; -} - diff --git a/src/api/projects/projects-core/project-routes.ts b/src/api/projects/projects-core/project-routes.ts deleted file mode 100644 index 21a6b99c..00000000 --- a/src/api/projects/projects-core/project-routes.ts +++ /dev/null @@ -1,370 +0,0 @@ -import type { Hono } from 'hono'; -import type { AgentSdk } from '@treeseed/sdk'; -import { buildTemplateMarketPackage, WorkflowSdk } from '@treeseed/sdk'; -import type { WorkstreamSummary } from '@treeseed/sdk'; -import { checkCodexProviderReadiness } from '../../../agents/adapters/codex/codex-readiness.ts'; -import { requireTeamCapability } from '../../support/capabilities.ts'; -import { jsonError } from '../../support/http.ts'; -import { nowIso, readOptionalString, routeParam, slugify, withPrefix } from './project-route-helpers.ts'; -import { summarizeAgents, summarizeDirect, summarizeProject } from './project-summary.ts'; -import type { ApiConfig } from '../../types.ts'; - -export function registerProjectRoutes( - app: Hono, - options: { - config: ApiConfig; - sharedSdk: AgentSdk; - prefix?: string; - }, -) { - const prefix = options.prefix ?? ''; - const workflow = new WorkflowSdk({ - cwd: options.config.repoRoot, - env: process.env, - transport: 'api', - }); - - app.get(withPrefix(prefix, '/v1/project/summary'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - return c.json({ - ok: true, - payload: await summarizeProject(options.sharedSdk, options.config, principal), - }); - }); - - app.get(withPrefix(prefix, '/v1/direct/summary'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - return c.json({ ok: true, payload: await summarizeDirect(options.sharedSdk, options.config.projectId) }); - }); - - app.get(withPrefix(prefix, '/v1/workstreams'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - const items = await options.sharedSdk.listWorkstreams(options.config.projectId); - return c.json({ - ok: true, - payload: { - projectId: options.config.projectId, - items: items.payload, - columns: ['Drafting', 'Active locally', 'Verifying', 'Saved remotely', 'In staging', 'Archived'], - }, - }); - }); - - app.post(withPrefix(prefix, '/v1/workstreams'), async (c) => { - const unauthorized = requireTeamCapability(c, 'manage_workstreams'); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Untitled change'; - const branchName = typeof body.branchName === 'string' && body.branchName.trim() ? body.branchName.trim() : `task/${slugify(title)}`; - await workflow.switchTask({ - branchName, - createIfMissing: true, - preview: body.preview === true, - }); - const workstream = await options.sharedSdk.upsertWorkstream({ - projectId: options.config.projectId, - title, - summary: typeof body.summary === 'string' ? body.summary : null, - state: 'active_local', - branchName, - branchRef: `refs/heads/${branchName}`, - owner: typeof body.owner === 'string' ? body.owner : c.get('principal')?.displayName ?? null, - linkedItems: Array.isArray(body.linkedItems) ? body.linkedItems as WorkstreamSummary['linkedItems'] : [], - metadata: typeof body.metadata === 'object' && body.metadata ? body.metadata as Record : {}, - }); - if (workstream.payload) { - await options.sharedSdk.appendWorkstreamEvent({ - projectId: options.config.projectId, - workstreamId: workstream.payload.id, - kind: 'created', - summary: 'Workstream created and branch activated.', - data: { branchName }, - }); - } - return c.json({ ok: true, payload: workstream.payload }, { status: 201 }); - }); - - app.get(withPrefix(prefix, '/v1/workstreams/:id'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - const workstreamId = routeParam(c, 'id'); - const detail = await options.sharedSdk.getWorkstream(workstreamId); - if (!detail.payload) return jsonError(c, 404, `Unknown workstream "${workstreamId}".`); - return c.json({ ok: true, payload: detail.payload }); - }); - - app.post(withPrefix(prefix, '/v1/workstreams/:id/save'), async (c) => { - const unauthorized = requireTeamCapability(c, 'manage_workstreams'); - if (unauthorized) return unauthorized; - const workstreamId = routeParam(c, 'id'); - const existing = await options.sharedSdk.getWorkstream(workstreamId); - if (!existing.payload) return jsonError(c, 404, `Unknown workstream "${workstreamId}".`); - const body = await c.req.json().catch(() => ({})) as Record; - const result = await workflow.save({ - message: typeof body.message === 'string' && body.message.trim() ? body.message.trim() : `Save ${existing.payload.title}`, - verify: body.verify !== false, - refreshPreview: body.refreshPreview === true, - }); - const updated = await options.sharedSdk.upsertWorkstream({ - ...existing.payload, - state: body.verify === false ? 'saved_remote' : 'verifying', - lastSaveAt: nowIso(), - verificationStatus: result.ok ? 'completed' : 'failed', - verificationSummary: result.summary ?? null, - }); - await options.sharedSdk.appendWorkstreamEvent({ - projectId: options.config.projectId, - workstreamId: existing.payload.id, - kind: 'saved', - summary: result.summary ?? 'Workstream saved.', - data: { workflow: result.payload ?? {} }, - }); - return c.json({ ok: true, payload: updated.payload }); - }); - - app.post(withPrefix(prefix, '/v1/workstreams/:id/stage'), async (c) => { - const unauthorized = requireTeamCapability(c, 'stage_releases'); - if (unauthorized) return unauthorized; - const workstreamId = routeParam(c, 'id'); - const existing = await options.sharedSdk.getWorkstream(workstreamId); - if (!existing.payload) return jsonError(c, 404, `Unknown workstream "${workstreamId}".`); - const body = await c.req.json().catch(() => ({})) as Record; - const result = await workflow.stage({ - message: typeof body.message === 'string' && body.message.trim() ? body.message.trim() : `Stage ${existing.payload.title}`, - }); - const updated = await options.sharedSdk.upsertWorkstream({ - ...existing.payload, - state: 'in_staging', - lastStageAt: nowIso(), - verificationStatus: result.ok ? 'completed' : existing.payload.verificationStatus, - verificationSummary: result.summary ?? existing.payload.verificationSummary, - }); - await options.sharedSdk.appendWorkstreamEvent({ - projectId: options.config.projectId, - workstreamId: existing.payload.id, - kind: 'staged', - summary: result.summary ?? 'Workstream moved to staging.', - data: { workflow: result.payload ?? {} }, - }); - return c.json({ ok: true, payload: updated.payload }); - }); - - app.post(withPrefix(prefix, '/v1/workstreams/:id/archive'), async (c) => { - const unauthorized = requireTeamCapability(c, 'manage_workstreams'); - if (unauthorized) return unauthorized; - const workstreamId = routeParam(c, 'id'); - const existing = await options.sharedSdk.getWorkstream(workstreamId); - if (!existing.payload) return jsonError(c, 404, `Unknown workstream "${workstreamId}".`); - const updated = await options.sharedSdk.upsertWorkstream({ - ...existing.payload, - state: 'archived', - archivedAt: nowIso(), - }); - await options.sharedSdk.appendWorkstreamEvent({ - projectId: options.config.projectId, - workstreamId: existing.payload.id, - kind: 'archived', - summary: 'Workstream archived.', - data: {}, - }); - return c.json({ ok: true, payload: updated.payload }); - }); - - app.get(withPrefix(prefix, '/v1/releases'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - const releases = await options.sharedSdk.listReleases(options.config.projectId); - const history = releases.payload; - return c.json({ - ok: true, - payload: { - projectId: options.config.projectId, - history, - currentProd: history.find((entry) => entry.state === 'published') ?? null, - stagingCandidates: history.filter((entry) => entry.state === 'ready_to_publish' || entry.state === 'waiting_on_verification'), - }, - }); - }); - - app.post(withPrefix(prefix, '/v1/releases'), async (c) => { - const unauthorized = requireTeamCapability(c, 'stage_releases'); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const workstreams = await options.sharedSdk.listWorkstreams(options.config.projectId); - const selectedIds = Array.isArray(body.workstreamIds) - ? body.workstreamIds.map(String) - : workstreams.payload.filter((entry) => entry.state === 'in_staging').map((entry) => entry.id); - const release = await options.sharedSdk.upsertRelease({ - projectId: options.config.projectId, - version: typeof body.version === 'string' && body.version.trim() ? body.version.trim() : `draft-${Date.now()}`, - title: typeof body.title === 'string' ? body.title : null, - state: 'ready_to_publish', - summary: typeof body.summary === 'string' ? body.summary : null, - workstreamIds: selectedIds, - items: selectedIds.map((workstreamId) => ({ - id: `${workstreamId}-item`, - workstreamId, - model: null, - recordId: null, - summary: 'Included workstream', - createdAt: nowIso(), - metadata: {}, - })), - }); - return c.json({ ok: true, payload: release.payload }, { status: 201 }); - }); - - app.get(withPrefix(prefix, '/v1/releases/:id'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - const releaseId = routeParam(c, 'id'); - const release = await options.sharedSdk.getRelease(releaseId); - if (!release.payload) return jsonError(c, 404, `Unknown release "${releaseId}".`); - return c.json({ ok: true, payload: release.payload }); - }); - - app.post(withPrefix(prefix, '/v1/releases/:id/publish'), async (c) => { - const unauthorized = requireTeamCapability(c, 'publish_releases'); - if (unauthorized) return unauthorized; - const releaseId = routeParam(c, 'id'); - const release = await options.sharedSdk.getRelease(releaseId); - if (!release.payload) return jsonError(c, 404, `Unknown release "${releaseId}".`); - const body = await c.req.json().catch(() => ({})) as Record; - const workflowResult = await workflow.release({ - bump: body.bump === 'major' || body.bump === 'minor' ? body.bump : 'patch', - }); - const updated = await options.sharedSdk.upsertRelease({ - ...release.payload, - state: 'published', - publishedAt: nowIso(), - releaseTag: readOptionalString((workflowResult.payload ?? {}) as Record, 'version', 'releaseTag'), - }); - return c.json({ ok: true, payload: updated.payload }); - }); - - app.post(withPrefix(prefix, '/v1/releases/:id/rollback'), async (c) => { - const unauthorized = requireTeamCapability(c, 'publish_releases'); - if (unauthorized) return unauthorized; - const releaseId = routeParam(c, 'id'); - const release = await options.sharedSdk.getRelease(releaseId); - if (!release.payload) return jsonError(c, 404, `Unknown release "${releaseId}".`); - const updated = await options.sharedSdk.upsertRelease({ - ...release.payload, - state: 'rolled_back', - rolledBackAt: nowIso(), - }); - return c.json({ ok: true, payload: updated.payload }); - }); - - app.get(withPrefix(prefix, '/v1/share/status'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - const packages = await options.sharedSdk.listSharePackages(options.config.projectId); - return c.json({ - ok: true, - payload: { - projectId: options.config.projectId, - packages: packages.payload, - listing: null, - canPublish: packages.payload.some((entry) => entry.state === 'ready_to_publish' || entry.state === 'published'), - }, - }); - }); - - for (const [path, kind] of [ - ['/v1/share/export', 'export'], - ['/v1/share/package-template', 'template'], - ] as const) { - app.post(withPrefix(prefix, path), async (c) => { - const unauthorized = requireTeamCapability(c, 'manage_products'); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const exportResult = kind === 'export' - ? await workflow.export({ - directory: typeof body.directory === 'string' ? body.directory : undefined, - }) - : null; - const packageResult = kind === 'template' - ? buildTemplateMarketPackage(options.config.repoRoot, { - id: typeof body.id === 'string' ? body.id : undefined, - title: typeof body.title === 'string' && body.title.trim() ? body.title.trim() : undefined, - summary: typeof body.summary === 'string' ? body.summary : null, - outputRoot: typeof body.outputRoot === 'string' ? body.outputRoot : null, - projectSlug: typeof body.projectSlug === 'string' ? body.projectSlug : options.config.projectId, - market: { - publisherId: typeof c.get('principal')?.metadata?.teamId === 'string' ? c.get('principal').metadata.teamId : null, - publisherName: typeof c.get('principal')?.displayName === 'string' ? c.get('principal').displayName : null, - publishMetadata: { - projectId: options.config.projectId, - kind, - }, - }, - }) - : null; - const item = await options.sharedSdk.upsertSharePackage({ - projectId: options.config.projectId, - kind, - title: typeof body.title === 'string' && body.title.trim() ? body.title.trim() : `${kind}-${Date.now()}`, - summary: kind === 'export' - ? exportResult?.summary ?? null - : packageResult?.manifest.summary ?? null, - state: 'ready_to_publish', - version: kind === 'export' ? null : packageResult?.manifest.version ?? null, - outputPath: kind === 'export' - ? readOptionalString((exportResult?.payload ?? {}) as Record, 'path', 'outputPath', 'directory') - : packageResult?.outputRoot ?? null, - artifactKey: kind === 'export' ? null : packageResult?.payloadRoot ?? null, - manifestKey: kind === 'export' ? null : packageResult?.manifestPath ?? null, - metadata: kind === 'export' - ? (typeof exportResult?.payload === 'object' && exportResult.payload ? exportResult.payload as Record : {}) - : { - manifest: packageResult?.manifest ?? null, - files: packageResult?.files ?? [], - payloadRoot: packageResult?.payloadRoot ?? null, - }, - }); - return c.json({ ok: true, payload: item.payload }); - }); - } - - app.post(withPrefix(prefix, '/v1/share/publish'), async (c) => { - const unauthorized = requireTeamCapability(c, 'publish_market_listings'); - if (unauthorized) return unauthorized; - const body = await c.req.json().catch(() => ({})) as Record; - const sharePackage = typeof body.packageId === 'string' ? await options.sharedSdk.getSharePackage(body.packageId) : null; - if (!sharePackage?.payload) { - return jsonError(c, 404, 'Unknown share package.'); - } - const updated = await options.sharedSdk.upsertSharePackage({ - ...sharePackage.payload, - state: 'published', - }); - return c.json({ ok: true, payload: updated.payload }); - }); - - app.get(withPrefix(prefix, '/v1/agents/status'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - const payload = await summarizeAgents(options.sharedSdk, options.config.projectId); - return c.json({ ok: true, payload: { projectId: options.config.projectId, agents: payload.agents } }); - }); - - app.get(withPrefix(prefix, '/v1/agents/messages'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - const payload = await summarizeAgents(options.sharedSdk, options.config.projectId); - return c.json({ ok: true, payload: payload.messages }); - }); - - app.get(withPrefix(prefix, '/v1/providers/codex/readiness'), async (c) => { - const principal = c.get('principal'); - if (!principal) return jsonError(c, 401, 'Authentication required.'); - return c.json({ - ok: true, - payload: checkCodexProviderReadiness({ env: process.env }), - }); - }); -} diff --git a/src/api/projects/projects-core/project-summary.ts b/src/api/projects/projects-core/project-summary.ts deleted file mode 100644 index bccb5c62..00000000 --- a/src/api/projects/projects-core/project-summary.ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { AgentSdk } from '@treeseed/sdk'; -import type { AgentMessageRecord, AgentStatusRecord, DirectBoardItemSummary, ProjectOverviewSummary, ReleaseDetail, ReleaseSummary, SharePackageStatus, WorkstreamDetail, WorkstreamState, WorkstreamSummary } from '@treeseed/sdk'; -import { normalizeProjectJobStatus } from '@treeseed/sdk'; -import { checkCodexProviderReadiness } from '../../../agents/adapters/codex/codex-readiness.ts'; -import type { ApiConfig } from '../../types.ts'; -import { asRecords, inferMessageKind, nowIso, readOptionalString, readString } from './project-route-helpers.ts'; - -export async function summarizeDirect(sdk: AgentSdk, projectId: string) { - const [objectives, questions, notes, proposals, decisions, workstreams, releases] = await Promise.all([ - sdk.search({ model: 'objective', sort: [{ field: 'updated_at', direction: 'desc' }], limit: 50 }), - sdk.search({ model: 'question', sort: [{ field: 'updated_at', direction: 'desc' }], limit: 50 }), - sdk.search({ model: 'note', sort: [{ field: 'updated_at', direction: 'desc' }], limit: 50 }), - sdk.search({ model: 'proposal', sort: [{ field: 'updated_at', direction: 'desc' }], limit: 50 }), - sdk.search({ model: 'decision', sort: [{ field: 'updated_at', direction: 'desc' }], limit: 50 }), - sdk.listWorkstreams(projectId), - sdk.listReleases(projectId), - ]); - - const workstreamPayload = workstreams.payload ?? []; - const releasePayload = releases.payload ?? []; - const linkIndex = new Map(); - for (const workstream of workstreamPayload) { - for (const ref of workstream.linkedItems) { - const key = `${ref.model}:${ref.id}`; - const current = linkIndex.get(key) ?? { workstreamIds: [], releaseIds: [] }; - current.workstreamIds.push(workstream.id); - linkIndex.set(key, current); - } - } - for (const release of releasePayload) { - for (const workstreamId of release.workstreamIds) { - const workstream = workstreamPayload.find((entry) => entry.id === workstreamId); - for (const ref of workstream?.linkedItems ?? []) { - const key = `${ref.model}:${ref.id}`; - const current = linkIndex.get(key) ?? { workstreamIds: [], releaseIds: [] }; - current.releaseIds.push(release.id); - linkIndex.set(key, current); - } - } - } - - const mapItems = (model: 'objective' | 'question' | 'note' | 'proposal' | 'decision', entries: Record[]) => - entries.slice(0, 15).map((entry): DirectBoardItemSummary => { - const id = readString(entry, 'id', 'slug'); - const links = linkIndex.get(`${model}:${id}`) ?? { workstreamIds: [], releaseIds: [] }; - return { - model, - id, - title: readString(entry, 'title', 'name') || id, - status: readOptionalString(entry, 'status'), - updatedAt: readOptionalString(entry, 'updated_at', 'updatedAt', 'updated'), - linkedWorkstreamIds: [...new Set(links.workstreamIds)], - linkedReleaseIds: [...new Set(links.releaseIds)], - }; - }); - - return { - projectId, - objectiveCount: objectives.payload.length, - questionCount: questions.payload.length, - noteCount: notes.payload.length, - proposalCount: proposals.payload.length, - decisionCount: decisions.payload.length, - savedViews: ['Now', 'Blocked', 'Ready for research', 'Ready for build', 'Release-linked'], - items: [ - ...mapItems('objective', objectives.payload as Record[]), - ...mapItems('question', questions.payload as Record[]), - ...mapItems('note', notes.payload as Record[]), - ...mapItems('proposal', proposals.payload as Record[]), - ...mapItems('decision', decisions.payload as Record[]), - ].slice(0, 15), - }; -} - -export async function summarizeAgents(sdk: AgentSdk, projectId: string) { - const [specs, runs, messages] = await Promise.all([ - sdk.listAgentSpecs({ enabled: true }), - sdk.search({ model: 'agent_run', sort: [{ field: 'startedAt', direction: 'desc' }], limit: 100 }), - sdk.search({ model: 'message', sort: [{ field: 'updated_at', direction: 'desc' }], limit: 100 }), - ]); - const workstreams = await sdk.listWorkstreams(projectId); - const workstreamIds = new Set((workstreams.payload ?? []).map((entry) => entry.id)); - - const agentStatuses: AgentStatusRecord[] = specs.map((spec) => { - const latestRun = (runs.payload as Record[]).find((entry) => readString(entry, 'agentSlug', 'agent_slug') === spec.slug); - const latestMessage = (messages.payload as Record[]).find((entry) => readString(entry, 'type') && readString(entry, 'payloadJson', 'payload_json')); - return { - agentSlug: spec.slug, - handler: spec.handler, - status: readString(latestRun ?? {}, 'status') === 'failed' - ? 'failed' - : readString(latestRun ?? {}, 'status') === 'running' - ? 'active' - : latestRun - ? 'idle' - : 'waiting', - currentTask: readOptionalString(latestRun ?? {}, 'summary'), - workstreamId: readOptionalString(latestRun ?? {}, 'selectedItemKey'), - lastMessage: readOptionalString(latestMessage ?? {}, 'type'), - lastRunAt: readOptionalString(latestRun ?? {}, 'startedAt', 'started_at', 'finishedAt', 'finished_at'), - }; - }); - - const messageRecords: AgentMessageRecord[] = (messages.payload as Record[]).map((entry) => { - const payloadJson = readString(entry, 'payloadJson', 'payload_json'); - let parsed: Record = {}; - try { - parsed = payloadJson ? JSON.parse(payloadJson) : {}; - } catch { - parsed = {}; - } - const workstreamId = typeof parsed.workstreamId === 'string' && workstreamIds.has(parsed.workstreamId) - ? parsed.workstreamId - : null; - return { - id: String(entry.id ?? ''), - agentSlug: readString(parsed, 'agentSlug') || readString(entry, 'relatedId', 'related_id') || 'agent', - kind: inferMessageKind(readString(entry, 'type'), readString(entry, 'status')), - type: readString(entry, 'type'), - status: readString(entry, 'status') || 'pending', - summary: readString(parsed, 'summary', 'message', 'failureSummary', 'blockingReason') || readString(entry, 'type'), - workstreamId, - releaseId: typeof parsed.releaseId === 'string' ? parsed.releaseId : null, - createdAt: readString(entry, 'updated_at', 'updatedAt', 'created_at') || nowIso(), - metadata: parsed, - }; - }); - - return { - projectId, - agents: agentStatuses, - messages: messageRecords, - }; -} - -export async function summarizeProject(sdk: AgentSdk, config: ApiConfig, principal: { metadata?: Record } | null): Promise { - const [direct, workstreams, agents, releases, packages] = await Promise.all([ - summarizeDirect(sdk, config.projectId), - sdk.listWorkstreams(config.projectId), - summarizeAgents(sdk, config.projectId), - sdk.listReleases(config.projectId), - sdk.listSharePackages(config.projectId), - ]); - - const workstreamPayload = workstreams.payload ?? []; - const releasePayload = releases.payload ?? []; - const packagePayload = packages.payload ?? []; - const failedWorkstream = workstreamPayload.find((entry) => entry.verificationStatus === 'failed'); - const releaseReady = releasePayload.find((entry) => entry.state === 'ready_to_publish'); - const publishingDraft = packagePayload.find((entry) => entry.state === 'ready_to_publish' || entry.state === 'published'); - const health = failedWorkstream - ? { state: 'verification_failing', label: 'Verification failing', reason: failedWorkstream.verificationSummary ?? 'A workstream verification failed.' } - : releaseReady - ? { state: 'release_ready', label: 'Release ready', reason: 'A release candidate is ready for approval.' } - : publishingDraft - ? { state: 'sharing_draft', label: 'Sharing draft', reason: 'A share package is ready for publishing.' } - : { state: 'working_normally', label: 'Working normally', reason: 'Project workstreams and agents are operating normally.' }; - - const recentActivity = [ - ...workstreamPayload.map((entry) => ({ - kind: 'workstream', - id: entry.id, - title: entry.title, - status: entry.state, - timestamp: entry.updatedAt, - summary: entry.summary ?? entry.verificationSummary, - metadata: { branchName: entry.branchName, linkedItems: entry.linkedItems }, - })), - ...releasePayload.map((entry) => ({ - kind: 'release', - id: entry.id, - title: entry.title ?? entry.version, - status: entry.state, - timestamp: entry.updatedAt, - summary: entry.summary, - metadata: { version: entry.version, releaseTag: entry.releaseTag }, - })), - ...agents.messages.map((entry) => ({ - kind: 'agent_message', - id: entry.id, - title: entry.type, - status: entry.status, - timestamp: entry.createdAt, - summary: entry.summary, - metadata: entry.metadata ?? {}, - })), - ].sort((left, right) => String(right.timestamp ?? '').localeCompare(String(left.timestamp ?? ''))).slice(0, 20); - - return { - projectId: config.projectId, - teamId: String(principal?.metadata?.teamId ?? config.projectId), - health, - counts: { - objectives: direct.objectiveCount, - questions: direct.questionCount, - notes: direct.noteCount, - proposals: direct.proposalCount, - decisions: direct.decisionCount, - activeWorkstreams: workstreamPayload.filter((entry) => entry.state !== 'archived').length, - agents: agents.agents.length, - releases: releasePayload.length, - }, - nextBestAction: releaseReady - ? 'Review the ready release and decide whether to publish.' - : failedWorkstream - ? 'Inspect the latest failed verification and update the workstream.' - : 'Open Direct or Workstreams to continue work.', - recentActivity, - }; -} diff --git a/src/api/support/app.ts b/src/api/support/app.ts deleted file mode 100644 index 4884e254..00000000 --- a/src/api/support/app.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - createApiRouter as createSharedApiRouter, - type ApiServerOptions, - type PlatformApiContext, -} from '@treeseed/sdk/api'; -import { AgentSdk } from '@treeseed/sdk'; -import { MemoryAgentDatabase } from '@treeseed/sdk/d1-store'; -import type { Hono } from 'hono'; -import { registerAgentRoutes } from '../agents/agent-routes.ts'; -import { registerOperationRoutes } from '../operations/operations-routes.ts'; -import { registerProjectRoutes } from '../projects/projects-core/project-routes.ts'; - -function createDefaultAgentApiSdk(repoRoot: string | undefined) { - return new AgentSdk({ - repoRoot, - database: new MemoryAgentDatabase(), - }); -} - -export type AgentApiApp = Hono; - -export function createApiRouter(options: ApiServerOptions = {}): AgentApiApp { - const surfaces = { - auth: true, - templates: true, - sdk: true, - operations: true, - agent: true, - project: true, - ...(options.surfaces ?? {}), - }; - const extensions = [ - ...(options.extensions ?? []), - { - name: '@treeseed/agent/api', - mount(app: Parameters>[0], runtime: PlatformApiContext) { - if (surfaces.agent) { - registerAgentRoutes(app as unknown as Parameters[0], { - sdk: runtime.sharedSdk, - prefix: `${runtime.internalPrefix}/agent`, - scope: 'agent', - projectId: runtime.resolved.config.projectId, - }); - } - if (surfaces.operations) { - registerOperationRoutes(app as unknown as Parameters[0], { - config: runtime.resolved.config, - scope: runtime.resolved.scopes.operations, - prefix: runtime.internalPrefix, - sdk: runtime.sharedSdk, - executeOperation: options.workflowExecutor, - }); - } - if (surfaces.project) { - registerProjectRoutes(app as unknown as Parameters[0], { - config: runtime.resolved.config, - sharedSdk: runtime.sharedSdk, - }); - } - }, - }, - ]; - - return createSharedApiRouter({ - ...options, - sdk: options.sdk ?? createDefaultAgentApiSdk(options.config?.repoRoot), - config: { - name: '@treeseed/agent/api', - ...(options.config ?? {}), - }, - surfaces: { - ...surfaces, - operations: false, - }, - extensions, - }) as unknown as AgentApiApp; -} - -export function createApiApp(options: ApiServerOptions = {}): AgentApiApp { - return createApiRouter(options); -} diff --git a/src/api/support/capabilities.ts b/src/api/support/capabilities.ts deleted file mode 100644 index 4150b9e0..00000000 --- a/src/api/support/capabilities.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ApiPrincipal } from '@treeseed/sdk/remote'; -import { jsonError, type ApiContext } from './http.ts'; - -function stringArray(value: unknown) { - return Array.isArray(value) - ? [...new Set(value.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0))] - : []; -} - -export function principalTeamCapabilities(principal: ApiPrincipal | null) { - return stringArray(principal?.metadata?.teamCapabilities); -} - -export function principalTeamRoles(principal: ApiPrincipal | null) { - return stringArray(principal?.metadata?.teamRoles); -} - -export function hasTeamCapability(principal: ApiPrincipal | null, capability: string) { - if (!principal) return false; - if (principal.permissions.includes('*:*:*') || principal.roles.includes('project_api') || principal.roles.includes('platform_admin')) { - return true; - } - const capabilities = principalTeamCapabilities(principal); - if (capabilities.includes(capability)) { - return true; - } - return principalTeamRoles(principal).includes('team_owner'); -} - -export function requireTeamCapability(c: ApiContext, capability: string) { - if (!hasTeamCapability(c.get('principal'), capability)) { - return jsonError(c, 403, 'Permission denied.', { capability }); - } - return null; -} diff --git a/src/api/support/http.ts b/src/api/support/http.ts deleted file mode 100644 index 12b8fb6c..00000000 --- a/src/api/support/http.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { Context } from 'hono'; -import type { ApiPrincipal, ApiScope } from '@treeseed/sdk/remote'; -import type { AppVariables } from '../types.ts'; -import { permissionGranted } from '../auth/rbac.ts'; - -export type ApiContext = Context<{ Variables: AppVariables }>; - -export function jsonError( - c: Context, - status: number, - error: string, - details?: Record, -) { - return c.json({ - ok: false, - error, - ...(details ?? {}), - }, { status: status as never }); -} - -export function bearerTokenFromRequest(request: Request) { - const header = request.headers.get('authorization'); - if (!header) return null; - const match = header.match(/^Bearer\s+(.+)$/i); - return match?.[1] ?? null; -} - -export function hasScope(principal: ApiPrincipal | null, requiredScope: ApiScope) { - return Boolean(principal && (principal.scopes.includes(requiredScope) || principal.scopes.includes('*'))); -} - -export function requireScope(c: ApiContext, requiredScope: ApiScope) { - if (!hasScope(c.get('principal'), requiredScope)) { - return jsonError(c, 401, 'Authentication required.', { requiredScope }); - } - return null; -} - -export function requireAuthentication(c: ApiContext) { - if (!c.get('principal')) { - return jsonError(c, 401, 'Authentication required.'); - } - return null; -} - -export function requireActorType(c: ApiContext, actorType: 'anonymous' | 'user' | 'service' | 'project', message = 'Trusted service authentication required.') { - if (c.get('actorType') !== actorType) { - return jsonError(c, 401, message); - } - return null; -} - -export function requirePermission(c: ApiContext, permission: string) { - const principal = c.get('principal'); - if (!principal || !permissionGranted(c.get('permissionGrants'), permission)) { - return jsonError(c, 403, 'Permission denied.', { permission }); - } - return null; -} diff --git a/src/api/support/sdk-dispatch.ts b/src/api/support/sdk-dispatch.ts deleted file mode 100644 index d5837f81..00000000 --- a/src/api/support/sdk-dispatch.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { AgentSdk, RemoteSdkOperationRequest } from '@treeseed/sdk'; -import { AgentSdk as AgentSdkClass, executeSdkOperation, findSdkOperation, listSdkOperationNames } from '@treeseed/sdk'; -import type { ApiConfig } from '../types.ts'; - -export { - executeSdkOperation, - findSdkOperation, - listSdkOperationNames, -}; - -export function resolveSdkInstance( - sharedSdk: AgentSdk | undefined, - config: ApiConfig, - request: RemoteSdkOperationRequest, -) { - if (!request.repoRoot || request.repoRoot === config.repoRoot) { - return sharedSdk ?? AgentSdkClass.createLocal({ repoRoot: config.repoRoot }); - } - return AgentSdkClass.createLocal({ repoRoot: request.repoRoot }); -} diff --git a/src/api/support/sdk-routes.ts b/src/api/support/sdk-routes.ts deleted file mode 100644 index 5c57e7ba..00000000 --- a/src/api/support/sdk-routes.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Hono } from 'hono'; -import type { AgentSdk, RemoteSdkOperationRequest } from '@treeseed/sdk'; -import { executeSdkOperation, resolveSdkInstance } from './sdk-dispatch.ts'; -import { jsonError, requireScope } from './http.ts'; -import type { AppVariables, ApiConfig } from '../types.ts'; - -interface RegisterSdkRoutesOptions { - config: ApiConfig; - sharedSdk?: AgentSdk; - scope: string; - prefix?: string; -} - -function withPrefix(prefix: string, path: string) { - if (!prefix) return path; - return `${prefix}${path}`.replace(/\/{2,}/g, '/'); -} - -function routeParam(c: { req: { param: (name: string) => string | undefined } }, name: string) { - const value = c.req.param(name); - if (!value) { - throw new Error(`Missing route parameter "${name}".`); - } - return value; -} - -export function registerSdkRoutes( - app: Hono, - options: RegisterSdkRoutesOptions, -) { - app.post(withPrefix(options.prefix ?? '', '/sdk/:operation'), async (c) => { - const unauthorized = requireScope(c, options.scope); - if (unauthorized) return unauthorized; - - const operation = routeParam(c, 'operation'); - const body = await c.req.json().catch(() => ({})) as RemoteSdkOperationRequest; - try { - const result = await executeSdkOperation( - resolveSdkInstance(options.sharedSdk, options.config, body), - operation, - (body.input ?? {}) as Record, - ); - return c.json(result); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - const status = /Unknown SDK operation/.test(message) ? 400 : 500; - return jsonError(c, status, message, { operation }); - } - }); -} diff --git a/src/api/support/server.ts b/src/api/support/server.ts deleted file mode 100644 index fb5a10da..00000000 --- a/src/api/support/server.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env node - -import { isDirectEntrypoint } from '../../entrypoint.ts'; -import { createRailwayApiServer } from '../hosting/railway.ts'; - -if (isDirectEntrypoint(import.meta.url, 'server.ts')) { - const instance = await createRailwayApiServer(); - process.stdout.write(`Treeseed API listening on ${instance.url}\n`); -} diff --git a/src/api/support/templates.ts b/src/api/support/templates.ts deleted file mode 100644 index b5f39ff7..00000000 --- a/src/api/support/templates.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { createRequire } from 'node:module'; -import { dirname, resolve } from 'node:path'; -import { parseTemplateCatalogResponse } from '@treeseed/sdk/template-catalog'; -import type { SdkTemplateCatalogResponse } from '@treeseed/sdk/types'; -import type { ApiConfig } from '../types.ts'; - -const require = createRequire(import.meta.url); - -function resolveSdkPackageRoot() { - const exportedEntrypoint = require.resolve('@treeseed/sdk'); - const distRoot = dirname(exportedEntrypoint); - const packageRoot = resolve(distRoot, '..'); - return packageRoot; -} - -function resolveDefaultCatalogPath() { - const sdkRoot = resolveSdkPackageRoot(); - const candidates = [ - resolve(sdkRoot, 'dist', 'treeseed', 'template-catalog', 'catalog.fixture.json'), - resolve(sdkRoot, 'src', 'treeseed', 'template-catalog', 'catalog.fixture.json'), - ]; - - for (const candidate of candidates) { - if (existsSync(candidate)) { - return candidate; - } - } - - throw new Error('Unable to resolve the bundled Treeseed template catalog fixture.'); -} - -export function loadTemplateCatalog(config: ApiConfig): SdkTemplateCatalogResponse { - const catalogPath = config.templateCatalogPath ?? resolveDefaultCatalogPath(); - return parseTemplateCatalogResponse(JSON.parse(readFileSync(catalogPath, 'utf8')) as unknown); -} diff --git a/src/api/types.ts b/src/api/types.ts deleted file mode 100644 index 03acaa6e..00000000 --- a/src/api/types.ts +++ /dev/null @@ -1,253 +0,0 @@ -import type { Hono } from 'hono'; -import type { AgentSdk } from '@treeseed/sdk'; -import type { - ApiPrincipal, - ApiScope, - DeviceCodeApproveRequest as SdkDeviceCodeApproveRequest, - DeviceCodePollRequest, - DeviceCodePollResponse, - DeviceCodeStartRequest, - DeviceCodeStartResponse, - RemoteWorkflowOperationRequest as WorkflowHttpOperationRequest, - RemoteWorkflowOperationResponse as ApiWorkflowOperationResponse, - RemoteSdkOperationRequest as SdkHttpOperationRequest, - TokenRefreshRequest, - TokenRefreshResponse, -} from '@treeseed/sdk/remote'; - -export type { - ApiPrincipal, - ApiScope, - DeviceCodePollRequest, - DeviceCodePollResponse, - DeviceCodeStartRequest, - DeviceCodeStartResponse, - WorkflowHttpOperationRequest, - ApiWorkflowOperationResponse, - SdkHttpOperationRequest, - TokenRefreshRequest, - TokenRefreshResponse, -}; - -export type DeviceCodeApproveRequest = SdkDeviceCodeApproveRequest; - -export interface ApiAuthProvider { - readonly id: string; - startDeviceFlow(request: DeviceCodeStartRequest): Promise; - pollDeviceFlow(request: DeviceCodePollRequest): Promise; - refreshAccessToken(request: TokenRefreshRequest): Promise; - approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{ ok: true }>; - authenticateBearerToken(token: string): Promise<{ principal: ApiPrincipal; credential: ApiCredential } | null>; - authenticateServiceCredential(serviceId: string, secret: string): Promise<{ principal: ApiPrincipal; credential: ApiCredential } | null>; - createPersonalAccessToken( - userId: string, - input: { name: string; scopes?: string[]; expiresAt?: string | null }, - ): Promise<{ id: string; token: string; prefix: string; name: string; expiresAt: string | null }>; - listPersonalAccessTokens(userId: string): Promise>; - revokePersonalAccessToken(userId: string, tokenId: string): Promise; - syncUserIdentity(identity: UserIdentityProfileInput): Promise<{ - principal: ApiPrincipal; - userId: string; - identityId: string | null; - }>; - createUser?(input: { email?: string | null; username?: string | null; displayName?: string | null; metadata?: Record }): Promise<{ - principal: ApiPrincipal; - userId: string; - }>; - setUserRoles?(userId: string, roles: string[]): Promise<{ - principal: ApiPrincipal; - userId: string; - }>; - createServiceToken(input: { serviceId: string; name: string; roles?: string[]; permissions?: string[] }): Promise<{ - id: string; - serviceId: string; - secret: string; - }>; - rotateServiceToken(serviceId: string): Promise<{ - id: string; - serviceId: string; - secret: string; - }>; - createTrustedUserAssertion(claims: TrustedUserAssertionClaims): string; - verifyTrustedUserAssertion(assertion: string): TrustedUserAssertionClaims | null; - exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims): Promise<{ - ok: true; - accessToken: string; - tokenType: 'Bearer'; - expiresAt: string; - expiresInSeconds: number; - principal: ApiPrincipal; - }>; -} - -export type ApiRuntimeProviderSelections = { - auth: string; - agents: { - execution: string; - queue: string; - notification: string; - repository: string; - verification: string; - }; -}; - -export interface ApiConfig { - name: string; - host: string; - port: number; - baseUrl: string; - authApprovalBaseUrl?: string; - issuer: string; - repoRoot: string; - projectId: string; - authSecret: string; - projectApiKey?: string; - projectApiLabel: string; - projectApiPermissions: string[]; - cloudflareAccountId?: string; - cloudflareApiToken?: string; - d1DatabaseId?: string; - d1DatabaseName?: string; - d1LocalPersistTo?: string; - d1WranglerConfigPath?: string; - webServiceId: string; - webServiceSecret: string; - webAssertionSecret: string; - webExchangeTtlSeconds: number; - bootstrapAdminAllowlist: string[]; - accessTokenTtlSeconds: number; - refreshTokenTtlSeconds: number; - deviceCodeTtlSeconds: number; - deviceCodePollIntervalSeconds: number; - templateCatalogPath?: string; - providers: ApiRuntimeProviderSelections; -} - -export interface AppVariables { - requestId: string; - config: ApiConfig; - principal: ApiPrincipal | null; - actingUser: ApiPrincipal | null; - credential: ApiCredential | null; - actorType: 'anonymous' | 'user' | 'service' | 'project'; - permissionGrants: string[]; -} - -export interface ApiCredential { - type: 'access_token' | 'personal_access_token' | 'service_secret' | 'service_token' | 'project_api_key' | 'team_api_key'; - id: string; - label?: string; -} - -export interface TrustedUserAssertionClaims { - userId: string; - sessionId: string; - identityId?: string | null; - teamId?: string | null; - projectId?: string | null; - membershipId?: string | null; - teamRoles?: string[]; - teamCapabilities?: string[]; - authTime: string; - expiresAt: string; - nonce: string; -} - -export interface UserIdentityProfileInput { - provider: string; - providerSubject: string; - email?: string | null; - emailVerified?: boolean; - username?: string | null; - displayName?: string | null; - profile?: Record; -} - -export type ApiProviderFactory = (options: { config: ApiConfig }) => T; - -export interface ApiRuntimeProviders { - auth?: Record>; - agentExecution?: Record; - agentQueue?: Record; - agentNotification?: Record; - agentRepository?: Record; - agentVerification?: Record; -} - -export interface ResolvedApiRuntimeProviders { - auth: ApiAuthProvider; - registries: { - auth: Map>; - agentExecution: Map; - agentQueue: Map; - agentNotification: Map; - agentRepository: Map; - agentVerification: Map; - }; - selections: ApiRuntimeProviderSelections; -} - -export interface ApiResolvedSettings { - config: ApiConfig; - surfaces: { - auth: boolean; - templates: boolean; - sdk: boolean; - agent: boolean; - operations: boolean; - project: boolean; - }; - scopes: { - authMe: ApiScope; - sdk: ApiScope; - agent: ApiScope; - operations: ApiScope; - }; -} - -export interface ApiAppRuntime { - resolved: ApiResolvedSettings; - runtimeProviders: ResolvedApiRuntimeProviders; - sharedSdk: AgentSdk; - internalPrefix: string; -} - -export type PlatformApiContext = ApiAppRuntime; - -export interface ApiExtension { - name: string; - mount(app: Hono, context: PlatformApiContext): void | Promise; -} - -export interface ApiServerOptions { - config?: Partial; - runtimeProviders?: ApiRuntimeProviders; - sdk?: AgentSdk; - workflowExecutor?: (operation: string, request: WorkflowHttpOperationRequest) => Promise; - surfaces?: Partial<{ - auth: boolean; - templates: boolean; - sdk: boolean; - agent: boolean; - operations: boolean; - project: boolean; - }>; - scopes?: Partial<{ - authMe: ApiScope; - sdk: ApiScope; - agent: ApiScope; - operations: ApiScope; - }>; - internalPrefix?: string; - extensions?: ApiExtension[]; - extendApp?: (app: Hono, runtime: ApiAppRuntime) => void; - log?: (message: string, details?: Record) => void; -} diff --git a/src/env.yaml b/src/env.yaml index 57b356a6..437a3d39 100644 --- a/src/env.yaml +++ b/src/env.yaml @@ -1,2082 +1,121 @@ entries: - TREESEED_AI_GATEWAY_TOKEN: - label: Local AI inference gateway token - group: auth - description: Bearer token used by TreeSeed-model execution providers to access the loopback AI appliance gateway. - howToGet: Generate a machine-local random token during appliance installation and store the same value in the appliance secret file and provider runtime configuration. - sensitivity: secret - targets: - - local-runtime - scopes: - - local + TREESEED_CAPACITY_PROVIDER_MANIFEST: + label: Capacity provider manifest + group: provider + description: Path to the repository-governed provider manifest. + sensitivity: public + targets: [local-runtime] + scopes: [local, staging] storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - minLength: 32 - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_OPENCODE_AUTH_FILE: - label: OpenCode auth file - group: auth - description: Optional path to OpenCode's local subscription credential store for the subscription-backed execution-provider profile. - howToGet: Authenticate OpenCode on the provider host, then select its auth.json path without copying its contents into project configuration. - sensitivity: secret - targets: - - local-runtime - scopes: - - local - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CODEX_API_KEY: - label: Codex API key - group: auth - description: Optional API-billed OpenAI key used by TreeSeed's Codex SDK adapter when Codex login auth.json is not available. - howToGet: >- - Prefer subscription-backed Codex login, which creates ~/.codex/auth.json. - Use TREESEED_CODEX_API_KEY only when API billing is intended: create or find a key - at https://platform.openai.com/api-keys, then store it as TREESEED_CODEX_API_KEY. - Do not commit it, print it, or paste it into logs. If this is the first - API key on the account, OpenAI may require phone verification before the - key can be created. - sensitivity: secret - targets: - - local-runtime - - github-secret - - railway-secret - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - minLength: 20 - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_AUTH_FILE: - label: Codex auth file - group: auth - description: Optional path to a Codex login auth.json file. Defaults to ~/.codex/auth.json, or $CODEX_HOME/auth.json when CODEX_HOME is set. - howToGet: >- - Run Codex login on a trusted machine to create ~/.codex/auth.json, then - copy that file to the service host if needed. Store the path here only - when the file is not at the default location. Treat auth.json like a - secret and do not commit or print it. - sensitivity: secret - targets: - - local-runtime - - github-secret - - railway-secret - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_AUTH_JSON_B64: - label: Codex auth JSON bootstrap secret - group: auth - description: Base64-encoded Codex login auth.json used only to bootstrap a missing auth file on a worker volume. - howToGet: >- - On a trusted machine, run Codex login with the automation account, then - base64-encode the resulting auth.json and store it as a Railway secret. - TreeSeed writes it to TREESEED_CODEX_AUTH_FILE, or to - /run/treeseed-secrets/codex-auth.json in provider diagnostic mode, only when the file is - missing. Do not bake this value into images, logs, source files, or test - fixtures. Because Codex may refresh and rewrite auth.json, keep the file - on a persistent worker volume and avoid overwriting it on every boot. - sensitivity: secret - targets: - - railway-secret - - github-secret - scopes: - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - bootstrap - - config - validation: - kind: nonempty - minLength: 20 - sourcePriority: - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_AUTH_JSON: - label: Codex auth JSON bootstrap secret - group: auth - description: Raw Codex login auth.json used only to bootstrap a missing auth file. Prefer TREESEED_CODEX_AUTH_JSON_B64 for hosted secrets. - howToGet: >- - On a trusted machine, run Codex login with the automation account, then - store the resulting auth.json content as a hosted secret only when base64 - transport is not available. Prefer TREESEED_CODEX_AUTH_JSON_B64 for - Railway and GitHub environments. - sensitivity: secret - targets: - - railway-secret - scopes: - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - bootstrap - - config - validation: - kind: nonempty - sourcePriority: - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_AUTH_OVERWRITE: - label: Overwrite Codex auth file - group: auth - description: Set to 1 only during an intentional auth rotation. By default TreeSeed will not overwrite an existing auth.json because Codex may have refreshed it on the persistent worker volume. - howToGet: Leave unset for normal operation. Set to 1 only while intentionally rotating a hosted Codex login secret. - sensitivity: plain - targets: - - railway-var - - local-runtime - scopes: - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - bootstrap - - config - validation: - kind: boolean - sourcePriority: - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_SUBSCRIPTION_PLAN: - label: Codex subscription plan - group: auth - description: Declares the Codex subscription tier used by the processing workers for readiness reporting and policy diagnostics. - howToGet: Set this to the subscription plan for the automation account used by hosted Codex workers. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: enum - values: - - plus - - pro - - business - - edu - - enterprise - - unknown - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_DEFAULT_MODEL: - label: Codex default model - group: auth - description: Default model passed to Codex-backed agent execution when an agent spec does not provide a narrower model. - howToGet: Use the approved default model for the worker environment, such as gpt-5.5. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_APPROVAL_POLICY: - label: Codex approval policy - group: auth - description: Approval policy passed to Codex execution for hosted workers. - howToGet: Use never for noninteractive hosted workers unless a governed workflow explicitly supports request handling. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: enum - values: - - never - - on_request - - always - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_SANDBOX_MODE: - label: Codex sandbox mode - group: auth - description: Sandbox mode passed to Codex execution for hosted workers. - howToGet: Use workspace_write for mutation-capable workers and read_only for inspection-only workers. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: enum - values: - - read_only - - workspace_write - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_TIMEOUT_MS: - label: Codex execution timeout - group: auth - description: Maximum Codex execution duration in milliseconds for one agent task. - howToGet: Set a bounded timeout that matches the worker queue lease and expected task complexity. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_REQUIRE_RELEASE_DECISION: - label: Require Codex release decision - group: auth - description: Requires a governed decision before production release actions may proceed from Codex-backed automation. - howToGet: Leave true for staging and production unless a separate approval gate replaces it. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: boolean - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_ALLOW_FEATURE_BRANCH_MUTATION: - label: Allow Codex feature branch mutation - group: auth - description: Allows Codex-backed workers to mutate approved feature branch worktrees. - howToGet: Leave true for mutation-capable workers; set false for read-only processing environments. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: boolean - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_ALLOW_AUTOMATIC_STAGING_MERGE: - label: Allow automatic staging merge - group: auth - description: Allows governed Codex automation to merge approved staged work when policy permits. - howToGet: Set according to the staging governance policy for the project. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: boolean - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_REQUIRE_ALLOWED_PATHS: - label: Require Codex allowed paths - group: auth - description: Requires mutation-capable Codex agents to declare allowed paths before file changes are accepted. - howToGet: Leave true for hosted workers. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: boolean - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_CODEX_RECORD_THREAD_IDS: - label: Record Codex thread IDs - group: auth - description: Records Codex thread identifiers in task metadata and reports when the execution provider returns them. - howToGet: Leave true unless task metadata must avoid provider thread references. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: boolean - sourcePriority: - - machine-config - - process-env - relevanceRef: codexExecutionSelected - TREESEED_JIRA_BASE_URL: - label: Jira base URL - group: auth - description: Base URL for the Jira site used by the Jira human issue queue execution provider. - howToGet: Copy the Atlassian site URL, such as https://example.atlassian.net. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_EMAIL: - label: Jira account email - group: auth - description: Email for the Jira account used by the Jira human issue queue execution provider. - howToGet: Use a provider-owned Jira service account email. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_API_TOKEN: - label: Jira API token - group: auth - description: API token for the Jira human issue queue execution provider. - howToGet: Create an Atlassian API token for the provider-owned Jira service account. - sensitivity: secret - targets: - - local-runtime - - github-secret - - railway-secret - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_PROJECT_KEY: - label: Jira project key - group: auth - description: Jira project key where Treeseed assignment issues are created. - howToGet: Copy the project key from Jira project settings. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_ISSUE_TYPE: - label: Jira issue type - group: auth - description: Jira issue type created for Treeseed assignments. - howToGet: Use Task unless the target Jira project requires another issue type. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_DONE_STATUSES: - label: Jira done statuses - group: auth - description: Comma-separated Jira statuses that map an assignment issue to completed. - howToGet: Match the target Jira workflow status names. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_BLOCKED_STATUSES: - label: Jira blocked statuses - group: auth - description: Comma-separated Jira statuses that map an assignment issue to blocked. - howToGet: Match the target Jira workflow status names. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_CANCELLED_STATUSES: - label: Jira cancelled statuses - group: auth - description: Comma-separated Jira statuses that map an assignment issue to failed/cancelled. - howToGet: Match the target Jira workflow status names. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_IN_PROGRESS_STATUSES: - label: Jira in-progress statuses - group: auth - description: Comma-separated Jira statuses that map an assignment issue to running. - howToGet: Match the target Jira workflow status names. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_JIRA_STORY_POINTS_FIELD: - label: Jira story points field - group: auth - description: Optional Jira custom field id used to collect partial story point usage. - howToGet: Copy the Jira custom field id, such as customfield_10016, when story point usage should be collected. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_GITHUB_ISSUES_TOKEN: - label: GitHub Issues provider token - group: auth - description: Provider-scoped GitHub token used only by the GitHub Issues human issue queue execution provider. - howToGet: Create a fine-grained GitHub token with Issues read/write access to the configured repository. - sensitivity: secret - targets: - - local-runtime - - github-secret - - railway-secret - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - minLength: 8 - sourcePriority: - - machine-config - - process-env - TREESEED_GITHUB_ISSUES_REPOSITORY: - label: GitHub Issues repository - group: auth - description: Repository in owner/repo form where GitHub issue queue assignments are created. - howToGet: Choose the provider-owned repository that should receive Treeseed assignment issues. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_GITHUB_ISSUES_LABELS: - label: GitHub Issues base labels - group: auth - description: Comma-separated labels applied to every Treeseed GitHub issue queue assignment. - howToGet: Leave unset to use treeseed, or provide existing repository labels. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_GITHUB_ISSUES_IN_PROGRESS_LABELS: - label: GitHub Issues in-progress labels - group: auth - description: Comma-separated labels that map an open GitHub assignment issue to running. - howToGet: Leave unset to use treeseed-in-progress. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_GITHUB_ISSUES_BLOCKED_LABELS: - label: GitHub Issues blocked labels - group: auth - description: Comma-separated labels that map a GitHub assignment issue to blocked. - howToGet: Leave unset to use treeseed-blocked. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_GITHUB_ISSUES_CANCELLED_LABELS: - label: GitHub Issues cancelled labels - group: auth - description: Comma-separated labels that map a GitHub assignment issue to failed/cancelled. - howToGet: Leave unset to use treeseed-cancelled. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_DISCORD_BOT_TOKEN: - label: Discord bot token - group: auth - description: Bot token used by the Discord execution provider to create assignment messages and threads. - howToGet: Create a Discord bot for the provider workspace and grant it access to the target channel. - sensitivity: secret - targets: - - local-runtime - - github-secret - - railway-secret - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - minLength: 8 - sourcePriority: - - machine-config - - process-env - TREESEED_DISCORD_CHANNEL_ID: - label: Discord channel ID - group: auth - description: Discord channel id where Treeseed assignment messages and threads are created. - howToGet: Enable Discord developer mode and copy the target channel id. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_DISCORD_GUILD_ID: - label: Discord guild ID - group: auth - description: Optional Discord guild id used to build external Discord thread and message URLs. - howToGet: Enable Discord developer mode and copy the server id. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_DISCORD_THREAD_PREFIX: - label: Discord thread prefix - group: auth - description: Prefix used when naming Discord assignment threads. - howToGet: Leave unset to use treeseed. - sensitivity: plain - targets: - - local-runtime - - github-variable - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - agent-execution - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_RAILWAY_PROJECT_TOKEN: - label: Railway project token - group: auth - description: Project-scoped Railway token used by the Railway CLI for CI deploy uploads. Treeseed can mint a scoped token from the shared SDK-owned Railway API token during provider deploys when this is not configured. - howToGet: In Railway, open the target project environment settings, create a project token, and store it as TREESEED_RAILWAY_PROJECT_TOKEN. Leave unset when Treeseed should mint an ephemeral CI deploy token from TREESEED_RAILWAY_API_TOKEN. - sensitivity: secret - targets: - - github-secret - scopes: - - staging - - prod - storage: scoped - requirement: optional - purposes: - - deploy - validation: - kind: nonempty - minLength: 8 - sourcePriority: - - machine-config - - process-env - relevanceRef: railwayManagedEnabled - TREESEED_RAILWAY_WORKSPACE: - label: Railway workspace - group: railway - description: Railway workspace Treeseed should use when listing or creating projects during bootstrap and config reconciliation. - howToGet: In Railway, use the workspace slug or name shown in the workspace switcher. Treeseed defaults this repository to treeseed-ai unless you override it here. - sensitivity: plain - targets: - - railway-var - scopes: - - staging - - prod - storage: shared - requirement: conditional - purposes: - - deploy - - destroy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - defaultValueRef: railwayWorkspaceDefault - relevanceRef: railwayManagedEnabled - requiredWhenRef: railwayManagedEnabled - TREESEED_PROJECT_RUNNER_TOKEN: - label: Project runner registration token - group: hosting - description: Shared bearer token used by a registered project runtime to report project infrastructure and deployment state to the market control plane. - howToGet: Generate or rotate this token from the market control plane after connecting a project environment. - sensitivity: secret - targets: - - github-secret - - railway-secret - scopes: - - staging - - prod - storage: scoped - requirement: conditional - purposes: - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: projectRegistrationEnabled - TREESEED_RAILWAY_PROJECT_ID: - label: Railway project ID - group: hosting - visibility: system - description: Railway project identifier used by canonical reconciliation helpers for the active environment. - howToGet: Copy the project ID from Railway when the environment is managed through Railway reconciliation. - sensitivity: plain - targets: - - github-variable - - railway-var - scopes: - - staging - - prod - storage: scoped - requirement: optional - purposes: - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_RAILWAY_ENVIRONMENT_ID: - label: Railway environment ID - group: hosting - visibility: system - description: Railway environment identifier used by canonical Railway deployment and reconciliation operations. - howToGet: Copy the environment ID from Railway for the matching staging or production environment when reconciliation cannot discover it. - sensitivity: plain - targets: - - github-variable - - railway-var - scopes: - - staging - - prod - storage: scoped - requirement: optional - purposes: - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - TREESEED_API_BASE_URL: - label: Treeseed API base URL - group: auth - description: Base URL for the API used by the site BFF, CLI, and remote auth flows. Local defaults should resolve automatically from the integrated runtime, while hosted environments prefer configured service URLs and project domains. - howToGet: Override this only when the generated local or hosted API URL is wrong for the selected environment. - sensitivity: plain - targets: - - local-runtime - - local-cloudflare - - github-variable - - railway-var - - cloudflare-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: required - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - defaultValueRef: apiBaseUrlDefault - relevanceRef: apiSurfaceEnabled - TREESEED_API_D1_DATABASE_ID: - label: API D1 database ID - group: auth - description: Cloudflare D1 database identifier used by the Railway API for remote auth and session persistence. - howToGet: In Cloudflare D1, copy the database ID for SITE_DATA_DB and paste it here. - sensitivity: plain - targets: - - railway-var - scopes: - - staging - - prod - requirement: optional - purposes: - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_D1_DATABASE_NAME: - label: API D1 database name - group: auth - description: D1 binding or database name used by the Railway API for local Wrangler-backed auth access. - howToGet: Use the D1 binding name, typically SITE_DATA_DB. - sensitivity: plain - targets: - - local-runtime - scopes: - - local - requirement: optional - purposes: - - dev - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_D1_LOCAL_PERSIST_TO: - label: API local D1 persist path - group: auth - description: Optional Wrangler D1 persistence directory used when the Railway API runs locally against SITE_DATA_DB. - howToGet: Use the local Wrangler D1 persist directory, such as .wrangler/state/v3/d1. - sensitivity: plain - targets: - - local-runtime - scopes: - - local - requirement: optional - purposes: - - dev - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_WEB_SERVICE_ID: - label: API trusted web service ID - group: auth - description: Trusted web service identity expected by the agent Treeseed API runtime in @treeseed/agent for Astro BFF calls. - howToGet: Match this to the web service ID configured on the Astro side, typically web. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - requirement: required - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - defaultValueRef: apiWebServiceIdDefault - localDefaultValueRef: apiWebServiceIdDefault - relevanceRef: apiSurfaceEnabled - TREESEED_API_WEB_SERVICE_SECRET: - label: API trusted web service secret - group: auth - description: Shared secret expected by the agent Treeseed API runtime in @treeseed/agent for Astro BFF calls. - howToGet: Match this to the web service secret configured on the Astro side. - sensitivity: secret - targets: - - local-runtime - - github-secret - - railway-secret - scopes: - - local - - staging - - prod - requirement: required - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - defaultValueRef: generatedSecret - localDefaultValueRef: generatedSecret - relevanceRef: apiSurfaceEnabled - TREESEED_API_WEB_ASSERTION_SECRET: - label: API web assertion secret - group: auth - description: Secret used by the agent Treeseed API runtime in @treeseed/agent to verify trusted web user-context assertions. - howToGet: Match this to the web assertion secret configured on the Astro side. - sensitivity: secret - targets: - - local-runtime - - github-secret - - railway-secret - scopes: - - local - - staging - - prod - requirement: required - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - defaultValueRef: generatedSecret - localDefaultValueRef: generatedSecret - relevanceRef: apiSurfaceEnabled - TREESEED_API_AUTH_SECRET: - label: API auth secret - group: auth - description: Secret used by the agent API auth provider to sign access, refresh, service, and trusted exchange tokens. - howToGet: Generate a strong random secret and keep it stable per API environment. - sensitivity: secret - targets: - - local-runtime - - github-secret - - railway-secret - scopes: - - local - - staging - - prod requirement: required - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - defaultValueRef: generatedSecret - localDefaultValueRef: generatedSecret - relevanceRef: apiSurfaceEnabled - TREESEED_API_ISSUER: - label: API token issuer - group: auth - description: Issuer claim used in agent API auth tokens. Defaults to TREESEED_API_BASE_URL when unset. - howToGet: Use the public API base URL for the environment. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: url - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_PROVIDER_AUTH: - label: API auth provider - group: auth - description: Core API auth provider implementation. Use d1 for durable users, identities, API tokens, and sessions. - howToGet: Use d1 for market environments. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: enum - values: - - d1 - - memory - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_ACCESS_TOKEN_TTL: - label: API access token TTL - group: auth - description: Access token lifetime in seconds for agent API user and service exchanges. - howToGet: Use 900 for a 15 minute access token unless a stricter policy is required. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_REFRESH_TOKEN_TTL: - label: API refresh token TTL - group: auth - description: Refresh token lifetime in seconds for agent API user sessions. - howToGet: Use 604800 for seven days, or a shorter value for stricter environments. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_WEB_EXCHANGE_TTL: - label: API web exchange TTL - group: auth - description: Lifetime in seconds for trusted web-to-API exchanged access tokens. - howToGet: Use 300 for short-lived browser BFF exchanges. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_DEVICE_CODE_TTL: - label: API device code TTL - group: auth - description: Device authorization code lifetime in seconds. - howToGet: Use 600 for a ten minute device authorization window. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_DEVICE_CODE_POLL_INTERVAL: - label: API device code poll interval - group: auth - description: Minimum polling interval in seconds for device authorization clients. - howToGet: Use 5 unless clients need a slower polling cadence. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_API_BOOTSTRAP_ADMIN_ALLOWLIST: - label: API bootstrap admin allowlist - group: auth - description: Comma-separated list of trusted emails or provider subjects that receive the platform_admin root role on first identity sync. The platform_admin role grants every global permission. - howToGet: | - Recipe: - 1. Decide who the root market operator is before the first sign-in for an environment. - 2. For internal email/password registration, add the normalized email address, for example founder@example.com. - 3. For OAuth-only bootstrapping, add provider:subject entries after you know the provider subject, for example github:1234567 or google:109876543210987654321. - 4. Separate multiple trusted identities with commas. - 5. Start narrowly. Remove bootstrap entries after the intended root users have signed in and received platform_admin. - 6. In the UI, the signed-in root user is shown on /app/account as "Root user" with the platform_admin badge. - Notes: - - platform_admin is defined in core RBAC and maps to *:*:*. - - Agent owns reusable global roles and permissions; market team ownership remains in the market tables. - sensitivity: plain - targets: - - local-runtime - - cloudflare-var - - railway-var - scopes: - - local - - staging - - prod - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: apiSurfaceEnabled - TREESEED_MARKET_URL: - label: Capacity provider Market URL - group: capacity-provider - description: Base URL for the TreeSeed API that authenticates this standalone capacity provider. - howToGet: Use the TreeSeed API URL shown in the provider connection instructions. Local development usually uses http://127.0.0.1:3000. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: url - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_MARKET_ID: - label: Capacity provider Market ID - group: capacity-provider - description: Stable configured Market identifier used by the provider client. - howToGet: Use the selected trsd market profile id, such as local, staging, or prod. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CAPACITY_ACCEPTANCE_API_URL: - label: Capacity acceptance API URL - group: capacity-provider - description: API base URL used by the capacity runtime proof. Local defaults to http://127.0.0.1:3000. - howToGet: Use managed local dev API URL, or set staging API URL for hosted proof. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: url - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CAPACITY_ACCEPTANCE_ADMIN_TOKEN: - label: Capacity acceptance admin token - group: capacity-provider - description: Bearer token for capacity runtime proof diagnostics. Local proof may use the provider API key for the read-only diagnostics projection. - howToGet: Store a scoped team API key or admin token through Treeseed config. - sensitivity: secret - targets: - - local-runtime - - railway-secret - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - minLength: 8 - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CAPACITY_ACCEPTANCE_TEAM_ID: - label: Capacity acceptance team ID - group: capacity-provider - description: Team id used by the capacity runtime proof. Local defaults to treeseed. - howToGet: Use the team that owns the proof project. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CAPACITY_ACCEPTANCE_PROJECT_ID: - label: Capacity acceptance project ID - group: capacity-provider - description: Project id used by the capacity runtime proof. Local defaults to market. - howToGet: Use the project that owns proof assignments and diagnostics. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CAPACITY_ACCEPTANCE_PROVIDER_ID: - label: Capacity acceptance provider ID - group: capacity-provider - description: Provider id used by the capacity runtime proof. Local defaults to treeseed-local-dev. - howToGet: Use the capacity provider assigned to the proof project. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CAPACITY_ACCEPTANCE_AGENT_CLASS_ID: - label: Capacity acceptance agent class ID - group: capacity-provider - description: Agent class used by the capacity runtime proof. Local defaults to planning. - howToGet: Use an agent class available on the proof project. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CAPACITY_ACCEPTANCE_ENVIRONMENT: - label: Capacity acceptance environment - group: capacity-provider - description: Environment profile for the capacity runtime proof. - howToGet: Leave unset for local proof, or set to staging for hosted proof. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: enum - values: - - local - - staging - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN: - label: Capacity provider access token - group: capacity-provider - description: Provider-scoped bearer key used by the standalone capacity provider to authenticate to Market provider endpoints. - howToGet: Create or rotate the key from the Market capacity provider page, then store it through trsd config. Do not write it to plaintext env files. - sensitivity: secret - targets: - - local-runtime - - railway-secret - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - minLength: 8 - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_HOST_DATA_DIR: - label: Capacity provider host data directory - group: capacity-provider - description: Host directory mounted as /data inside the capacity provider containers. - howToGet: Use a provider-specific path beneath .treeseed/local-capacity-providers for local development or a dedicated persistent volume path on a hosted provider. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_ENVIRONMENT: - label: Capacity provider environment - group: capacity-provider - description: Environment label reported by the provider runtime. - howToGet: Use local, staging, or prod to match the Market environment being served. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled + purposes: [config] + validation: { kind: nonempty } + sourcePriority: [process-env] TREESEED_PROVIDER_DATA_DIR: - label: Capacity provider container data directory - group: capacity-provider - description: Data directory inside provider containers. Treeseed-managed images default this to /data. - howToGet: Leave unset for hosted and local container runtimes unless the provider image uses a custom mounted path. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_STARTUP_MODE: - label: Capacity provider startup mode - group: capacity-provider - description: Provider entrypoint mode. Use live for normal service startup and diagnostic for local/container health probes. - howToGet: Leave unset for live hosted providers; set diagnostic only for explicit diagnostic container runs. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod + label: Provider data directory + group: provider + description: Private runtime state directory for leases, tokens, and receipts. + sensitivity: private + targets: [local-runtime] + scopes: [local, staging] storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: enum - values: - - live - - diagnostic - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_ROLE: - label: Capacity provider service role - group: capacity-provider - description: Role executed by a provider service instance. - howToGet: Treeseed provider templates set this to api, manager, or runner for each service. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - deploy - - config - validation: - kind: enum - values: - - api - - manager - - runner - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_RUNNER_ID: - label: Capacity provider runner ID - group: capacity-provider - description: Stable runner service identifier used by provider runtime diagnostics and hosted service checks. - howToGet: Treeseed hosted provider reconciliation sets this for runner service instances. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_MANAGER_POLL_SECONDS: - label: Capacity provider manager poll seconds - group: capacity-provider - description: Poll interval for the provider manager lease loop. - howToGet: Leave unset to use the runtime default. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_RUNNER_POLL_SECONDS: - label: Capacity provider runner poll seconds - group: capacity-provider - description: Poll interval for provider runner assignment checks. - howToGet: Leave unset to use the runtime default. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_RUNTIME_VERSION: - label: Capacity provider runtime version - group: capacity-provider - description: Runtime protocol/version label advertised by provider services. - howToGet: Leave unset to use the package runtime default. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_CAPABILITIES_FILE: - label: Capacity provider capabilities file - group: capacity-provider - description: Optional YAML file overriding the runtime-broadcast provider capabilities. - howToGet: Leave unset for the built-in Codex documentation-work capability, or point to a YAML file mounted under /data. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod - storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled - TREESEED_PROVIDER_BUDGET_FILE: - label: Capacity provider budget file - group: capacity-provider - description: Optional YAML file overriding the runtime-broadcast provider budget capacities. - howToGet: Leave unset for environment-derived defaults, or point to a YAML file mounted under /data. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod + requirement: required + purposes: [runtime] + validation: { kind: nonempty } + sourcePriority: [process-env] + TREESEED_PROVIDER_ENVIRONMENT: + label: Provider environment + group: provider + description: Runtime environment name included in provider evidence. + sensitivity: public + targets: [local-runtime] + scopes: [local, staging] storage: scoped - requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: nonempty - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled + requirement: required + purposes: [config] + validation: { kind: nonempty } + sourcePriority: [process-env] + TREESEED_AGENT_EXECUTOR_MODULE: + label: Trusted agent executor module + group: execution + description: Optional trusted module that owns model execution; absent means the provider advertises no executable capacity. + sensitivity: private + targets: [local-runtime] + scopes: [local, staging] + storage: scoped + requirement: optional + purposes: [agent-execution] + validation: { kind: nonempty } + sourcePriority: [machine-config, process-env] + TREESEED_SERVER_PROFILE_LOCAL_URL: + label: Local control-plane URL + group: control-plane + description: Reachable local API URL for provider connections using the local server profile. + sensitivity: public + targets: [local-runtime] + scopes: [local] + storage: scoped + requirement: optional + purposes: [config] + validation: { kind: url } + sourcePriority: [machine-config, process-env] + TREESEED_SERVER_PROFILE_LOCAL_AUDIENCE: + label: Local control-plane audience + group: control-plane + description: OAuth audience bound to the local control-plane server profile. + sensitivity: public + targets: [local-runtime] + scopes: [local] + storage: scoped + requirement: optional + purposes: [config] + validation: { kind: url } + sourcePriority: [machine-config, process-env] TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS: - label: Capacity provider max concurrent workdays - group: capacity-provider - description: Maximum number of active workdays this provider should plan concurrently. - howToGet: Leave unset to use the package default of 1. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod + label: Maximum concurrent workdays + group: capacity + description: Provider-local ceiling applied beneath API admission. + sensitivity: public + targets: [local-runtime] + scopes: [local, staging] storage: scoped requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled + purposes: [capacity] + validation: { kind: integer } + sourcePriority: [process-env] TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS: - label: Capacity provider max concurrent runners - group: capacity-provider - description: Maximum number of runner processes this provider should use. - howToGet: Leave unset to use the package default of 4. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod + label: Maximum concurrent runners + group: capacity + description: Provider-local runner ceiling applied beneath API admission. + sensitivity: public + targets: [local-runtime] + scopes: [local, staging] storage: scoped requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled + purposes: [capacity] + validation: { kind: integer } + sourcePriority: [process-env] TREESEED_PROVIDER_DAILY_AGENT_SECONDS_LIMIT: - label: Capacity provider daily agent-time limit - group: capacity-provider - description: Optional daily active agent-seconds enforced by this provider. - howToGet: Leave unset when Market policy should decide the limit. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod + label: Daily agent seconds + group: capacity + description: Optional provider-local daily execution ceiling. + sensitivity: public + targets: [local-runtime] + scopes: [local, staging] storage: scoped requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled + purposes: [capacity] + validation: { kind: integer } + sourcePriority: [process-env] TREESEED_PROVIDER_MONTHLY_AGENT_SECONDS_LIMIT: - label: Capacity provider monthly agent-time limit - group: capacity-provider - description: Optional monthly active agent-seconds enforced by this provider. - howToGet: Leave unset when Market policy should decide the limit. - sensitivity: plain - targets: - - local-runtime - - railway-var - scopes: - - local - - staging - - prod + label: Monthly agent seconds + group: capacity + description: Optional provider-local monthly execution ceiling. + sensitivity: public + targets: [local-runtime] + scopes: [local, staging] storage: scoped requirement: optional - purposes: - - dev - - deploy - - config - validation: - kind: number - sourcePriority: - - machine-config - - process-env - relevanceRef: processingPlaneEnabled + purposes: [capacity] + validation: { kind: integer } + sourcePriority: [process-env] diff --git a/src/index.ts b/src/index.ts index 2f7a829f..3a9eaf5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,63 +1,14 @@ -export { AgentKernel } from './agents/kernel/agents/agent-kernel.ts'; -export { listAgentCommands, renderAgentHelp, runAgentCli } from './agents/support/cli.ts'; -export { resolveAgentHandler, listRegisteredAgentHandlers } from './agents/support/registry.ts'; -export { resolveAgentRuntimeProviders } from './agent-runtime.ts'; -export { - checkCodexProviderReadiness, - resolveCodexProviderConfig, -} from './agents/adapters/codex/codex-readiness.ts'; -export { - codexClientEnvironment, - materializeCodexAuthFromEnv, - resolveCodexAuthFile, -} from './agents/adapters/accounts/codex-auth.ts'; -export { - CodexRequestSafetyError, - CodexExecutionProviderAdapter, - runCodexTask, - validateCodexExecutionRequest, -} from './agents/adapters/codex/execution-codex.ts'; -export { - createAgentToolMcpServerCommand, - startAgentToolMcpServer, -} from './agents/tools/agent-tool-mcp-server.ts'; -export { callTreeDxProxyTool } from './agents/tools/treedx-proxy-client.ts'; -export { - JiraExecutionProviderAdapter, - resolveJiraExecutionProviderConfig, -} from './agents/adapters/integrations/execution-jira.ts'; -export { - WorkflowExecutionProviderAdapter, - type WorkflowExecutionProviderAdapterOptions, - type WorkflowOperationDispatchResult, -} from './agents/adapters/operations/execution-workflow.ts'; -export { createOperationsAdapter, SdkOperationsAdapter } from './agents/adapters/operations/operations.ts'; -export { - createApiApp, - createApiRouter, - createNodeServer, - createRailwayApiServer, - resolveApiConfig, - resolveApiRuntimeProviders, -} from './api/index.ts'; -export { - diagnoseAgentAuthoring, - summarizeAgentAuthoringDiagnostics, -} from './agents/testing/agent-authoring-diagnostics.ts'; -export { parseAgentMessagePayload, AGENT_MESSAGE_TYPES } from './agents/contracts/messages.ts'; -export { resolveHandlerContextPacks } from './agents/context/context-processor.ts'; -export { AgentWorktreeManager, changedPathViolations } from './services/agent-worktrees.ts'; -export type * from './agents/runtime/runtime-types.ts'; -export type * from './agents/contracts/run.ts'; -export type * from './agents/contracts/research.ts'; -export type * from './agents/contracts/knowledge.ts'; -export type * from './agents/context/context-processor.ts'; -export type * from './services/agent-worktrees.ts'; -export type * from './agents/adapters/codex/codex-readiness.ts'; -export type * from './agents/adapters/accounts/codex-auth.ts'; -export type * from './agents/adapters/codex/execution-codex.ts'; -export type * from './agents/tools/agent-tool-mcp-server.ts'; -export type * from './agents/testing/agent-authoring-diagnostics.ts'; -export type * from './agents/adapters/integrations/execution-jira.ts'; -export type * from './agents/adapters/operations/execution-workflow.ts'; -export type * from './api/types.ts'; +export type { + AgentExecutionRequest, + AgentExecutionResult, + AgentExecutor, + AgentExecutorModule, + AgentExecutorObservation, +} from './provider/execution/contracts.ts'; +export { resolveAgentExecutor } from './provider/execution/executor-loader.ts'; +export { runProviderAssignment, type ProviderAssignmentRunInput } from './provider/operations/runner.ts'; +export { + CapacityProviderCoordinator, + type ProviderConnectionResult, + type ProviderConnectionRuntime, +} from './provider/coordination/coordinator.ts'; diff --git a/src/provider-acceptance.ts b/src/provider-acceptance.ts deleted file mode 100644 index 7fbd2a27..00000000 --- a/src/provider-acceptance.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { isAbsolute, resolve } from 'node:path'; -import { stringify as stringifyYaml } from 'yaml'; -import { ProviderProtocolClient, type CapacityProviderPrivateJwk } from '@treeseed/sdk/capacity-provider'; -import { resolveProviderConfig } from './provider/configuration/config.ts'; -import { ProviderLocalCapacityStore } from './provider/capacity/capacity-core/local-capacity-store.ts'; -import { DEFAULT_PROVIDER_MANIFEST, loadProviderManifest } from './provider/configuration/manifest.ts'; -import { runMultiTeamProviderManager, runMultiTeamProviderRunners } from './provider/teams/multi-team-runtime.ts'; - -export interface LiveCapacityAcceptanceInput { - runId: string; - cwd: string; - env: NodeJS.ProcessEnv | Record; - apiUrl: string; - teamId: string; - projectId: string; - providerId: string; - membershipId: string; - credentialId: string; - membershipCredential: string; - providerAccessToken: string; - providerSessionId: string; - providerSessionSequence: number; - privateJwk: CapacityProviderPrivateJwk; - assignmentId?: string | null; - assignmentIds?: string[]; - expectedAssignmentCount?: number; - maxConcurrentRunners?: number; - repositoryRoot?: string; - executionProviderId: string; - capabilities?: string[]; - activityProfile?: { - kind: 'research-planning' | 'research-workflow' | 'engineering-workflow'; - subjectModel: 'objective' | 'question'; - subjectSlug: string; - }; - competingConnection?: { - teamId: string; - projectId: string; - providerId: string; - membershipId: string; - credentialId: string; - membershipCredential: string; - providerAccessToken: string; - providerSessionId: string; - providerSessionSequence: number; - assignmentId: string; - }; -} - -export async function resolveLiveAcceptanceExecutionProvider(input: Pick) { - const capabilities = [...new Set(input.capabilities ?? ['planning', 'agent_mode_run', 'repo_read', 'usage_report'])]; - const configuredPath = String(input.env.TREESEED_CAPACITY_PROVIDER_MANIFEST ?? '').trim(); - const manifestPath = configuredPath - ? isAbsolute(configuredPath) ? configuredPath : resolve(input.cwd, configuredPath) - : resolve(input.cwd, DEFAULT_PROVIDER_MANIFEST); - const configured = await loadProviderManifest(manifestPath); - const source = configured.manifest.executionProviders.find((provider) => provider.id === input.executionProviderId); - if (!source) { - throw new Error(`Live acceptance execution provider ${input.executionProviderId} is not declared in ${manifestPath}.`); - } - if (capabilities.includes('research') && !source.researchSourcePolicy) { - throw new Error(`Live acceptance research provider ${input.executionProviderId} requires researchSourcePolicy in ${manifestPath}.`); - } - return { - ...source, - id: input.executionProviderId, - adapter: 'codex' as const, - nativeLimits: { maxConcurrentRunners: input.maxConcurrentRunners ?? 1, availableAgentSeconds: 3_600 }, - capabilities, - }; -} - -type ProviderManagerResult = Awaited>; - -export async function waitForAcceptanceDispatches(input: { - runManager: () => Promise; - expectedAssignmentIds: string[]; - expectedDispatchCount: number; - attempts?: number; - intervalMs?: number; -}) { - const dispatches: Array> = []; - let manager: ProviderManagerResult | null = null; - const attempts = input.attempts ?? 20; - for (let attempt = 0; attempt < attempts; attempt += 1) { - manager = await input.runManager(); - for (const dispatch of Array.isArray(manager.dispatches) ? manager.dispatches : []) { - const record = dispatch as Record; - const duplicate = record.status === 'ready' && dispatches.some((entry) => - entry.status === 'ready' && entry.assignmentId === record.assignmentId); - if (!duplicate) dispatches.push(record); - } - const selected = dispatches.filter((entry) => entry.status === 'ready' - && (!input.expectedAssignmentIds.length || input.expectedAssignmentIds.includes(String(entry.assignmentId ?? '')))); - if (selected.length >= input.expectedDispatchCount) return { ...manager, dispatches }; - if (attempt + 1 < attempts) await new Promise((resolveDelay) => setTimeout(resolveDelay, input.intervalMs ?? 500)); - } - return { ...manager, dispatches } as ProviderManagerResult; -} - -export async function runWithAcceptanceAvailabilityHeartbeat(input: { - operation: () => Promise; - heartbeat: () => Promise; - intervalMs?: number; -}) { - let stopped = false; - let timer: ReturnType | null = null; - const sleeper: { wake?: () => void } = {}; - let heartbeatError: unknown; - const heartbeatLoop = (async () => { - while (!stopped) { - await new Promise((resolveDelay) => { - sleeper.wake = resolveDelay; - timer = setTimeout(resolveDelay, input.intervalMs ?? 30_000); - }); - timer = null; - delete sleeper.wake; - if (stopped) break; - try { - await input.heartbeat(); - } catch (error) { - heartbeatError = error; - break; - } - } - })(); - let operationError: unknown; - try { - const result = await input.operation(); - return result; - } catch (error) { - operationError = error; - throw error; - } finally { - stopped = true; - if (timer) clearTimeout(timer); - sleeper.wake?.(); - await heartbeatLoop; - if (heartbeatError) { - if (operationError) throw new AggregateError([operationError, heartbeatError], 'Acceptance execution and availability heartbeat both failed.'); - throw heartbeatError; - } - } -} - -export async function recoverOrphanedAcceptanceClaims( - store: ProviderLocalCapacityStore, - activeConnectionIds: string[], -) { - const active = new Set(activeConnectionIds); - const runKey = (connectionId: string) => connectionId.match(/(?:acceptance|competition)-(\d{14})/u)?.[1] ?? null; - const activeRunKeys = new Set(activeConnectionIds.map(runKey).filter((value): value is string => Boolean(value))); - const snapshot = await store.snapshot(); - const orphaned = snapshot.claims.filter((claim) => - (claim.connectionId.startsWith('acceptance-') || claim.connectionId.startsWith('competition-')) - && !active.has(claim.connectionId) - && (!runKey(claim.connectionId) || !activeRunKeys.has(runKey(claim.connectionId)!))); - for (const claim of orphaned) await store.finalize(claim.id, 'acceptance-orphan-recovered'); - return orphaned.map((claim) => claim.id); -} - -export async function executeLiveCapacityAcceptance(input: LiveCapacityAcceptanceInput) { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-capacity-acceptance-')); - const dataDir = String(input.env.TREESEED_PROVIDER_HOST_DATA_DIR ?? resolve(input.cwd, '.treeseed/local-capacity-providers/agent-standalone')); - const connectionId = `acceptance-${input.runId}`; - const competingConnectionId = `competition-${input.runId}`; - const identityPath = resolve(root, 'identity.json'); - const credentialPath = resolve(root, 'membership.credential'); - const competingCredentialPath = resolve(root, 'competing-membership.credential'); - const manifestPath = resolve(root, 'treeseed.capacity-provider.yaml'); - await writeFile(identityPath, `${JSON.stringify(input.privateJwk)}\n`, { mode: 0o600 }); - await writeFile(credentialPath, `${input.membershipCredential}\n`, { mode: 0o600 }); - if (input.competingConnection) await writeFile(competingCredentialPath, `${input.competingConnection.membershipCredential}\n`, { mode: 0o600 }); - const capabilities = [...new Set(input.capabilities ?? ['planning', 'agent_mode_run', 'repo_read', 'usage_report'])]; - const maxConcurrentRunners = input.maxConcurrentRunners ?? 1; - if (!Number.isInteger(maxConcurrentRunners) || maxConcurrentRunners < 1 || maxConcurrentRunners > 2) { - throw new Error('Live acceptance concurrency must be one or two runners.'); - } - const expectedAssignmentIds = input.assignmentIds?.length ? [...new Set(input.assignmentIds)] : input.assignmentId ? [input.assignmentId] : []; - const expectedDispatchCount = expectedAssignmentIds.length || input.expectedAssignmentCount || 1; - if (!Number.isInteger(expectedDispatchCount) || expectedDispatchCount < 1) throw new Error('Live acceptance expected assignment count must be a positive whole number.'); - if (expectedDispatchCount > maxConcurrentRunners) throw new Error('Live acceptance assignments exceed the bounded runner concurrency.'); - const executionProvider = await resolveLiveAcceptanceExecutionProvider(input); - const connections = [{ - id: connectionId, - marketUrl: input.apiUrl, - marketAudience: input.apiUrl, - teamId: input.teamId, - providerId: input.providerId, - membershipId: input.membershipId, - membershipCredentialRef: `file://${credentialPath}`, - membershipCredentialId: input.credentialId, - offer: { weight: 1, maxConcurrentRunners, capabilities }, - }]; - if (input.competingConnection) connections.push({ - id: competingConnectionId, - marketUrl: input.apiUrl, - marketAudience: input.apiUrl, - teamId: input.competingConnection.teamId, - providerId: input.competingConnection.providerId, - membershipId: input.competingConnection.membershipId, - membershipCredentialRef: `file://${competingCredentialPath}`, - membershipCredentialId: input.competingConnection.credentialId, - offer: { weight: 1, maxConcurrentRunners: 1, capabilities }, - }); - await writeFile(manifestPath, stringifyYaml({ - schemaVersion: 2, - providerClass: 'agent', - ownership: { type: 'external' }, - configuration: { generation: `acceptance-${input.runId}` }, - supplyCeilings: { maxConcurrentAssignments: maxConcurrentRunners }, - identity: { privateKeyRef: `file://${identityPath}`, displayName: `Treeseed live Codex acceptance ${input.runId}` }, - executionProviders: [executionProvider], - connections, - }), { mode: 0o600 }); - const config = resolveProviderConfig({ env: { - ...process.env, - ...input.env, - HOME: root, - TREESEED_PROVIDER_DATA_DIR: dataDir, - TREESEED_CAPACITY_PROVIDER_MANIFEST: manifestPath, - TREESEED_PROVIDER_ENVIRONMENT: 'local', - TREESEED_PROVIDER_WORKSPACE_ROOT: input.cwd, - TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS: String(maxConcurrentRunners), - } }); - const localState = new ProviderLocalCapacityStore(dataDir); - await recoverOrphanedAcceptanceClaims(localState, [connectionId, ...(input.competingConnection ? [competingConnectionId] : [])]); - const sessionStateKey = `${connectionId}|${input.teamId}|${input.providerId}`; - const competingSessionStateKey = input.competingConnection - ? `${competingConnectionId}|${input.competingConnection.teamId}|${input.competingConnection.providerId}` - : null; - try { - await localState.saveSession(sessionStateKey, { id: input.providerSessionId, sequence: input.providerSessionSequence }); - if (input.competingConnection && competingSessionStateKey) { - await localState.saveSession(competingSessionStateKey, { - id: input.competingConnection.providerSessionId, - sequence: input.competingConnection.providerSessionSequence, - }); - } - const manager = await waitForAcceptanceDispatches({ - runManager: () => runMultiTeamProviderManager(config).catch((error) => { - throw new Error(`Live acceptance provider manager failed: ${error instanceof Error ? error.message : String(error)}`); - }), - expectedAssignmentIds, - expectedDispatchCount, - }); - const dispatches = Array.isArray(manager.dispatches) ? manager.dispatches : []; - const readyDispatches = dispatches.filter((entry) => entry.status === 'ready'); - if (input.competingConnection) { - const successfulConnections = Array.isArray(manager.connections) - ? manager.connections.filter((entry) => entry && typeof entry === 'object' && (entry as Record).ok === true) - : []; - const scheduler = manager.scheduler && typeof manager.scheduler === 'object' - ? manager.scheduler as { maxConcurrentSlots?: number; connections?: unknown[] } - : {}; - const snapshot = await localState.snapshot(); - if (successfulConnections.length !== 2 || scheduler.connections?.length !== 2) { - throw new Error(`Provider manager did not reconcile two independently runnable connections: ${JSON.stringify(manager.connections)}`); - } - if (scheduler.maxConcurrentSlots !== 1 || readyDispatches.length !== 1 || snapshot.claims.length !== 1) { - throw new Error(`Provider-global final-slot enforcement failed: ${JSON.stringify({ scheduler, readyDispatches, claims: snapshot.claims })}`); - } - } - const selectedDispatches = dispatches.filter((entry) => entry.status === 'ready' - && (!expectedAssignmentIds.length || expectedAssignmentIds.includes(String(entry.assignmentId ?? '')))); - if (selectedDispatches.length !== expectedDispatchCount) { - throw new Error(`Provider manager did not create the expected durable dispatch: ${JSON.stringify({ - dispatches: dispatches.map((entry) => ({ assignmentId: entry.assignmentId, connectionId: entry.connectionId, status: entry.status, error: entry.error ?? null })), - connections: manager.connections, - })}`); - } - const currentAccessToken = await localState.token(connectionId); - if (!currentAccessToken || Date.parse(currentAccessToken.expiresAt) <= Date.now()) { - throw new Error('Provider coordinator did not persist a current membership access token before live runner dispatch.'); - } - const availabilityConnections = [{ - connectionId, sessionStateKey, teamId: input.teamId, providerId: input.providerId, - membershipId: input.membershipId, marketUrl: input.apiUrl, maxConcurrentRunners, - }, ...(input.competingConnection && competingSessionStateKey ? [{ - connectionId: competingConnectionId, sessionStateKey: competingSessionStateKey, - teamId: input.competingConnection.teamId, providerId: input.competingConnection.providerId, - membershipId: input.competingConnection.membershipId, marketUrl: input.apiUrl, maxConcurrentRunners: 1, - }] : [])]; - const runner = await runWithAcceptanceAvailabilityHeartbeat({ - operation: () => runMultiTeamProviderRunners(config).catch((error) => { - throw new Error(`Live acceptance provider runner failed: ${error instanceof Error ? error.message : String(error)}`); - }), - heartbeat: async () => { - const localCapacity = await localState.snapshot(); - await Promise.all(availabilityConnections.map(async (connection) => { - const [session, token] = await Promise.all([ - localState.session(connection.sessionStateKey), - localState.token(connection.connectionId), - ]); - if (!session || !token || Date.parse(token.expiresAt) <= Date.now()) { - throw new Error(`Acceptance availability heartbeat lost current authority for ${connection.connectionId}.`); - } - const refreshed = await new ProviderProtocolClient({ - marketUrl: connection.marketUrl, accessToken: token.accessToken, - }).refreshAvailabilitySession(session.id, { - expectedSequence: session.sequence, ttlSeconds: 90, environment: 'local', status: 'open', capabilities, - nativeLimits: { availableAgentSeconds: 3_600, maxConcurrentRunners: connection.maxConcurrentRunners }, - runnerPressure: { - activeRunners: localCapacity.claims.filter((claim) => claim.connectionId === connection.connectionId).length, - maxConcurrentRunners: connection.maxConcurrentRunners, - }, - metadata: { liveAcceptance: true, runId: input.runId, heartbeatOnly: true }, - executionProviders: [executionProvider], - }); - await localState.saveSession(connection.sessionStateKey, { - id: session.id, sequence: Number(refreshed.payload.sequence), - }); - })); - }, - }); - if (runner.dispatched !== expectedDispatchCount) throw new Error(`Provider runner dispatched ${runner.dispatched} assignments instead of ${expectedDispatchCount}.`); - const failed = runner.results?.find((entry) => entry && typeof entry === 'object' && (entry as Record).ok === false); - if (failed) throw new Error(`Provider runner failed live acceptance: ${JSON.stringify(failed)}`); - const completed = runner.results?.filter((entry) => { - const result = entry && typeof entry === 'object' ? (entry as Record).result : null; - const envelope = result && typeof result === 'object' ? result as Record : {}; - const payload = envelope.payload && typeof envelope.payload === 'object' ? envelope.payload as Record : envelope; - return payload.status === 'completed'; - }).length ?? 0; - if (completed !== expectedDispatchCount) throw new Error(`Provider runner completed ${completed} assignments instead of ${expectedDispatchCount}: ${JSON.stringify(runner.results ?? [])}`); - const session = await localState.session(sessionStateKey); - const assignmentIds = selectedDispatches.map((entry) => String(entry.assignmentId)); - return { - assignmentId: assignmentIds[0]!, - assignmentIds, - providerSessionSequence: session?.sequence ?? input.providerSessionSequence, - ...(input.competingConnection ? { - finalSlot: { twoRunnableConnections: true, providerGlobalLimit: 1, readyDispatches: readyDispatches.length, localClaimsAtCapacity: 1 }, - } : {}), - }; - } finally { - await localState.removeSession(sessionStateKey); - if (competingSessionStateKey) await localState.removeSession(competingSessionStateKey); - await localState.removeToken(connectionId); - await localState.removeConnection(connectionId); - if (input.competingConnection) { - await localState.removeToken(competingConnectionId); - await localState.removeConnection(competingConnectionId); - } - await rm(root, { recursive: true, force: true }); - } -} diff --git a/src/provider-governance.ts b/src/provider-governance.ts index 1366a9cb..7a73bdba 100644 --- a/src/provider-governance.ts +++ b/src/provider-governance.ts @@ -12,10 +12,10 @@ export { loadProviderManifest, writeProviderManifest, providerSecretPath, - providerConnectionMarketUrl, - providerConnectionMarketAudience, - providerMarketProfileEnvironmentName, - providerMarketProfileAudienceEnvironmentName, + providerConnectionControlPlaneUrl, + providerConnectionControlPlaneAudience, + providerServerProfileEnvironmentName, + providerServerProfileAudienceEnvironmentName, resolveProviderSecret, removeProviderSecret, stageProviderSecret, diff --git a/src/provider/accounts/identity.ts b/src/provider/accounts/identity.ts index 571f86b2..f7f70384 100644 --- a/src/provider/accounts/identity.ts +++ b/src/provider/accounts/identity.ts @@ -1,10 +1,45 @@ -import { - capacityProviderPublicIdentity, - generateCapacityProviderIdentity, - type CapacityProviderPrivateJwk, -} from '@treeseed/sdk/capacity-provider'; +import { createHash, createPrivateKey, generateKeyPairSync, randomUUID, sign } from 'node:crypto'; +import type { CapacityProviderProofPayload, CapacityProviderPublicJwk, CapacityProviderSignedProof } from '@treeseed/sdk/capacity-provider/contracts'; import { resolveProviderSecret, writeProviderSecret, type ProviderSecretResolver } from '../configuration/manifest.ts'; +export interface CapacityProviderPrivateJwk extends CapacityProviderPublicJwk { d: string } + +function canonical(value: unknown): unknown { + if (Array.isArray(value)) return value.map(canonical); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries(Object.entries(value as Record) + .filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => [key, canonical(entry)])); +} + +const sha256 = (value: string) => createHash('sha256').update(value).digest('base64url'); +const canonicalJson = (value: unknown) => JSON.stringify(canonical(value)); + +export function generateCapacityProviderIdentity(): CapacityProviderPrivateJwk { + return generateKeyPairSync('ed25519').privateKey.export({ format: 'jwk' }) as CapacityProviderPrivateJwk; +} + +export function capacityProviderPublicIdentity(privateJwk: CapacityProviderPrivateJwk): CapacityProviderPublicJwk { + if (privateJwk.kty !== 'OKP' || privateJwk.crv !== 'Ed25519' || !privateJwk.x || !privateJwk.d) throw new Error('Capacity provider identity must be an Ed25519 private JWK.'); + return { kty: 'OKP', crv: 'Ed25519', x: privateJwk.x, alg: 'EdDSA' }; +} + +export async function signCapacityProviderProof(input: { privateJwk: CapacityProviderPrivateJwk; publicJwk: CapacityProviderPublicJwk; + method: string; path: string; audience: string; body: unknown; jti?: string; identityVersion?: number; now?: Date }): Promise { + const now = input.now ?? new Date(); + const fingerprint = `sha256:${sha256(canonicalJson({ crv: input.publicJwk.crv, kty: input.publicJwk.kty, x: input.publicJwk.x }))}`; + const payload: CapacityProviderProofPayload = { schemaVersion: 1, algorithm: 'Ed25519', providerFingerprint: fingerprint, + identityVersion: input.identityVersion ?? 1, method: input.method.toUpperCase(), path: input.path, + bodySha256: sha256(canonicalJson(input.body)), audience: input.audience.replace(/\/$/u, ''), + issuedAt: new Date(now.getTime() - 1_000).toISOString(), expiresAt: new Date(now.getTime() + 59_000).toISOString(), + jti: input.jti ?? randomUUID() }; + const protectedValue = Buffer.from(JSON.stringify({ alg: 'EdDSA', typ: 'JOSE' })).toString('base64url'); + const payloadValue = Buffer.from(JSON.stringify(payload)).toString('base64url'); + const signature = sign(null, Buffer.from(`${protectedValue}.${payloadValue}`), + createPrivateKey({ key: input.privateJwk as unknown as import('node:crypto').JsonWebKey, format: 'jwk' })).toString('base64url'); + return { protected: protectedValue, payload: payloadValue, signature }; +} + export async function initializeCapacityProviderIdentity(input: { ref: string; baseDirectory: string; dataDirectory?: string }) { const privateJwk = generateCapacityProviderIdentity(); await writeProviderSecret(input.ref, JSON.stringify(privateJwk), input.baseDirectory, input.dataDirectory); diff --git a/src/provider/capacity/accounting/usage-reporter.ts b/src/provider/capacity/accounting/usage-reporter.ts deleted file mode 100644 index 2055b564..00000000 --- a/src/provider/capacity/accounting/usage-reporter.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { AgentKernelModeExecutionResult } from '@treeseed/sdk/agent-capacity'; -import type { ExecutionUsageActual } from '@treeseed/sdk/types/agents'; - -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export function settlementUsageActual(modeResult: AgentKernelModeExecutionResult) { - const usage: ExecutionUsageActual[] = Array.isArray(modeResult.artifactManifest?.usage) - ? modeResult.artifactManifest.usage - : []; - const sum = (field: string) => usage.reduce((total, entry) => { - const value = Number(record(entry.metadata)[field]); - return Number.isFinite(value) && value >= 0 ? total + value : total; - }, 0); - const providerUnits = usage.reduce((total, entry) => { - const value = Number(entry.amount); - return Number.isFinite(value) && value >= 0 ? total + value : total; - }, 0); - return { - providerUnits: usage.length > 0 ? providerUnits : null, - usageActual: { - nativeUsage: { executionUsage: usage }, - inputTokens: sum('inputTokens') || null, - outputTokens: sum('outputTokens') || null, - cachedInputTokens: sum('cachedInputTokens') || null, - wallMinutes: sum('wallMinutes') || null, - filesOpened: sum('filesOpened') || null, - filesChanged: sum('filesChanged') || null, - diffLinesAdded: sum('diffLinesAdded') || null, - diffLinesRemoved: sum('diffLinesRemoved') || null, - testRuns: sum('testRuns') || null, - retryCount: sum('retryCount') || null, - }, - }; -} - -export function usageDimension(kind: string, index: number) { - const normalized = kind.toLowerCase() - .replace(/[^a-z0-9._-]+/gu, '-') - .replace(/^-+|-+$/gu, '') - .slice(0, 54) || 'provider-usage'; - return `${normalized}.${index}`; -} diff --git a/src/provider/capacity/assignments/assignment-agent-spec-loader.ts b/src/provider/capacity/assignments/assignment-agent-spec-loader.ts deleted file mode 100644 index cc4fc27d..00000000 --- a/src/provider/capacity/assignments/assignment-agent-spec-loader.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { AgentTreeDxAdapter } from '../../../agents/runtime/runtime-types.ts'; -import { parse as parseYaml } from 'yaml'; -import { record, stringValue } from '../../configuration/value-utils.ts'; -import { deliverProviderModeRunTelemetry, type AssignmentModeRunRecorder } from '../../reporting/mode-run-telemetry.ts'; - -function parseFrontmatterDocument(source: string): { frontmatter: Record; body: string } { - const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/u); - if (!match) return { frontmatter: {}, body: source }; - return { frontmatter: record(parseYaml(match[1] ?? '')), body: match[2] ?? '' }; -} - -function fileText(file: Record, keys: string[]) { - for (const key of keys) if (typeof file[key] === 'string') return file[key] as string; - return ''; -} - -function fileSlug(filePath: string, agentsRoot: string) { - const normalizedPath = filePath.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, ''); - const normalizedRoot = agentsRoot.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, ''); - const relative = normalizedPath.startsWith(`${normalizedRoot}/`) ? normalizedPath.slice(normalizedRoot.length + 1) : normalizedPath; - return relative.replace(/\.(mdx|md)$/iu, '').replace(/^\/+|\/+$/gu, ''); -} - -export async function loadAssignmentRawAgentSpecs(input: { treeDx: AgentTreeDxAdapter | null; assignmentId: string; agentSlug: string; agentContentPath?: string | null; workspaceId: string | null; contentRoot: string | null; client: AssignmentModeRunRecorder; mode: string; capacityEnvelope: Record; decisionPayload: Record; runnerId: string; options?: { enabled?: boolean } }) { - if (!input.treeDx) return null; - const startedAt = Date.now(); - const agentsRoot = `${(input.contentRoot ?? 'src/content').replace(/\/+$/u, '')}/agents`; - const baseMetadata = { source: 'provider_runner_agent_spec_loader', loaderVersion: 'exact-agent-spec-sequential-v2', assignmentId: input.assignmentId, runnerId: input.runnerId, agentSlug: input.agentSlug, agentsRoot }; - let rawSpecs: Array<{ id: string; body: string; frontmatter: Record }>; - let metadata: Record; - try { - const attemptedPaths: string[] = []; - const missingPaths: string[] = []; - const readResponses: Record[] = []; - const candidatePaths = input.agentContentPath - ? [input.agentContentPath] - : [`${agentsRoot}/${input.agentSlug}.mdx`, `${agentsRoot}/${input.agentSlug}.md`]; - for (const candidatePath of candidatePaths) { - attemptedPaths.push(candidatePath); - try { - if (input.workspaceId) { - const response = record(await input.treeDx.readWorkspaceFile({ workspaceId: input.workspaceId, path: candidatePath })); - const file = record(response.file ?? record(response.payload).file ?? response); - readResponses.push(Object.keys(file).length ? { files: [file] } : response); - } else { - readResponses.push(record(await input.treeDx.readRepositoryFiles({ repoId: '', paths: [candidatePath], body: { includeBody: true, includeFrontmatter: true } }))); - } - break; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (message.includes('(404)') || message.includes(', 404')) { missingPaths.push(candidatePath); continue; } - throw error; - } - } - const files = [ - ...readResponses.flatMap((response) => Array.isArray(response.files) ? response.files : []), - ...readResponses.flatMap((response) => Array.isArray(response.results) ? response.results : []), - ...readResponses.flatMap((response) => Array.isArray(record(response.payload).files) ? record(response.payload).files : []), - ...readResponses.flatMap((response) => Array.isArray(record(response.payload).results) ? record(response.payload).results : []), - ].map(record).filter((file) => Object.keys(file).length > 0); - const pathsToRead = files.map((file) => stringValue(file.path, file.relativePath, file.name)).filter((path): path is string => Boolean(path)); - rawSpecs = files.map((file) => { - const path = stringValue(file.path, file.relativePath, file.name) ?? ''; - const rawDocument = fileText(file, ['content', 'source', 'text']); - const parsed = rawDocument ? parseFrontmatterDocument(rawDocument) : { frontmatter: record(file.frontmatter), body: fileText(file, ['body']) }; - return { id: fileSlug(path, agentsRoot), body: parsed.body, frontmatter: parsed.frontmatter }; - }).filter((entry) => input.options?.enabled === true ? entry.frontmatter.enabled !== false : true); - metadata = { ...baseMetadata, attemptedPathCount: attemptedPaths.length, attemptedPaths, missingPaths, readPathCount: pathsToRead.length, readPaths: pathsToRead, fileCount: files.length, specCount: rawSpecs.length, durationMs: Date.now() - startedAt }; - } catch (error) { - const metadata = { ...baseMetadata, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - startedAt }; - await deliverProviderModeRunTelemetry({ - recorder: input.client, - assignmentId: input.assignmentId, - eventId: 'agent-spec-load:failed', - request: { mode: input.mode, status: 'running', selectedInput: input.decisionPayload, capacityEnvelope: input.capacityEnvelope, outputs: { status: 'agent_spec_load_failed', summary: metadata.error, metadata }, metadata }, - }); - throw error; - } - await deliverProviderModeRunTelemetry({ - recorder: input.client, - assignmentId: input.assignmentId, - eventId: 'agent-spec-load:succeeded', - request: { mode: input.mode, status: 'running', selectedInput: input.decisionPayload, capacityEnvelope: input.capacityEnvelope, outputs: { status: 'agent_specs_loaded', summary: `Loaded ${rawSpecs.length} agent spec(s) through the assignment TreeDX proxy.`, metadata }, metadata }, - }); - return rawSpecs; -} diff --git a/src/provider/capacity/assignments/assignment-result-reporter.ts b/src/provider/capacity/assignments/assignment-result-reporter.ts deleted file mode 100644 index 340e522d..00000000 --- a/src/provider/capacity/assignments/assignment-result-reporter.ts +++ /dev/null @@ -1,299 +0,0 @@ -import type { - AgentKernelModeExecutionResult, - ProviderAssignment, -} from '@treeseed/sdk/agent-capacity'; -import type { ProviderAssignmentClient } from '../../coordination/lease-client.ts'; -import { settlementUsageActual, usageDimension } from '../accounting/usage-reporter.ts'; -import { positiveNumberValue, record, stringValue } from '../../configuration/value-utils.ts'; -import { reportProviderRuntimeEvent } from '../../reporting/runtime-event-reporter.ts'; -import { buildAssignmentPerformanceSummary } from './performance-summary.ts'; -import { workdayAssignmentAuthorityProjection } from './workday-assignment-authority.ts'; - -function retryableCompletionError(error: unknown) { - const status = Number(record(error).status ?? 0); - return status === 0 || status >= 500; -} - -async function publishResultSignals(client: ProviderAssignmentClient, assignmentId: string, result: AgentKernelModeExecutionResult) { - const manifest = result.artifactManifest; - if (!manifest?.signals.length) return; - if (!client.publishAssignmentSignal) throw new Error('Provider client does not implement assignment signal publication.'); - for (const [index, signal] of manifest.signals.entries()) { - const metadata = record(signal.metadata); - const requestedSubjectId = stringValue(metadata.subjectId, metadata.proposalId); - const content = manifest.contentReferences.find((entry) => requestedSubjectId && entry.subjectId === requestedSubjectId) - ?? manifest.contentReferences.find((entry) => entry.subjectId || entry.commitSha); - const control = manifest.controlPlaneReferences?.find((entry) => requestedSubjectId && (entry.id === requestedSubjectId || stringValue(entry.metadata?.proposalId) === requestedSubjectId)) - ?? manifest.controlPlaneReferences?.[0]; - const subjectId = stringValue(metadata.subjectId, metadata.proposalId, content?.subjectId, control?.id); - const subjectKind = stringValue(metadata.subjectKind, content?.model, control?.kind); - if (!subjectId || !subjectKind) throw new Error(`Signal ${signal.code} is missing its durable subject identity.`); - const controlPlaneRef = control ? `${control.kind}:${control.id}` : null; - const requestedReceiptId = stringValue(metadata.mutationReceiptId, metadata.artifactReceiptId); - const requestedPhase = stringValue(metadata.mutationPhase); - const receipt = manifest.mutationReceipts?.find((candidate) => requestedReceiptId && candidate.id === requestedReceiptId) - ?? manifest.mutationReceipts?.find((candidate) => candidate.effectiveRef === stringValue(metadata.commitSha, content?.commitSha) - && (!content?.contentPath || candidate.changedPaths.includes(content.contentPath))); - if (!controlPlaneRef && !receipt) throw new Error(`Artifact-trigger signal ${signal.code} is not bound to an artifact mutation receipt.`); - if (receipt && requestedPhase && receipt.phase !== requestedPhase) throw new Error(`Signal ${signal.code} requested ${requestedPhase} evidence but receipt ${receipt.id} is ${receipt.phase}.`); - const commitSha = receipt?.effectiveRef ?? null; - await client.publishAssignmentSignal(assignmentId, { - contractId: signal.code, - subjectKind, - subjectId, - subjectGroupIds: Array.isArray(metadata.subjectGroupIds) ? metadata.subjectGroupIds.map(String).filter(Boolean) : content?.groupIds ?? [], - causationId: `${manifest.modeRunId}:${index}`, - correlationId: stringValue(metadata.correlationId, subjectId), - idempotencyKey: stringValue(metadata.idempotencyKey), - changeSummary: signal.message ?? manifest.summary, - payload: record(metadata.payload ?? metadata), - evidence: { - commitSha, - immutableRef: receipt?.effectiveRef ?? null, - digest: receipt?.after.digest ?? null, - changedPaths: receipt?.changedPaths ?? [], - controlPlaneRef, - mutationReceipt: receipt ?? null, - }, - metadata: { severity: signal.severity, agentId: manifest.agentId, activityType: manifest.activityType, - mutationReceiptId: receipt?.id ?? null, mutationPhase: receipt?.phase ?? null }, - }); - } -} - -export async function completeProviderAssignmentWithRetry( - client: ProviderAssignmentClient, - assignmentId: string, - request: Record, - wait: (durationMs: number) => Promise = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs)), -) { - for (let attempt = 1; attempt <= 3; attempt += 1) { - try { - return await client.completeAssignment(assignmentId, request); - } catch (error) { - if (attempt === 3 || !retryableCompletionError(error)) throw error; - await wait(250 * attempt); - } - } - throw new Error('Assignment completion retry loop ended unexpectedly.'); -} - -export async function closeTerminalWorkspaceWithRetry( - closeWorkspace: (() => Promise) | null | undefined, - wait: (durationMs: number) => Promise = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs)), -): Promise<'completed' | 'not_required'> { - if (!closeWorkspace) return 'not_required'; - for (let attempt = 1; attempt <= 3; attempt += 1) { - try { - await closeWorkspace(); - return 'completed'; - } catch (error) { - if (attempt === 3 || !retryableCompletionError(error)) throw error; - await wait(250 * attempt); - } - } - throw new Error('TreeDX workspace cleanup retry loop ended unexpectedly.'); -} - -export async function reportProviderAssignmentResult(input: { - client: ProviderAssignmentClient; - assignmentId: string; - assignment: ProviderAssignment; - modeResult: AgentKernelModeExecutionResult; - capacityEnvelope: Record; - leaseToken: string | null; - runnerId: string; - projectId: string; - agentSlug: string; - fallbackOutput: Record | null; - closeWorkspace?: (() => Promise) | null; -}) { - const { - client, - assignmentId, - modeResult, - leaseToken, - runnerId, - projectId, - agentSlug, - fallbackOutput, - closeWorkspace, - } = input; - let typedAssignment=input.assignment; - let capacityEnvelope=input.capacityEnvelope; - if(client.assignment){ - const observed=record(await client.assignment(assignmentId)); const latest=record(observed.payload); - if(stringValue(latest.id)===assignmentId){ typedAssignment={ ...typedAssignment,...latest } as typeof typedAssignment; capacityEnvelope=record(latest.capacityEnvelope); } - } - const workdayAuthority = workdayAssignmentAuthorityProjection(typedAssignment as unknown as Record); - const numberValue = positiveNumberValue; - const modeMetadata = record(modeResult.metadata); - const outputMetadata = record(record(modeResult.outputs).metadata); - const usageActual = record(modeMetadata.usageActual ?? outputMetadata.usageActual); - const settlementUsage = settlementUsageActual(modeResult); - const budgetTime=record(record(capacityEnvelope.budget).time); const completedMs=Date.now(); - const startedAt = stringValue(budgetTime.preparationStartedAt,typedAssignment.claimedAt, typedAssignment.assignedAt); - const executionStartedMs=Date.parse(stringValue(budgetTime.executionStartedAt)??''); - const executionEndedMs=Date.parse(stringValue(budgetTime.executionDeadlineAt)??''); - const closeoutStartedMs=Date.parse(stringValue(budgetTime.closeoutStartedAt)??''); - const elapsedSeconds = Math.max(0, Math.ceil(startedAt ? (completedMs - Date.parse(startedAt)) / 1_000 : 0)); - const providerWallSeconds = numberValue(settlementUsage.usageActual.wallMinutes) != null - ? Math.ceil(Number(settlementUsage.usageActual.wallMinutes) * 60) - : null; - const phasedTiming=Number.isFinite(executionStartedMs); - const preparationSeconds=Math.max(0,phasedTiming&&startedAt?Math.ceil((executionStartedMs-Date.parse(startedAt))/1_000):0); - const executionSeconds=Math.max(0,phasedTiming?Math.ceil((Math.min(completedMs,Number.isFinite(executionEndedMs)?executionEndedMs:completedMs)-executionStartedMs)/1_000):(providerWallSeconds??elapsedSeconds)); - const closeoutSeconds=Math.max(0,Number.isFinite(closeoutStartedMs)?Math.ceil((completedMs-closeoutStartedMs)/1_000):0); - const activeSeconds = Math.max(0, Math.min(providerWallSeconds ?? executionSeconds,executionSeconds)); - if (modeResult.status === 'completed') { - const completion = record(outputMetadata.completion); - const disposition = ['completed', 'completed_early', 'deadline_exhausted', 'budget_exhausted', 'blocked', 'cancelled', 'failed'].includes(String(completion.disposition)) - ? String(completion.disposition) as import('@treeseed/sdk/agent-capacity').AssignmentTerminalDisposition - : 'completed'; - const performance = buildAssignmentPerformanceSummary({ assignment: typedAssignment, disposition, - reason: stringValue(completion.completionReason, modeResult.summary) ?? 'Assignment completed.', completion, - capacityEnvelope, usage: { ...settlementUsage.usageActual, ...usageActual }, activeSeconds, elapsedSeconds,preparationSeconds,executionSeconds,closeoutSeconds, - agentAssessment: record(outputMetadata.performanceAssessment), artifactManifest: modeResult.artifactManifest }); - if (!client.preflightAssignmentCompletion) throw new Error('Provider client does not implement semantic completion preflight.'); - const semanticPreflight=record(await client.preflightAssignmentCompletion(assignmentId,{ - leaseToken,runnerId,artifactManifest:modeResult.artifactManifest??null, - idempotencyKey:`assignment:${assignmentId}:semantic-completion-preflight`, - }));const semanticCompletionPreflightReceiptDigest=stringValue(record(semanticPreflight.payload).receiptDigest); - if(!semanticCompletionPreflightReceiptDigest)throw new Error('Semantic completion preflight omitted its exact receipt digest.'); - const workspaceCleanup: Record = { - status: await closeTerminalWorkspaceWithRetry(closeWorkspace), - }; - await publishResultSignals(client, assignmentId, modeResult); - if (!client.reportAssignmentUsage) throw new Error('Provider client does not implement dimensional capacity usage reporting.'); - const executionUsage = Array.isArray(modeResult.artifactManifest?.usage) ? modeResult.artifactManifest.usage : []; - for (const [index, entry] of executionUsage.entries()) { - const dimension = usageDimension(String(entry.kind ?? 'provider-usage'), index); - await client.reportAssignmentUsage(assignmentId, { - usageDimension: dimension, - accountingMode: 'informational', - activeSeconds: 0, - elapsedSeconds: 0, - providerUnits: Number.isFinite(Number(entry.amount)) ? Number(entry.amount) : null, - modeRunId: stringValue(modeMetadata.modeRunId, outputMetadata.modeRunId), - usageActual: { - nativeUsage: { executionUsage: [entry] }, - taskSignature: `${typedAssignment.projectAgentClassId}:${modeResult.mode}`, - executionProviderId: typedAssignment.executionProviderId ?? null, - }, - metadata: { runnerId: runnerId, mode: modeResult.mode, source: '@treeseed/agent/provider-runner' }, - }, `assignment:${assignmentId}:usage:${dimension}`); - } - if (!client.settleAssignment) throw new Error('Provider client does not implement exactly-once capacity settlement.'); - await client.settleAssignment(assignmentId, { - activeSeconds, - elapsedSeconds, - providerUnits: numberValue(usageActual.providerUnits, settlementUsage.providerUnits), - usd: numberValue(usageActual.usd, usageActual.actualUsd), - modeRunId: stringValue(modeMetadata.modeRunId, outputMetadata.modeRunId), - usageActual: { - ...settlementUsage.usageActual, - taskSignature: `${typedAssignment.projectAgentClassId}:${modeResult.mode}`, - executionProviderId: typedAssignment.executionProviderId ?? null, - }, - metadata: { runnerId: runnerId, mode: modeResult.mode, agentSlug, activityType: stringValue(record(typedAssignment.metadata).activityType), completion: Object.keys(completion).length ? completion : { disposition: 'completed' }, capacityBudget: record(capacityEnvelope.budget), phaseDurations:{preparationSeconds,executionSeconds,closeoutSeconds,custodySeconds:elapsedSeconds}, workdayAuthority, source: '@treeseed/agent/provider-runner' }, - }, `assignment:${assignmentId}:terminal-settlement`); - await reportProviderRuntimeEvent({ client, assignmentId, event: { - id: `assignment-performance:${assignmentId.toLowerCase()}`, eventType: 'provider.assignment.performance.finalized', status: 'completed', component: 'provider-runner', - message: 'Final assignment performance summary recorded.', createdAt: performance.systemAssessment.measuredAt, - context: { performance }, - } }); - return completeProviderAssignmentWithRetry(client, assignmentId, { - leaseToken: leaseToken, - runnerId: runnerId, - output: { - projectId, - agentSlug, - mode: modeResult.mode, - status: modeResult.status, - summary: modeResult.summary, - metadata: { - ...(modeResult.metadata ?? {}), - ...record(record(modeResult.outputs).metadata), - workspaceCleanup, - workdayAuthority, - }, - traceRefs: modeResult.traceRefs ?? {}, - artifactManifest: modeResult.artifactManifest ?? null, - }, - summary: { - summary: modeResult.summary, - mode: modeResult.mode, - }, - ...(Object.keys(completion).length ? { completion } : {}), - metadata:{semanticCompletionPreflightReceiptDigest}, - performance, - }); - } - if (modeResult.status === 'returned' && client.returnAssignment) { - if (!client.settleAssignment) throw new Error('Provider client does not implement exactly-once capacity settlement for returned work.'); - await client.settleAssignment(assignmentId, { - activeSeconds, - elapsedSeconds, - providerUnits: numberValue(usageActual.providerUnits, settlementUsage.providerUnits), - usd: numberValue(usageActual.usd, usageActual.actualUsd), - modeRunId: stringValue(modeMetadata.modeRunId, outputMetadata.modeRunId), - usageActual: { - ...settlementUsage.usageActual, - taskSignature: `${typedAssignment.projectAgentClassId}:${modeResult.mode}`, - executionProviderId: typedAssignment.executionProviderId ?? null, - }, - metadata: { runnerId, mode: modeResult.mode, agentSlug, activityType: stringValue(record(typedAssignment.metadata).activityType), completion: { disposition: 'suspended' }, capacityBudget: record(capacityEnvelope.budget), phaseDurations:{preparationSeconds,executionSeconds,closeoutSeconds,custodySeconds:elapsedSeconds}, workdayAuthority, source: '@treeseed/agent/provider-runner' }, - }, `assignment:${assignmentId}:terminal-settlement`); - return client.returnAssignment(assignmentId, { - leaseToken: leaseToken, - runnerId: runnerId, - reason: modeResult.fallback?.reason ?? modeResult.summary, - code: modeResult.fallback?.code ?? 'provider_assignment_returned', - retryable: modeResult.fallback?.retryable ?? true, - output: { - ...(modeResult.outputs ?? {}), - artifactManifest: modeResult.artifactManifest ?? null, - }, - artifactManifest: modeResult.artifactManifest ?? null, - fallbackOutput: fallbackOutput ?? undefined, - }); - } - const failureReason = modeResult.fallback?.reason ?? modeResult.summary; - const failureCode = modeResult.fallback?.code ?? 'provider_assignment_failed'; - const failureDisposition = /deadline|timeout/iu.test(`${failureCode} ${failureReason}`) ? 'deadline_exhausted' - : /budget|quota|token|cost|capacity/iu.test(`${failureCode} ${failureReason}`) ? 'budget_exhausted' - : /cancel|abort/iu.test(`${failureCode} ${failureReason}`) ? 'cancelled' - : /block|authority|credential|dependency|evidence/iu.test(`${failureCode} ${failureReason}`) ? 'blocked' : 'failed'; - const performance = buildAssignmentPerformanceSummary({ assignment: typedAssignment, disposition: failureDisposition, - reason: failureReason, capacityEnvelope, usage: { ...settlementUsage.usageActual, ...usageActual }, activeSeconds, elapsedSeconds,preparationSeconds,executionSeconds,closeoutSeconds, - artifactManifest: modeResult.artifactManifest }); - await closeTerminalWorkspaceWithRetry(closeWorkspace); - await reportProviderRuntimeEvent({ client, assignmentId, event: { - id: `assignment-performance:${assignmentId.toLowerCase()}`, eventType: 'provider.assignment.performance.finalized', status: 'failed', component: 'provider-runner', - message: 'Final assignment performance summary recorded.', createdAt: performance.systemAssessment.measuredAt, - context: { performance }, - } }); - return client.failAssignment(assignmentId, { - leaseToken: leaseToken, - runnerId: runnerId, - code: failureCode, - message: failureReason, - retryable: modeResult.fallback?.retryable ?? false, - activeSeconds, - elapsedSeconds, - providerUnits: numberValue(usageActual.providerUnits), - actualUsd: numberValue(usageActual.usd, usageActual.actualUsd), - modeRunId: stringValue(modeMetadata.modeRunId, outputMetadata.modeRunId), - output: { - ...(modeResult.outputs ?? {}), - artifactManifest: modeResult.artifactManifest ?? null, - metadata: { - ...record(record(modeResult.outputs).metadata), - ...(modeResult.metadata ?? {}), - workdayAuthority, - }, - }, - fallbackOutput: fallbackOutput ?? undefined, - performance, - }); -} diff --git a/src/provider/capacity/assignments/assignment-tool-policy.ts b/src/provider/capacity/assignments/assignment-tool-policy.ts deleted file mode 100644 index b84920b7..00000000 --- a/src/provider/capacity/assignments/assignment-tool-policy.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { AgentActivityType, AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; -import { selectAgentActivityProfile } from '../../../agents/kernel/telemetry/activity-profile-resolver.ts'; -import { record, stringValue } from '../../configuration/value-utils.ts'; - -export function resolveAssignmentAgentToolPolicy( - agent: AgentRuntimeSpec | undefined, - mode: 'planning' | 'acting', - activityType?: string | null, -) { - if (!agent) return null; - return selectAgentActivityProfile(agent, mode, activityType as AgentActivityType | null | undefined); -} - -export function assignmentWorkspaceAccessMode(assignment: Record) { - const handles = record(assignment.capabilityHandles); - const workspaceContext = record(assignment.workspaceContext); - const mode = stringValue(handles.workspaceAccessMode, workspaceContext.workspaceAccessMode); - return ['context_only', 'workspace_write', 'brokered_workspace', 'full_workspace_no_credentials', 'trusted_direct'] - .includes(mode ?? '') ? mode : 'context_only'; -} - -export function assignmentWorkflowOperationHandles(assignment: Record) { - const operations = record(assignment.capabilityHandles).workflowOperations; - return Array.isArray(operations) ? operations as Record[] : []; -} diff --git a/src/provider/capacity/assignments/kernel-assignment.ts b/src/provider/capacity/assignments/kernel-assignment.ts deleted file mode 100644 index 329f8204..00000000 --- a/src/provider/capacity/assignments/kernel-assignment.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { assertProviderAssignment, type ProviderAssignment, type ProviderAssignmentCapabilityHandles } from '@treeseed/sdk/agent-capacity'; -import { record, stringValue } from '../../configuration/value-utils.ts'; -import { isApiIssuedWorkdayAssignment } from './workday-assignment-authority.ts'; - -export function buildKernelProviderAssignment(input: { - assignment: Record; - assignmentId: string; - membershipId: string; - stateVersion: number; - decisionInput: Record; - decisionPayload: Record; - capacityEnvelope: Record; - projectId: string; - agentSlug: string; - workspaceMode: string | null; - treedxProxyHandle: Record; - capabilityHandles: ProviderAssignmentCapabilityHandles; -}): ProviderAssignment { - const { assignment, assignmentId, membershipId, stateVersion, decisionInput, decisionPayload, capacityEnvelope, projectId, agentSlug, workspaceMode, treedxProxyHandle, capabilityHandles } = input; - if (isApiIssuedWorkdayAssignment(assignment)) { - return assertProviderAssignment({ - ...assignment, - treedxProxyHandle, - capabilityHandles, - workspaceContext: { - ...record(assignment.workspaceContext), - workspaceAccessMode: workspaceMode, - treedxProxyHandle, - capabilityHandles, - }, - }); - } - return assertProviderAssignment({ - ...assignment, - id: assignmentId, - membershipId, - stateVersion, - teamId: stringValue(assignment.teamId, decisionInput.teamId, capacityEnvelope.teamId) ?? '', - projectId, - capacityProviderId: stringValue(assignment.capacityProviderId, capacityEnvelope.capacityProviderId) ?? '', - projectAgentClassId: stringValue(assignment.projectAgentClassId, decisionInput.projectAgentClassId, capacityEnvelope.projectAgentClassId) ?? agentSlug, - mode: stringValue(assignment.mode, decisionInput.mode, capacityEnvelope.mode) ?? 'planning', - status: stringValue(assignment.status) ?? 'leased', - leaseState: stringValue(assignment.leaseState) ?? 'leased', - agentId: agentSlug, - handlerId: stringValue(assignment.handlerId, decisionInput.handlerId), - treedxProxyHandle, - capacityEnvelope: { - ...capacityEnvelope, - teamId: stringValue(capacityEnvelope.teamId, assignment.teamId, decisionInput.teamId) ?? '', - projectId, - mode: stringValue(capacityEnvelope.mode, assignment.mode, decisionInput.mode) ?? 'planning', - projectAgentClassId: stringValue(capacityEnvelope.projectAgentClassId, assignment.projectAgentClassId, decisionInput.projectAgentClassId) ?? agentSlug, - capacityProviderId: stringValue(capacityEnvelope.capacityProviderId, assignment.capacityProviderId) ?? '', - }, - decisionInput: { - ...decisionInput, - teamId: stringValue(decisionInput.teamId, assignment.teamId, capacityEnvelope.teamId) ?? '', - projectId, - projectAgentClassId: stringValue(decisionInput.projectAgentClassId, assignment.projectAgentClassId, capacityEnvelope.projectAgentClassId) ?? agentSlug, - mode: stringValue(decisionInput.mode, assignment.mode, capacityEnvelope.mode) ?? 'planning', - agentId: agentSlug, - input: { ...decisionPayload, projectId, agentSlug, assignmentId }, - }, - capabilityHandles, - workspaceContext: { - ...record(assignment.workspaceContext), - workspaceAccessMode: workspaceMode, - treedxProxyHandle, - capabilityHandles, - }, - }); -} diff --git a/src/provider/capacity/assignments/performance-summary.ts b/src/provider/capacity/assignments/performance-summary.ts deleted file mode 100644 index 8e8aed96..00000000 --- a/src/provider/capacity/assignments/performance-summary.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - ASSIGNMENT_PERFORMANCE_SCHEMA, CAPACITY_BUDGET_SCHEMA, emptyCapacityBudget, - type AgentArtifactManifest,type AssignmentPerformanceSummary, type AssignmentTerminalDisposition, type CapacityBudgetV2, type ProviderAssignment, -} from '@treeseed/sdk/agent-capacity'; -import { record, stringValue } from '../../configuration/value-utils.ts'; - -const number = (value: unknown) => Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : 0; -const strings = (value: unknown) => Array.isArray(value) ? value.map(String).filter(Boolean) : []; - -export function buildAssignmentPerformanceSummary(input: { - assignment: ProviderAssignment; - disposition: AssignmentTerminalDisposition; - reason: string; - completion?: Record; - capacityEnvelope: Record; - usage: Record; - activeSeconds: number; - elapsedSeconds: number; - preparationSeconds?: number; - executionSeconds?: number; - closeoutSeconds?: number; - agentAssessment?: Record | null; - artifactManifest?: AgentArtifactManifest | null; -}): AssignmentPerformanceSummary { - const assignment = input.assignment; - const metadata = record(assignment.metadata); - const completion = record(input.completion); - const budgetCandidate = record(input.capacityEnvelope.budget); - const budget = budgetCandidate.schemaVersion === CAPACITY_BUDGET_SCHEMA - ? budgetCandidate as unknown as CapacityBudgetV2 - : emptyCapacityBudget(stringValue(budgetCandidate.deadline) ?? new Date().toISOString(), number(record(budgetCandidate.time).requestedSeconds)); - const tokens = record(input.usage.tokens ?? input.usage); - const cost = record(input.usage.cost); - const revisions = record(metadata.configurationRevisions); - const recordedOutcomes = Array.isArray(completion.downstreamOutcomes) - ? completion.downstreamOutcomes as AssignmentPerformanceSummary['downstreamOutcomes'] : []; - const receiptOutcomes: AssignmentPerformanceSummary['downstreamOutcomes'] = (input.artifactManifest?.mutationReceipts ?? []).map((receipt) => ({ - kind: 'integration', status: receipt.phase, evidenceRefs: [receipt.before.ref,receipt.after.ref], - artifactMutationReceiptIds: [receipt.id], occurredAt: receipt.createdAt, - })); - const verificationOutcomes: AssignmentPerformanceSummary['downstreamOutcomes'] = (input.artifactManifest?.verification ?? []).map((verification) => ({ - kind: 'validation', status: verification.status, evidenceRefs: verification.evidenceRefs ?? [], artifactMutationReceiptIds: [], - occurredAt: input.artifactManifest?.createdAt ?? new Date().toISOString(), - })); - return { - schemaVersion: ASSIGNMENT_PERFORMANCE_SCHEMA, - assignmentId: assignment.id, workdayId: assignment.workDayId ?? null, teamId: assignment.teamId, projectId: assignment.projectId, - agentId: assignment.agentId ?? null, agentClassId: assignment.projectAgentClassId, - activityProfile: stringValue(metadata.activityProfile, metadata.activityType, assignment.mode) ?? assignment.mode, - handlerId: assignment.handlerId ?? null, capacityProviderId: assignment.capacityProviderId, - executionProviderId: assignment.executionProviderId ?? null, model: stringValue(metadata.model), - groupIds: strings(metadata.groupIds), - configuration: { - planningGraphRevision: stringValue(revisions.planningGraphRevision, metadata.planningGraphRevision), - agentDefinitionRevision: stringValue(revisions.agentDefinitionRevision, metadata.agentDefinitionRevision), - agentClassRevision: stringValue(revisions.agentClassRevision, metadata.agentClassRevision), - activityProfileRevision: stringValue(revisions.activityProfileRevision, metadata.activityProfileRevision), - handlerRevision: stringValue(revisions.handlerRevision, metadata.handlerRevision), - groupMembershipRevision: stringValue(revisions.groupMembershipRevision, metadata.groupMembershipRevision), - executionProviderConfigurationRevision: stringValue(revisions.executionProviderConfigurationRevision, metadata.executionProviderConfigurationRevision), - }, - taskSignature: stringValue(input.usage.taskSignature) ?? `${assignment.projectAgentClassId}:${assignment.mode}`, - disposition: input.disposition, reason: input.reason, - acceptanceChecks: Array.isArray(completion.acceptanceChecks) ? completion.acceptanceChecks as AssignmentPerformanceSummary['acceptanceChecks'] : [], - completedScope: strings(completion.completedScope), remainingScope: strings(completion.remainingScope), - artifactRefs: strings(completion.durableArtifactRefs), budget, - actual: { preparationSeconds:number(input.preparationSeconds),executionSeconds:number(input.executionSeconds??input.activeSeconds),closeoutSeconds:number(input.closeoutSeconds),custodySeconds:input.elapsedSeconds, - activeSeconds: input.activeSeconds, elapsedSeconds: input.elapsedSeconds, - inputTokens: number(tokens.inputTokens ?? input.usage.promptTokens), cachedInputTokens: number(tokens.cachedInputTokens), - reasoningTokens: number(tokens.reasoningTokens), outputTokens: number(tokens.outputTokens ?? input.usage.completionTokens), - costAmount: cost.amount == null && input.usage.usd == null ? null : number(cost.amount ?? input.usage.usd), - costCurrency: stringValue(cost.currency) ?? (input.usage.usd == null ? null : 'USD'), - native: Array.isArray(input.usage.native) ? input.usage.native as AssignmentPerformanceSummary['actual']['native'] : [], attempts: number(assignment.attemptCount) + 1 }, - noUsefulScopedWorkRemaining: completion.noUsefulScopedWorkRemaining === true, - agentAssessment: input.agentAssessment ?? null, - systemAssessment: { generatedBy: 'agent-runner', measuredAt: new Date().toISOString(), enforcementConfidence: budget.enforcementConfidence }, - downstreamOutcomes: [...recordedOutcomes,...receiptOutcomes,...verificationOutcomes], - }; -} diff --git a/src/provider/capacity/assignments/workday-assignment-authority.ts b/src/provider/capacity/assignments/workday-assignment-authority.ts deleted file mode 100644 index 419d3512..00000000 --- a/src/provider/capacity/assignments/workday-assignment-authority.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { ProviderAssignment } from '@treeseed/sdk/agent-capacity'; -import { record, stringValue } from '../../configuration/value-utils.ts'; - -export interface WorkdayAssignmentAuthorityDiagnostic { - code: string; - path: string; - message: string; -} - -function required( - diagnostics: WorkdayAssignmentAuthorityDiagnostic[], - value: unknown, - path: string, - code: string, -) { - const resolved = stringValue(value); - if (!resolved) diagnostics.push({ code, path, message: `API-issued workday assignment requires ${path}.` }); - return resolved; -} - -function conflict( - diagnostics: WorkdayAssignmentAuthorityDiagnostic[], - left: string | null, - right: string | null, - path: string, -) { - if (left && right && left !== right) diagnostics.push({ - code: 'workday_assignment_authority_conflict', - path, - message: `API-issued workday assignment contains conflicting ${path} identities.`, - }); -} - -export function validateWorkdayAssignmentAuthority(value: Record): WorkdayAssignmentAuthorityDiagnostic[] { - if (stringValue(value.synthesizedFrom) !== 'workday_demand') return []; - const diagnostics: WorkdayAssignmentAuthorityDiagnostic[] = []; - const metadata = record(value.metadata); - const envelope = record(value.capacityEnvelope); - const decisionInput = record(value.decisionInput); - const decisionMetadata = record(decisionInput.metadata); - const mode = required(diagnostics, value.mode, 'mode', 'workday_assignment_mode_missing'); - const teamId = required(diagnostics, value.teamId, 'teamId', 'workday_assignment_team_missing'); - const projectId = required(diagnostics, value.projectId, 'projectId', 'workday_assignment_project_missing'); - const providerId = required(diagnostics, value.capacityProviderId, 'capacityProviderId', 'workday_assignment_provider_missing'); - const classId = required(diagnostics, value.projectAgentClassId, 'projectAgentClassId', 'workday_assignment_class_missing'); - const workdayId = required(diagnostics, value.workDayId, 'workDayId', 'workday_assignment_workday_missing'); - required(diagnostics, metadata.workdayRunId, 'metadata.workdayRunId', 'workday_assignment_run_missing'); - const demandId = required(diagnostics, metadata.demandId, 'metadata.demandId', 'workday_assignment_demand_missing'); - required(diagnostics, value.allocationSetId, 'allocationSetId', 'workday_assignment_allocation_missing'); - required(diagnostics, value.reservationId, 'reservationId', 'workday_assignment_reservation_missing'); - required(diagnostics, value.membershipId, 'membershipId', 'workday_assignment_membership_missing'); - if (!Number.isInteger(Number(value.stateVersion)) || Number(value.stateVersion) < 1) diagnostics.push({ - code: 'workday_assignment_state_version_invalid', - path: 'stateVersion', - message: 'API-issued workday assignment requires a positive stateVersion.', - }); - conflict(diagnostics, teamId, stringValue(envelope.teamId), 'teamId'); - conflict(diagnostics, projectId, stringValue(envelope.projectId), 'projectId'); - conflict(diagnostics, providerId, stringValue(envelope.capacityProviderId), 'capacityProviderId'); - conflict(diagnostics, classId, stringValue(envelope.projectAgentClassId), 'projectAgentClassId'); - conflict(diagnostics, workdayId, stringValue(envelope.workDayId), 'workDayId'); - conflict(diagnostics, mode, stringValue(envelope.mode), 'mode'); - conflict(diagnostics, demandId, stringValue(decisionMetadata.demandId), 'demandId'); - if (mode === 'acting') { - required(diagnostics, value.decisionId, 'decisionId', 'workday_acting_decision_missing'); - required(diagnostics, decisionMetadata.capacityPlanId, 'decisionInput.metadata.capacityPlanId', 'workday_acting_capacity_plan_missing'); - } - return diagnostics; -} - -export function isApiIssuedWorkdayAssignment(value: Record): value is Record & ProviderAssignment { - return stringValue(value.synthesizedFrom) === 'workday_demand' - && validateWorkdayAssignmentAuthority(value).length === 0; -} - -export function workdayAssignmentAuthorityProjection(value: Record) { - if (stringValue(value.synthesizedFrom) !== 'workday_demand') return null; - const metadata = record(value.metadata); - const decisionMetadata = record(record(value.decisionInput).metadata); - return { - workdayRunId: stringValue(metadata.workdayRunId), - workdayId: stringValue(value.workDayId), - demandId: stringValue(metadata.demandId), - allocationSetId: stringValue(value.allocationSetId), - projectAgentClassId: stringValue(value.projectAgentClassId), - reservationId: stringValue(value.reservationId), - capacityProviderId: stringValue(value.capacityProviderId), - decisionId: stringValue(value.decisionId), - capacityPlanId: stringValue(decisionMetadata.capacityPlanId), - stateVersion: Number(value.stateVersion), - }; -} diff --git a/src/provider/capacity/providers/execution-provider-selection.ts b/src/provider/capacity/providers/execution-provider-selection.ts deleted file mode 100644 index a4c5c48a..00000000 --- a/src/provider/capacity/providers/execution-provider-selection.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { createExecutionProviderAdapter } from '../../../agents/adapters/execution/execution.ts'; -import type { DiscordExecutionProviderConfig } from '../../../agents/adapters/integrations/execution-discord.ts'; -import type { GitHubIssuesExecutionProviderConfig } from '../../../agents/adapters/repositories/execution-github-issues.ts'; -import type { JiraExecutionProviderConfig } from '../../../agents/adapters/integrations/execution-jira.ts'; -import type { WorkflowExecutionProviderAdapterOptions } from '../../../agents/adapters/operations/execution-workflow.ts'; -import type { CapacityProviderManifestV2 } from '@treeseed/sdk/capacity-provider'; -import { record, stringValue } from '../../configuration/value-utils.ts'; - -type ManifestExecutionProvider = CapacityProviderManifestV2['executionProviders'][number]; - -export class AssignmentExecutionProviderSelectionError extends Error { - constructor(readonly code: 'assignment_execution_provider_id_required' | 'assignment_execution_provider_not_configured', message: string) { - super(message); - this.name = 'AssignmentExecutionProviderSelectionError'; - } -} - -function requestedExecutionProviderId(assignment: Record) { - const metadata = record(assignment.metadata); - return stringValue( - assignment.executionProviderId, - metadata.executionProviderId, - record(assignment.capacityEnvelope).executionProviderId, - ); -} - -export function resolveAssignmentExecutionProvider(input: { - assignment: Record; - executionProviders: ManifestExecutionProvider[]; - defaultExecutionProviderId?: string; -}): ManifestExecutionProvider { - const requestedId = requestedExecutionProviderId(input.assignment); - if (requestedId) { - const selected = input.executionProviders.find((provider) => provider.id === requestedId); - if (selected) return selected; - throw new AssignmentExecutionProviderSelectionError( - 'assignment_execution_provider_not_configured', - `Assignment execution provider "${requestedId}" is not configured in Provider Manifest V2.`, - ); - } - if (input.defaultExecutionProviderId) { - const selected = input.executionProviders.find((provider) => provider.id === input.defaultExecutionProviderId); - if (selected) return selected; - throw new AssignmentExecutionProviderSelectionError( - 'assignment_execution_provider_not_configured', - `Default execution provider "${input.defaultExecutionProviderId}" is not configured in Provider Manifest V2.`, - ); - } - if (input.executionProviders.length === 1) return input.executionProviders[0]!; - throw new AssignmentExecutionProviderSelectionError( - 'assignment_execution_provider_id_required', - input.executionProviders.length === 0 - ? 'Assignment execution provider cannot be resolved because Provider Manifest V2 declares no execution providers.' - : 'Assignment execution provider id is required when Provider Manifest V2 declares more than one execution provider.', - ); -} - -export function createAssignmentExecutionProviderAdapter(input: { - selection: string | null; - executionProvider?: ManifestExecutionProvider | null; - repoRoot: string; - jira?: JiraExecutionProviderConfig | null; - githubIssues?: GitHubIssuesExecutionProviderConfig | null; - discord?: DiscordExecutionProviderConfig | null; - workflow?: WorkflowExecutionProviderAdapterOptions | null; - accessToken: string; - apiBaseUrl: string; - researchSourcePolicy?: import('@treeseed/sdk/agent-capacity').ResearchSourcePolicy; - onCodexEvent?: (event: Record) => void | Promise; -}) { - const runtime = buildExecutionProviderRuntimeConfiguration({ - executionProvider: input.executionProvider, - accessToken: input.accessToken, - apiBaseUrl: input.apiBaseUrl, - }); - const configured = input.executionProvider; - return createExecutionProviderAdapter(input.selection ?? 'codex', { - repoRoot: input.repoRoot, - env: runtime.env, - jira: input.jira, - githubIssues: input.githubIssues, - discord: input.discord, - workflow: input.workflow, - codex: { env: runtime.env, onEvent: input.onCodexEvent }, - opencode: { env: runtime.env, providerId: runtime.openCodeProviderId, model: runtime.model }, - copilot: runtime.copilotProvider, - copilotModel: runtime.model, - researchSourcePolicy: input.researchSourcePolicy, - }); -} - -export function buildExecutionProviderRuntimeConfiguration(input: { - executionProvider?: ManifestExecutionProvider | null; - accessToken: string; - apiBaseUrl: string; - env?: NodeJS.ProcessEnv; -}) { - const configured = input.executionProvider; - const profile = stringValue(configured?.profile) ?? undefined; - const baseUrl = stringValue(configured?.model?.baseUrl) ?? undefined; - const model = stringValue(configured?.model?.model) ?? undefined; - const env: NodeJS.ProcessEnv = { - ...(input.env ?? process.env), - TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN: input.accessToken, - TREESEED_API_BASE_URL: input.apiBaseUrl, - TREESEED_EXECUTION_PROVIDER_ID: configured?.id ?? '', - TREESEED_EXECUTION_PROVIDER_PROFILE: profile, - ...(model ? { TREESEED_EXECUTION_MODEL: model } : {}), - ...(baseUrl ? { TREESEED_EXECUTION_BASE_URL: baseUrl } : {}), - }; - if (configured?.adapter === 'codex' && profile === 'key') { - env.TREESEED_CODEX_API_KEY = env.TREESEED_OPENAI_API_KEY ?? env.TREESEED_CODEX_API_KEY; - env.OPENAI_API_KEY = env.TREESEED_CODEX_API_KEY; - } - if (configured?.adapter === 'codex' && profile === 'treeseed') { - env.TREESEED_CODEX_API_KEY = env.TREESEED_AI_GATEWAY_TOKEN; - env.OPENAI_API_KEY = env.TREESEED_AI_GATEWAY_TOKEN; - env.TREESEED_CODEX_MODEL_PROVIDER = 'treeseed'; - env.TREESEED_CODEX_BASE_URL = baseUrl; - env.TREESEED_CODEX_DEFAULT_MODEL = model; - } - return { - env, - model, - openCodeProviderId: profile === 'treeseed' ? 'treeseed' : undefined, - copilotProvider: profile === 'treeseed' - ? { type: 'openai' as const, baseUrl, apiKey: env.TREESEED_AI_GATEWAY_TOKEN, wireApi: configured?.protocol === 'chat-completions' ? 'completions' as const : 'responses' as const } - : undefined, - }; -} diff --git a/src/provider/commerce/catalog/assignment-tool-catalog.ts b/src/provider/commerce/catalog/assignment-tool-catalog.ts deleted file mode 100644 index 804435d9..00000000 --- a/src/provider/commerce/catalog/assignment-tool-catalog.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { AgentContentAccessPolicy } from '@treeseed/sdk/types/agents'; -import type { ExecutionProviderToolDescriptor, TreeDxProxyExecutionToolDescriptor } from '../../../agents/runtime/runtime-types.ts'; -import { CONTENT_READ_ACTIONS, CONTENT_WRITE_ACTIONS, type ContentAction, type ContentModel } from '@treeseed/sdk/content-operations'; -import { findAgentToolDefinition, type AgentToolRequirement } from '@treeseed/sdk/agent-tools'; -import { evaluateTreeDxProxyHandleAccess } from '@treeseed/sdk/agent-capacity'; -import { stringValue } from '../../configuration/value-utils.ts'; -import { normalizeTreeDxProxyHandle } from '../../treedx/repositories/treedx-handle.ts'; - -function createAssignmentTreeDxToolDescriptor(input: { projectId: string; assignmentId: string; treedxProxyHandle: Record; workspaceMode?: string | null; allowWrite: boolean; allowCommit: boolean }): TreeDxProxyExecutionToolDescriptor | null { - const handleId = stringValue(input.treedxProxyHandle.id); - const scopedHandle = normalizeTreeDxProxyHandle(input.treedxProxyHandle); - if (!handleId || !scopedHandle || !evaluateTreeDxProxyHandleAccess(scopedHandle, { projectId: input.projectId, assignmentId: input.assignmentId }).ok) return null; - const repositoryId = stringValue(input.treedxProxyHandle.repositoryId); - const workspaceId = stringValue(input.treedxProxyHandle.workspaceId); - const rawAllowedOperations = Array.isArray(input.treedxProxyHandle.allowedOperations) ? input.treedxProxyHandle.allowedOperations.map(String).filter(Boolean) : []; - const writable = input.workspaceMode !== 'context_only' && input.allowWrite; - const allowedOperations = rawAllowedOperations.length > 0 - ? rawAllowedOperations.filter((operation) => { - if (operation === 'files:write' || operation === 'workspace:write') return writable; - if (operation === 'git:commit') return input.allowCommit; - return true; - }) - : [...(writable ? ['files:read', 'files:search', 'files:write'] : ['files:read', 'files:search']), ...(input.allowCommit ? ['git:commit'] : [])]; - const allowedPaths = Array.isArray(input.treedxProxyHandle.allowedPaths) ? input.treedxProxyHandle.allowedPaths.map(String).filter(Boolean) : []; - const allowedReadPaths = Array.isArray(input.treedxProxyHandle.allowedReadPaths) ? input.treedxProxyHandle.allowedReadPaths.map(String).filter(Boolean) : allowedPaths; - const allowedWritePaths = writable - ? Array.isArray(input.treedxProxyHandle.allowedWritePaths) ? input.treedxProxyHandle.allowedWritePaths.map(String).filter(Boolean) : allowedPaths - : []; - const project = encodeURIComponent(input.projectId); - const repo = repositoryId ? encodeURIComponent(repositoryId) : ':repoId'; - const workspace = workspaceId ? encodeURIComponent(workspaceId) : ':workspaceId'; - return { - kind: 'agent_tool', id: 'treedx.proxy', name: 'TreeDX assignment proxy', description: 'Assignment-scoped TreeDX content and workspace operations proxied through the TreeSeed API.', inputSchema: { type: 'object', additionalProperties: true }, executionTarget: 'treedx_proxy', mutability: writable || input.allowCommit ? 'content_write' : 'read', - projectId: input.projectId, assignmentId: input.assignmentId, handleId, repositoryId, workspaceId, allowedOperations, allowedPaths, allowedReadPaths, allowedWritePaths, - routes: { buildContext: `POST /v1/dx/projects/${project}/repos/${repo}/context/build`, readRepositoryFiles: `POST /v1/dx/projects/${project}/repos/${repo}/files/read`, searchWorkspace: `POST /v1/dx/projects/${project}/workspaces/${workspace}/search`, readWorkspaceFile: `GET /v1/dx/projects/${project}/workspaces/${workspace}/files?path=:path`, applyWorkspaceChangeset: `POST /v1/dx/projects/${project}/workspaces/${workspace}/changesets`, commitWorkspace: `POST /v1/dx/projects/${project}/workspaces/${workspace}/commit` }, - metadata: { baseCommitSha: stringValue(input.treedxProxyHandle.baseCommitSha), baseRef: stringValue(input.treedxProxyHandle.baseRef), requiresHeaders: ['Authorization: Bearer ', 'x-treeseed-assignment-id', 'x-treeseed-treedx-proxy-handle-id'] }, - }; -} - -export interface AssignmentToolCatalog { - requested: string[]; - exposed: string[]; - omitted: Array<{ id: string; missing: AgentToolRequirement[] }>; - descriptors: ExecutionProviderToolDescriptor[]; -} - -function listAllows(value: string | undefined, allowed: string[] | undefined) { - if (!value) return true; - if (!allowed || allowed.length === 0) return false; - return allowed.includes('*') || allowed.includes(value); -} - -function contentToolAllowed(policy: AgentContentAccessPolicy | undefined, action: ContentAction, model: ContentModel | undefined) { - if (!policy) return false; - const scope = CONTENT_READ_ACTIONS.has(action) ? policy.read : CONTENT_WRITE_ACTIONS.has(action) ? policy.write : action === 'commit' ? policy.write : undefined; - if (action === 'commit') return policy.commit?.allowed === true; - if (!scope) return false; - if (scope.actions?.length && !scope.actions.includes(action)) return false; - return listAllows(model, scope.models); -} - -function summarizeContentAccess(policy: AgentContentAccessPolicy | undefined, agentTools: string[]) { - if (!policy) return null; - const requestedActions = new Set(agentTools.flatMap((toolId) => { - const definition = findAgentToolDefinition(toolId); - if (definition?.content?.action) return [definition.content.action]; - return toolId === 'treedx.commit_workspace' ? ['commit' as const] : []; - })); - const readActions = (policy.read?.actions ?? []).filter((action) => requestedActions.has(action)); - const writeActions = (policy.write?.actions ?? []).filter((action) => requestedActions.has(action)); - const commitAllowed = policy.commit?.allowed === true && requestedActions.has('commit'); - return { - readModels: readActions.length ? policy.read?.models ?? [] : [], readActions, readPaths: policy.read?.paths ?? [], - writeModels: writeActions.length ? policy.write?.models ?? [] : [], writeActions, writePaths: policy.write?.paths ?? [], commitAllowed, - }; -} - -export function createAssignmentToolCatalog(input: { agentTools: string[]; projectId: string; assignmentId: string; agentSlug?: string; agentContentPath?: string | null; environment?: string; treedxProxyHandle: Record; workspaceMode?: string | null; treeDxWorkspaceMode?: 'context_only' | 'read_write' | 'commit'; contentRoot?: string | null; permissionProjection?: AgentContentAccessPolicy; allowedProposalTypes?: string[]; researchNetworkPolicy?: { allowWeb?: boolean; allowedDomains?: string[] }; providerResearchSourcePolicy?: { allowedDomains?: string[] }; worktreeRoot?: string | null; providerManagesWorktree?: boolean; allowedPaths?: string[]; forbiddenPaths?: string[] }): AssignmentToolCatalog { - const agentTools = [...new Set([ - ...input.agentTools, 'treeseed.status', 'treeseed.assignment_plan', 'treeseed.assignment_status_update', - 'treeseed.assignment_summary', 'treeseed.discussion.read', 'treeseed.discussion.follow', 'treeseed.discussion.respond', - 'treeseed.discussion.request_handoff', 'treeseed.discussion.create_artifact', 'treeseed.operation.prepare_handoff', 'treeseed.client_session.request_action', - ])]; - const requestedDefinitions = agentTools.map(findAgentToolDefinition).filter((definition) => Boolean(definition)); - const allowWrite = requestedDefinitions.some((definition) => definition?.mutability === 'content_write' - && (!definition.content || contentToolAllowed(input.permissionProjection, definition.content.action, definition.content.model))); - const allowCommit = agentTools.some((toolId) => toolId === 'treedx.commit_workspace' - || findAgentToolDefinition(toolId)?.content?.action === 'commit') - && input.permissionProjection?.commit?.allowed === true; - const treeDxBase = createAssignmentTreeDxToolDescriptor({ ...input, allowWrite, allowCommit }); - const descriptors: ExecutionProviderToolDescriptor[] = []; - const omitted: Array<{ id: string; missing: AgentToolRequirement[] }> = []; - const writableWorkspace = Boolean(treeDxBase?.allowedOperations.includes('files:write')); - const commitAllowed = input.permissionProjection?.commit?.allowed === true; - const projectResearchDomains = input.researchNetworkPolicy?.allowedDomains ?? []; - const providerResearchDomains = input.providerResearchSourcePolicy?.allowedDomains ?? []; - const effectiveResearchDomains = projectResearchDomains.filter((projectDomain) => providerResearchDomains.some( - (providerDomain) => projectDomain === providerDomain - || projectDomain.endsWith(`.${providerDomain}`) - || providerDomain.endsWith(`.${projectDomain}`), - )); - for (const toolId of agentTools) { - const definition = findAgentToolDefinition(toolId); - if (!definition) continue; - const missing: AgentToolRequirement[] = []; - if (definition.requirements.includes('treedx_proxy_handle') && !treeDxBase) missing.push('treedx_proxy_handle'); - if (definition.requirements.includes('assignment_worktree') && !input.worktreeRoot && input.providerManagesWorktree !== true) missing.push('assignment_worktree'); - if (definition.requirements.includes('treedx_writable_workspace') && !writableWorkspace) missing.push('treedx_writable_workspace'); - if (definition.requirements.includes('content_access') && definition.content && !contentToolAllowed(input.permissionProjection, definition.content.action, definition.content.model)) missing.push('content_access'); - if (definition.requirements.includes('content_commit') && !commitAllowed) missing.push('content_commit'); - if (definition.requirements.includes('research_source_policy') && (input.researchNetworkPolicy?.allowWeb !== true || !effectiveResearchDomains.length)) missing.push('research_source_policy'); - if (missing.length) { omitted.push({ id: definition.id, missing }); continue; } - const base: ExecutionProviderToolDescriptor = { kind: 'agent_tool', id: definition.id, name: definition.title, description: definition.description, inputSchema: definition.inputSchema, outputSchema: definition.outputSchema, executionTarget: definition.executionTarget, mutability: definition.mutability, metadata: { dispatch: definition.dispatch, dispatchPreferredMode: definition.dispatch?.assignmentPreferredMode, telemetryCategory: definition.telemetryCategory, assignmentId: input.assignmentId, projectId: input.projectId, agentSlug: input.agentSlug ?? null, agentContentPath: input.agentContentPath ?? null, environment: input.environment ?? null, contentRoot: input.contentRoot ?? null, worktreeRoot: input.worktreeRoot ?? null, allowedPaths: input.allowedPaths ?? [], forbiddenPaths: input.forbiddenPaths ?? [], allowedProposalTypes: input.allowedProposalTypes ?? [], researchAllowedDomains: definition.telemetryCategory === 'research' ? effectiveResearchDomains : undefined, contentAction: definition.content?.action, contentModel: definition.content?.model, contentPreset: definition.content?.preset, permissionSummary: summarizeContentAccess(input.permissionProjection, agentTools) } }; - const requiresTreeDxProxy = definition.requirements.includes('treedx_proxy_handle') - || definition.executionTarget === 'treedx_proxy' || definition.executionTarget === 'treeseed_content'; - if (!requiresTreeDxProxy) { descriptors.push(base); continue; } - if (!treeDxBase) { omitted.push({ id: definition.id, missing: ['treedx_proxy_handle'] }); continue; } - descriptors.push({ ...treeDxBase, id: definition.id, name: definition.title, description: definition.description, inputSchema: definition.inputSchema, outputSchema: definition.outputSchema, executionTarget: definition.executionTarget, mutability: definition.mutability, metadata: { ...(treeDxBase.metadata ?? {}), ...(base.metadata ?? {}) } }); - } - return { requested: input.agentTools, exposed: descriptors.map((descriptor) => descriptor.id), omitted, descriptors }; -} diff --git a/src/provider/configuration/capabilities.ts b/src/provider/configuration/capabilities.ts index 43fc9059..8e101714 100644 --- a/src/provider/configuration/capabilities.ts +++ b/src/provider/configuration/capabilities.ts @@ -18,11 +18,7 @@ export function discoverProviderCapabilities(config: ProviderHostRuntimeConfig): const loaded = capabilitiesFromFile(config.capabilitiesFile); if (loaded) return loaded; } - const models = [ - 'codex', - ...(config.env?.TREESEED_OPENCODE_SERVER_URL ? ['opencode'] : []), - ...(config.env?.TREESEED_GITHUB_COPILOT_TOKEN ? ['copilot'] : []), - ]; + const models = config.executorModule ? ['external-executor'] : []; return [{ id: 'agent-execution', agents: ['*'], diff --git a/src/provider/configuration/config.ts b/src/provider/configuration/config.ts index 60e89ab4..3b3c1683 100644 --- a/src/provider/configuration/config.ts +++ b/src/provider/configuration/config.ts @@ -1,243 +1,82 @@ -import { - redactCapacityProviderEnv, - type CapacityProviderManifestV2, -} from '@treeseed/sdk/capacity-provider'; -import type { AgentSdkTreeDxOptions } from '@treeseed/sdk/sdk'; -import { mintTreeDxHs256Token } from '@treeseed/sdk/treedx/auth'; import { existsSync, readFileSync } from 'node:fs'; -import { resolveCodexAuthFile } from '../../agents/adapters/accounts/codex-auth.ts'; -import { - resolveJiraExecutionProviderConfig, - type JiraExecutionProviderConfig, -} from '../../agents/adapters/integrations/execution-jira.ts'; -import { - resolveGitHubIssuesExecutionProviderConfig, - type GitHubIssuesExecutionProviderConfig, -} from '../../agents/adapters/repositories/execution-github-issues.ts'; -import { - resolveDiscordExecutionProviderConfig, - type DiscordExecutionProviderConfig, -} from '../../agents/adapters/integrations/execution-discord.ts'; +import type { CapacityProviderManifestV2 } from '@treeseed/sdk/capacity-provider'; export type ProviderRole = 'manager' | 'runner' | 'doctor' | 'healthcheck' | 'plan' | 'version'; const packageVersion = String(JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8')).version); -export interface JiraProviderRuntimeConfig extends JiraExecutionProviderConfig {} -export interface GitHubIssuesProviderRuntimeConfig extends GitHubIssuesExecutionProviderConfig {} -export interface DiscordProviderRuntimeConfig extends DiscordExecutionProviderConfig {} - export interface ProviderHostRuntimeConfig { - dataDir: string; - apiPort: number; - environment: string; - capabilitiesFile: string | null; - budgetFile: string | null; - maxConcurrentWorkdays: number; - maxConcurrentRunners: number; - dailyAgentSecondsLimit: number | null; - monthlyAgentSecondsLimit: number | null; - codexAuthFile: string | null; - codexAuthJsonB64: string | null; - codexAuthOverwrite: boolean; - jira: JiraProviderRuntimeConfig | null; - githubIssues: GitHubIssuesProviderRuntimeConfig | null; - discord: DiscordProviderRuntimeConfig | null; - treeDx: AgentSdkTreeDxOptions | null; - env: Record; - redactedEnv: Record; - manifestPath?: string | null; + dataDir: string; + environment: string; + maxConcurrentRunners: number; + maxConcurrentWorkdays: number; + capabilitiesFile: string | null; + budgetFile: string | null; + dailyAgentSecondsLimit: number | null; + monthlyAgentSecondsLimit: number | null; + executorModule: string | null; + env: Record; + redactedEnv: Record; + manifestPath: string | null; } export interface ProviderConnectionRuntimeContext extends ProviderHostRuntimeConfig { - connectionId: string; - marketUrl: string; - marketAudience: string; - teamId: string; - providerId: string; - membershipId: string; - accessToken: string; - accessTokenProvider?: (minimumValidityMs?: number) => Promise; - defaultExecutionProviderId?: string; - executionProviders?: CapacityProviderManifestV2['executionProviders']; -} - -function envValue(env: NodeJS.ProcessEnv, name: string) { - return env[name]?.trim() || ''; -} - -function intValue(value: string, fallback: number) { - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : fallback; -} - -function optionalIntValue(value: string) { - if (!value) return null; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : null; -} - -function booleanValue(value: string) { - return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase()); -} - -function arrayEnvValue(env: NodeJS.ProcessEnv, name: string, fallback: string[]) { - const raw = envValue(env, name); - if (!raw) return fallback; - return raw.split(',').map((entry) => entry.trim()).filter(Boolean); -} - -function optionalEnvEntries(env: NodeJS.ProcessEnv, names: string[]) { - return Object.fromEntries( - names - .map((name) => [name, envValue(env, name)] as const) - .filter(([, value]) => value), - ); -} - -function mintLocalTreeDxJwt(env: NodeJS.ProcessEnv) { - const secret = envValue(env, 'TREESEED_TREEDX_JWT_HS256_SECRET'); - const issuer = envValue(env, 'TREESEED_TREEDX_JWT_ISSUER') || envValue(env, 'TREEDX_JWT_ISSUER'); - const audience = envValue(env, 'TREESEED_TREEDX_JWT_AUDIENCE') || envValue(env, 'TREEDX_JWT_AUDIENCE'); - if (!secret || !issuer || !audience) return ''; - const actorId = envValue(env, 'TREESEED_TREEDX_PROXY_ACTOR_ID') || envValue(env, 'TREESEED_TREEDX_ACTOR_ID') || 'treeseed-provider'; - const tenantId = envValue(env, 'TREESEED_TREEDX_PROXY_TENANT_ID') || envValue(env, 'TREESEED_TREEDX_TENANT_ID') || 'treeseed-control-plane'; - return mintTreeDxHs256Token({ - secret, issuer, audience, actorId, tenantId, - repoIds: arrayEnvValue(env, 'TREESEED_TREEDX_REPO_IDS', ['*']), - capabilities: arrayEnvValue(env, 'TREESEED_TREEDX_CAPABILITIES', ['*']), - refs: arrayEnvValue(env, 'TREESEED_TREEDX_REFS', ['*']), - paths: arrayEnvValue(env, 'TREESEED_TREEDX_PATHS', ['**']), - ttlSeconds: 3600, - }); + connectionId: string; + controlPlaneUrl: string; + controlPlaneAudience: string; + teamId: string; + providerId: string; + membershipId: string; + accessToken: string; + accessTokenProvider?: (minimumValidityMs?: number) => Promise; + defaultExecutionProviderId?: string; + executionProviders?: CapacityProviderManifestV2['executionProviders']; } -export function resolveProviderTreeDxOptions(env: NodeJS.ProcessEnv = process.env): AgentSdkTreeDxOptions | null { - const baseUrl = envValue(env, 'TREESEED_TREEDX_BASE_URL') || envValue(env, 'TREESEED_TREEDX_URL'); - if (!baseUrl) return null; - const token = envValue(env, 'TREESEED_TREEDX_TOKEN') || mintLocalTreeDxJwt(env); - return { - baseUrl, - ...(token ? { token } : {}), - ...(envValue(env, 'TREESEED_TREEDX_REPO_ID') ? { repoId: envValue(env, 'TREESEED_TREEDX_REPO_ID') } : {}), - ...(envValue(env, 'TREESEED_TREEDX_REF') ? { ref: envValue(env, 'TREESEED_TREEDX_REF') } : {}), - ...(envValue(env, 'TREESEED_TREEDX_WORKSPACE_ID') ? { workspaceId: envValue(env, 'TREESEED_TREEDX_WORKSPACE_ID') } : {}), - }; +function value(env: NodeJS.ProcessEnv, name: string) { + return env[name]?.trim() || ''; } -function configuredCodexAuthFile(env: NodeJS.ProcessEnv) { - const explicit = envValue(env, 'TREESEED_CODEX_AUTH_FILE') || envValue(env, 'CODEX_AUTH_FILE'); - const resolved = resolveCodexAuthFile(env); - if (explicit) return resolved; - return existsSync(resolved) ? resolved : ''; +function positiveInteger(raw: string, fallback: number) { + const parsed = Number.parseInt(raw, 10); + return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; } -export function resolveProviderEnvironmentInput(env: NodeJS.ProcessEnv = process.env) { - const codexAuthFile = configuredCodexAuthFile(env); - const providerApiPort = envValue(env, 'TREESEED_PROVIDER_API_PORT') || envValue(env, 'PORT') || '3100'; - return { - providerDataDir: envValue(env, 'TREESEED_PROVIDER_DATA_DIR') || '/data', - providerApiPort, - providerEnvironment: envValue(env, 'TREESEED_PROVIDER_ENVIRONMENT') || envValue(env, 'TREESEED_ENVIRONMENT') || 'local', - capabilitiesFile: envValue(env, 'TREESEED_PROVIDER_CAPABILITIES_FILE') || undefined, - budgetFile: envValue(env, 'TREESEED_PROVIDER_BUDGET_FILE') || undefined, - maxConcurrentWorkdays: envValue(env, 'TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS') || undefined, - maxConcurrentRunners: envValue(env, 'TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS') || undefined, - dailyAgentSecondsLimit: envValue(env, 'TREESEED_PROVIDER_DAILY_AGENT_SECONDS_LIMIT') || undefined, - monthlyAgentSecondsLimit: envValue(env, 'TREESEED_PROVIDER_MONTHLY_AGENT_SECONDS_LIMIT') || undefined, - codexAuthFile: codexAuthFile || undefined, - codexAuthJsonB64: envValue(env, 'TREESEED_CODEX_AUTH_JSON_B64') || undefined, - codexAuthOverwrite: envValue(env, 'TREESEED_CODEX_AUTH_OVERWRITE') || undefined, - }; +function optionalInteger(raw: string) { + if (!raw) return null; + const parsed = Number.parseInt(raw, 10); + return Number.isInteger(parsed) && parsed >= 0 ? parsed : null; } -export function resolveProviderConfig(options: { - env?: NodeJS.ProcessEnv; - requireConnection?: boolean; -} = {}): ProviderHostRuntimeConfig { - const env = options.env ?? process.env; - const input = resolveProviderEnvironmentInput(env); - const codexAuthFile = configuredCodexAuthFile(env); - const jira = resolveJiraExecutionProviderConfig(env); - const githubIssues = resolveGitHubIssuesExecutionProviderConfig(env); - const discord = resolveDiscordExecutionProviderConfig(env); - const treeDx = resolveProviderTreeDxOptions(env); - const configuredManifestPath = envValue(env, 'TREESEED_CAPACITY_PROVIDER_MANIFEST'); - const manifestPath = configuredManifestPath || (existsSync('treeseed.capacity-provider.yaml') ? 'treeseed.capacity-provider.yaml' : null); - const missing = [!manifestPath ? 'TREESEED_CAPACITY_PROVIDER_MANIFEST' : null] - .filter((entry): entry is string => Boolean(entry)); - if (options.requireConnection && missing.length > 0) { - throw new Error(`Capacity provider connection is missing: ${missing.join(', ')}.`); - } - const resolvedEnv = { - TREESEED_PROVIDER_DATA_DIR: input.providerDataDir ?? '/data', - TREESEED_PROVIDER_API_PORT: String(input.providerApiPort ?? '3100'), - TREESEED_PROVIDER_ENVIRONMENT: String(input.providerEnvironment ?? 'local'), - }; - resolvedEnv.TREESEED_PROVIDER_API_PORT ??= String(input.providerApiPort ?? '3100'); - Object.assign(resolvedEnv, optionalEnvEntries(env, [ - 'TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST', - 'TREESEED_MIN_FREE_DISK_BYTES', - 'TREESEED_PROVIDER_MIN_FREE_DISK_BYTES', - 'TREESEED_PROVIDER_ASSIGNMENT_DISK_HEADROOM_BYTES', - 'TREESEED_PROVIDER_WORKSPACE_ROOT', - 'TREESEED_PROVIDER_WORKSPACE_ABSOLUTE_CONTAINER', - 'TREESEED_PROVIDER_WORKSPACE_GITDIR_CONTAINER', - 'TREESEED_MARKET_GIT_COMMON_DIR_ABSOLUTE_CONTAINER', - 'TREESEED_MARKET_GIT_COMMON_DIR_ROOT_CONTAINER', - ])); - const redactedEnv = { - ...redactCapacityProviderEnv(resolvedEnv), - ...(jira ? { - TREESEED_JIRA_BASE_URL: jira.baseUrl, - TREESEED_JIRA_EMAIL: jira.email, - TREESEED_JIRA_API_TOKEN: '', - TREESEED_JIRA_PROJECT_KEY: jira.projectKey, - TREESEED_JIRA_ISSUE_TYPE: jira.issueType, - TREESEED_JIRA_DONE_STATUSES: jira.doneStatuses.join(','), - TREESEED_JIRA_BLOCKED_STATUSES: jira.blockedStatuses.join(','), - TREESEED_JIRA_CANCELLED_STATUSES: jira.cancelledStatuses.join(','), - TREESEED_JIRA_IN_PROGRESS_STATUSES: jira.inProgressStatuses.join(','), - ...(jira.storyPointsField ? { TREESEED_JIRA_STORY_POINTS_FIELD: jira.storyPointsField } : {}), - } : {}), - ...(githubIssues ? { - TREESEED_GITHUB_ISSUES_TOKEN: '', - TREESEED_GITHUB_ISSUES_REPOSITORY: githubIssues.repository, - TREESEED_GITHUB_ISSUES_LABELS: githubIssues.labels.join(','), - TREESEED_GITHUB_ISSUES_IN_PROGRESS_LABELS: githubIssues.inProgressLabels.join(','), - TREESEED_GITHUB_ISSUES_BLOCKED_LABELS: githubIssues.blockedLabels.join(','), - TREESEED_GITHUB_ISSUES_CANCELLED_LABELS: githubIssues.cancelledLabels.join(','), - } : {}), - ...(discord ? { - TREESEED_DISCORD_BOT_TOKEN: '', - TREESEED_DISCORD_CHANNEL_ID: discord.channelId, - ...(discord.guildId ? { TREESEED_DISCORD_GUILD_ID: discord.guildId } : {}), - TREESEED_DISCORD_THREAD_PREFIX: discord.threadPrefix, - } : {}), - }; - return { - dataDir: resolvedEnv.TREESEED_PROVIDER_DATA_DIR ?? '/data', - apiPort: intValue(resolvedEnv.TREESEED_PROVIDER_API_PORT ?? '', 3100), - environment: resolvedEnv.TREESEED_PROVIDER_ENVIRONMENT ?? 'local', - capabilitiesFile: envValue(env, 'TREESEED_PROVIDER_CAPABILITIES_FILE') || null, - budgetFile: envValue(env, 'TREESEED_PROVIDER_BUDGET_FILE') || null, - maxConcurrentWorkdays: intValue(envValue(env, 'TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS'), 1), - maxConcurrentRunners: intValue(envValue(env, 'TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS'), 4), - dailyAgentSecondsLimit: optionalIntValue(envValue(env, 'TREESEED_PROVIDER_DAILY_AGENT_SECONDS_LIMIT')), - monthlyAgentSecondsLimit: optionalIntValue(envValue(env, 'TREESEED_PROVIDER_MONTHLY_AGENT_SECONDS_LIMIT')), - codexAuthFile: codexAuthFile || null, - codexAuthJsonB64: envValue(env, 'TREESEED_CODEX_AUTH_JSON_B64') || null, - codexAuthOverwrite: booleanValue(envValue(env, 'TREESEED_CODEX_AUTH_OVERWRITE')), - jira, - githubIssues, - discord, - treeDx, - env: resolvedEnv, - redactedEnv, - manifestPath, - }; +export function resolveProviderConfig(options: { env?: NodeJS.ProcessEnv; requireConnection?: boolean } = {}): ProviderHostRuntimeConfig { + const source = options.env ?? process.env; + const configuredManifest = value(source, 'TREESEED_CAPACITY_PROVIDER_MANIFEST'); + const manifestPath = configuredManifest || (existsSync('treeseed.capacity-provider.yaml') ? 'treeseed.capacity-provider.yaml' : null); + if (options.requireConnection && !manifestPath) { + throw new Error('Capacity provider runtime requires TREESEED_CAPACITY_PROVIDER_MANIFEST or treeseed.capacity-provider.yaml.'); + } + const env = { + TREESEED_PROVIDER_DATA_DIR: value(source, 'TREESEED_PROVIDER_DATA_DIR') || '/data', + TREESEED_PROVIDER_ENVIRONMENT: value(source, 'TREESEED_PROVIDER_ENVIRONMENT') || value(source, 'TREESEED_ENVIRONMENT') || 'local', + ...(value(source, 'TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST') ? { TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST: value(source, 'TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST') } : {}), + ...(value(source, 'TREESEED_MIN_FREE_DISK_BYTES') ? { TREESEED_MIN_FREE_DISK_BYTES: value(source, 'TREESEED_MIN_FREE_DISK_BYTES') } : {}), + }; + return { + dataDir: env.TREESEED_PROVIDER_DATA_DIR, + environment: env.TREESEED_PROVIDER_ENVIRONMENT, + maxConcurrentRunners: positiveInteger(value(source, 'TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS'), 1), + maxConcurrentWorkdays: positiveInteger(value(source, 'TREESEED_PROVIDER_MAX_CONCURRENT_WORKDAYS'), 1), + capabilitiesFile: value(source, 'TREESEED_PROVIDER_CAPABILITIES_FILE') || null, + budgetFile: value(source, 'TREESEED_PROVIDER_BUDGET_FILE') || null, + dailyAgentSecondsLimit: optionalInteger(value(source, 'TREESEED_PROVIDER_DAILY_AGENT_SECONDS_LIMIT')), + monthlyAgentSecondsLimit: optionalInteger(value(source, 'TREESEED_PROVIDER_MONTHLY_AGENT_SECONDS_LIMIT')), + executorModule: value(source, 'TREESEED_AGENT_EXECUTOR_MODULE') || null, + env, + redactedEnv: { ...env, ...(value(source, 'TREESEED_AGENT_EXECUTOR_MODULE') ? { TREESEED_AGENT_EXECUTOR_MODULE: '' } : {}) }, + manifestPath, + }; } -export function providerRuntimeVersion(env: NodeJS.ProcessEnv = process.env) { - return envValue(env, 'TREESEED_PROVIDER_RUNTIME_VERSION') || packageVersion; +export function providerRuntimeVersion() { + return packageVersion; } diff --git a/src/provider/configuration/manifest.ts b/src/provider/configuration/manifest.ts index 3cf11c18..207246e6 100644 --- a/src/provider/configuration/manifest.ts +++ b/src/provider/configuration/manifest.ts @@ -119,32 +119,32 @@ export async function stageProviderSecret(ref: string, value: string, baseDirect }; } -export function providerMarketProfileEnvironmentName(profile: string) { - return `TREESEED_MARKET_PROFILE_${profile.replace(/[^a-z0-9]/giu, '_').toUpperCase()}_URL`; +export function providerServerProfileEnvironmentName(profile: string) { + return `TREESEED_SERVER_PROFILE_${profile.replace(/[^a-z0-9]/giu, '_').toUpperCase()}_URL`; } -export function providerMarketProfileAudienceEnvironmentName(profile: string) { - return `TREESEED_MARKET_PROFILE_${profile.replace(/[^a-z0-9]/giu, '_').toUpperCase()}_AUDIENCE`; +export function providerServerProfileAudienceEnvironmentName(profile: string) { + return `TREESEED_SERVER_PROFILE_${profile.replace(/[^a-z0-9]/giu, '_').toUpperCase()}_AUDIENCE`; } -type ProviderMarketTarget = Pick | Pick; +type ProviderControlPlaneTarget = Pick | Pick; -export function providerConnectionMarketUrl(connection: ProviderMarketTarget, env: NodeJS.ProcessEnv = process.env) { - if (connection.marketUrl?.trim()) return connection.marketUrl.replace(/\/$/u, ''); - const profile = connection.marketProfile?.trim(); - if (!profile) throw new Error(`Provider connection ${connection.id} does not declare marketUrl or marketProfile.`); - const name = providerMarketProfileEnvironmentName(profile); +export function providerConnectionControlPlaneUrl(connection: ProviderControlPlaneTarget, env: NodeJS.ProcessEnv = process.env) { + if (connection.controlPlaneUrl?.trim()) return connection.controlPlaneUrl.replace(/\/$/u, ''); + const profile = connection.serverProfile?.trim(); + if (!profile) throw new Error(`Provider connection ${connection.id} does not declare controlPlaneUrl or serverProfile.`); + const name = providerServerProfileEnvironmentName(profile); const url = env[name]?.trim(); - if (!url) throw new Error(`Provider market profile ${profile} requires ${name}.`); + if (!url) throw new Error(`Provider server profile ${profile} requires ${name}.`); return url.replace(/\/$/u, ''); } -export function providerConnectionMarketAudience(connection: ProviderMarketTarget, env: NodeJS.ProcessEnv = process.env) { - if (connection.marketAudience?.trim()) return connection.marketAudience.replace(/\/$/u, ''); - const profile = connection.marketProfile?.trim(); +export function providerConnectionControlPlaneAudience(connection: ProviderControlPlaneTarget, env: NodeJS.ProcessEnv = process.env) { + if (connection.controlPlaneAudience?.trim()) return connection.controlPlaneAudience.replace(/\/$/u, ''); + const profile = connection.serverProfile?.trim(); if (profile) { - const configured = env[providerMarketProfileAudienceEnvironmentName(profile)]?.trim(); + const configured = env[providerServerProfileAudienceEnvironmentName(profile)]?.trim(); if (configured) return configured.replace(/\/$/u, ''); } - return providerConnectionMarketUrl(connection, env); + return providerConnectionControlPlaneUrl(connection, env); } diff --git a/src/provider/configuration/value-utils.ts b/src/provider/configuration/value-utils.ts deleted file mode 100644 index 35cd819b..00000000 --- a/src/provider/configuration/value-utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -export function stringValue(...values: unknown[]) { - for (const value of values) if (typeof value === 'string' && value.trim()) return value.trim(); - return null; -} - -export function positiveNumberValue(...values: unknown[]) { - for (const value of values) { - const numeric = Number(value); - if (Number.isFinite(numeric) && numeric > 0) return numeric; - } - return null; -} diff --git a/src/provider/coordination/client.ts b/src/provider/coordination/client.ts index 17636617..3d295471 100644 --- a/src/provider/coordination/client.ts +++ b/src/provider/coordination/client.ts @@ -1,19 +1,31 @@ import { ProviderProtocolClient } from '@treeseed/sdk/capacity-provider'; -import { providerRuntimeVersion, type ProviderConnectionRuntimeContext } from '../configuration/config.ts'; +import type { ProviderControlPlaneConnection } from './contracts.ts'; // Assignment acquisition performs API-owned synthesis before a lease can be // returned. Keep the provider transport deadline comfortably beyond the // control plane's 30-second synthesis/TreeDX request windows so a successful // lease response cannot be abandoned at the boundary. -export const PROVIDER_MARKET_REQUEST_TIMEOUT_MS = 120_000; -export const PROVIDER_ASSIGNMENT_POLLING_TTL_MS = PROVIDER_MARKET_REQUEST_TIMEOUT_MS + 30_000; +export const PROVIDER_CONTROL_PLANE_REQUEST_TIMEOUT_MS = 120_000; +export const PROVIDER_ASSIGNMENT_POLLING_TTL_MS = PROVIDER_CONTROL_PLANE_REQUEST_TIMEOUT_MS + 30_000; -export function createProviderMarketClient(config: ProviderConnectionRuntimeContext) { +export function createProviderControlPlaneClient(config: ProviderControlPlaneConnection) { return new ProviderProtocolClient({ - marketUrl: config.marketUrl, + controlPlaneUrl: config.controlPlaneUrl, accessToken: config.accessToken, accessTokenProvider: config.accessTokenProvider, - userAgent: `@treeseed/agent capacity-provider/${providerRuntimeVersion()}`, - requestTimeoutMs: PROVIDER_MARKET_REQUEST_TIMEOUT_MS, + userAgent: '@treeseed/agent capacity-provider', + requestTimeoutMs: PROVIDER_CONTROL_PLANE_REQUEST_TIMEOUT_MS, }); } + +export function providerOperationPath(binding: { descriptor: { operationId: string; rest?: { path: string } } }, parameters: Record = {}) { + const template = binding.descriptor.rest?.path; + if (!template) throw new Error(`Provider operation ${binding.descriptor.operationId} has no REST binding.`); + const path = template.replace(/\{([A-Za-z][A-Za-z0-9]*)\}/gu, (_match, name: string) => { + const value = parameters[name]; + if (value === undefined) throw new Error(`Provider operation ${binding.descriptor.operationId} requires path parameter ${name}.`); + return encodeURIComponent(value); + }); + if (/\{[^}]+\}/u.test(path)) throw new Error(`Provider operation ${binding.descriptor.operationId} has unresolved path parameters.`); + return path; +} diff --git a/src/provider/coordination/connection-scheduler.ts b/src/provider/coordination/connection-scheduler.ts deleted file mode 100644 index e0d461b9..00000000 --- a/src/provider/coordination/connection-scheduler.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { ProviderConnectionConfig } from '@treeseed/sdk/capacity-provider/contracts'; - -export interface ProviderSchedulableConnection { - connection: ProviderConnectionConfig; -} - -export interface ProviderSlotLease { - id: string; - connection: T; - release(): void; -} - -function schedulingWeight(connection: ProviderConnectionConfig) { - return connection.offer.sharePercent ?? connection.offer.weight ?? 1; -} - -/** - * Provider-local weighted-deficit scheduler. Slot accounting is global to one - * provider process while connection caps remain independently enforceable. - */ -export class ProviderGlobalSlotScheduler { - private connections: T[] = []; - private readonly deficits = new Map(); - private readonly activeByConnection = new Map(); - private activeTotal = 0; - private sequence = 0; - - constructor(readonly maxConcurrentSlots: number) { - if (!Number.isInteger(maxConcurrentSlots) || maxConcurrentSlots < 1) { - throw new Error('Provider-global maxConcurrentSlots must be a positive integer.'); - } - } - - updateConnections(connections: T[]) { - this.connections = connections.filter((entry) => entry.connection.enabled !== false); - const ids = new Set(this.connections.map((entry) => entry.connection.id)); - for (const id of this.deficits.keys()) if (!ids.has(id)) this.deficits.delete(id); - } - - snapshot() { - return { - maxConcurrentSlots: this.maxConcurrentSlots, - activeTotal: this.activeTotal, - availableSlots: Math.max(0, this.maxConcurrentSlots - this.activeTotal), - connections: this.connections.map(({ connection }) => ({ - id: connection.id, - weight: schedulingWeight(connection), - active: this.activeByConnection.get(connection.id) ?? 0, - maxConcurrentRunners: connection.offer.maxConcurrentRunners ?? this.maxConcurrentSlots, - deficit: this.deficits.get(connection.id) ?? 0, - })), - }; - } - - acquire(): ProviderSlotLease | null { - if (this.activeTotal >= this.maxConcurrentSlots) return null; - const eligible = this.connections.filter(({ connection }) => - (this.activeByConnection.get(connection.id) ?? 0) < (connection.offer.maxConcurrentRunners ?? this.maxConcurrentSlots)); - if (eligible.length === 0) return null; - const totalWeight = eligible.reduce((total, { connection }) => total + schedulingWeight(connection), 0); - for (const { connection } of eligible) { - this.deficits.set(connection.id, (this.deficits.get(connection.id) ?? 0) + schedulingWeight(connection)); - } - eligible.sort((left, right) => - (this.deficits.get(right.connection.id) ?? 0) - (this.deficits.get(left.connection.id) ?? 0) - || left.connection.id.localeCompare(right.connection.id)); - const selected = eligible[0]; - const connectionId = selected.connection.id; - this.deficits.set(connectionId, (this.deficits.get(connectionId) ?? 0) - totalWeight); - this.activeTotal += 1; - this.activeByConnection.set(connectionId, (this.activeByConnection.get(connectionId) ?? 0) + 1); - let released = false; - return { - id: `provider-slot-${++this.sequence}`, - connection: selected, - release: () => { - if (released) return; - released = true; - this.activeTotal = Math.max(0, this.activeTotal - 1); - this.activeByConnection.set(connectionId, Math.max(0, (this.activeByConnection.get(connectionId) ?? 1) - 1)); - }, - }; - } -} - -export function providerManifestConcurrency(input: { - executionProviders: Array<{ nativeLimits: Record }>; - hostLimit: number; -}) { - const explicit = input.executionProviders - .map((provider) => Number(provider.nativeLimits.maxConcurrentRunners)) - .filter((value) => Number.isInteger(value) && value > 0); - const hostLimit = Math.max(1, Math.floor(input.hostLimit)); - const providerLimit = explicit.length > 0 ? explicit.reduce((total, value) => total + value, 0) : hostLimit; - return Math.min(hostLimit, providerLimit); -} diff --git a/src/provider/coordination/connection-state.ts b/src/provider/coordination/connection-state.ts index 733868b8..9adf9321 100644 --- a/src/provider/coordination/connection-state.ts +++ b/src/provider/coordination/connection-state.ts @@ -5,9 +5,9 @@ import type { ProviderSupplyOffer } from '@treeseed/sdk/capacity-provider/contra export interface ProviderConnectionState { schemaVersion: 1; connectionId: string; - marketUrl: string; - marketProfile?: string | null; - marketAudience?: string | null; + controlPlaneUrl: string; + serverProfile?: string | null; + controlPlaneAudience?: string | null; offer: ProviderSupplyOffer; teamId?: string | null; providerId?: string | null; diff --git a/src/provider/coordination/contracts.ts b/src/provider/coordination/contracts.ts new file mode 100644 index 00000000..a2d70f9f --- /dev/null +++ b/src/provider/coordination/contracts.ts @@ -0,0 +1,5 @@ +export interface ProviderControlPlaneConnection { + controlPlaneUrl: string; + accessToken: string; + accessTokenProvider?: () => Promise; +} diff --git a/src/provider/coordination/coordinator.ts b/src/provider/coordination/coordinator.ts index f64ad3be..8faffcdc 100644 --- a/src/provider/coordination/coordinator.ts +++ b/src/provider/coordination/coordinator.ts @@ -7,6 +7,8 @@ import type { ProviderRegistrationSubmission, } from '@treeseed/sdk/capacity-provider/contracts'; import { ProviderProtocolClient } from '@treeseed/sdk/capacity-provider'; +import { CONTROL_PLANE_OPERATIONS } from '@treeseed/sdk/operator-contracts'; +import { providerOperationPath } from './client.ts'; import { generatedMembershipCredentialRef, removeProviderConnectionState, @@ -14,15 +16,11 @@ import { writeProviderConnectionState, type ProviderConnectionState, } from './connection-state.ts'; +import { generateCapacityProviderIdentity, loadCapacityProviderIdentity, signCapacityProviderProof, + type CapacityProviderPrivateJwk } from '../accounts/identity.ts'; import { - generateCapacityProviderIdentity, - signCapacityProviderProof, - type CapacityProviderPrivateJwk, -} from '@treeseed/sdk/capacity-provider'; -import { loadCapacityProviderIdentity } from '../accounts/identity.ts'; -import { - providerConnectionMarketUrl, - providerConnectionMarketAudience, + providerConnectionControlPlaneUrl, + providerConnectionControlPlaneAudience, removeProviderSecret, resolveProviderSecret, stageProviderSecret, @@ -35,8 +33,8 @@ import { ProviderLocalCapacityStore } from '../capacity/capacity-core/local-capa export interface ProviderConnectionRuntime { connection: ProviderConnectionConfig; - marketUrl: string; - marketAudience: string; + controlPlaneUrl: string; + controlPlaneAudience: string; teamId: string; providerId: string; membershipId: string; @@ -71,8 +69,8 @@ function unsignedRegistration(manifest: CapacityProviderManifestV2, connection: }; } -function nextState(connectionId: string, marketUrl: string, prior: ProviderConnectionState | null, patch: Partial): ProviderConnectionState { - const next = { schemaVersion: 1 as const, connectionId, marketUrl, ...(prior ?? {}), ...patch, updatedAt: new Date().toISOString() }; +function nextState(connectionId: string, controlPlaneUrl: string, prior: ProviderConnectionState | null, patch: Partial): ProviderConnectionState { + const next = { schemaVersion: 1 as const, connectionId, controlPlaneUrl, ...(prior ?? {}), ...patch, updatedAt: new Date().toISOString() }; if (!next.offer) throw new Error(`Provider connection ${connectionId} is missing its durable supply offer.`); return next as ProviderConnectionState; } @@ -100,8 +98,8 @@ export class CapacityProviderCoordinator { return this.identity; } - private client(marketUrl: string) { - return new ProviderProtocolClient({ marketUrl, fetchImpl: this.options.fetch }); + private client(controlPlaneUrl: string) { + return new ProviderProtocolClient({ controlPlaneUrl, fetchImpl: this.options.fetch }); } private async proof(input: { audience: string; method: string; path: string; body: unknown; identity: CoordinatorIdentity }) { @@ -110,8 +108,8 @@ export class CapacityProviderCoordinator { private async connectApproved(input: { connection: ProviderConnectionConfig; - marketUrl: string; - marketAudience: string; + controlPlaneUrl: string; + controlPlaneAudience: string; credentialRef: string; credentialId: string; minimumValidityMs?: number; @@ -124,8 +122,8 @@ export class CapacityProviderCoordinator { const idempotencyKey = `access:${input.connection.id}:${randomUUID()}`; const requestedValiditySeconds = Math.ceil((minimumValidityMs + 60_000) / 1000); const body = { credentialId: input.credentialId, idempotencyKey, requestedValiditySeconds }; - const proof = await this.proof({ audience: input.marketAudience, method: 'POST', path: '/v1/provider/access-tokens', body, identity }); - const token = await this.client(input.marketUrl).issueAccessToken(credential, input.credentialId, proof, idempotencyKey, requestedValiditySeconds); + const proof = await this.proof({ audience: input.controlPlaneAudience, method: 'POST', path: providerOperationPath(CONTROL_PLANE_OPERATIONS.providers.issueAccessToken), body, identity }); + const token = await this.client(input.controlPlaneUrl).issueAccessToken(credential, input.credentialId, proof, idempotencyKey, requestedValiditySeconds); if (token.teamId !== input.connection.teamId || token.providerId !== input.connection.providerId || token.membershipId !== input.connection.membershipId || token.credentialId !== input.credentialId) { throw new Error(`Provider connection ${input.connection.id} access token does not match its configured team, provider, membership, and credential binding.`); } @@ -140,8 +138,8 @@ export class CapacityProviderCoordinator { if (current) return current; const refresh = this.connectApproved({ connection, - marketUrl: providerConnectionMarketUrl(connection, this.options.env), - marketAudience: providerConnectionMarketAudience(connection, this.options.env), + controlPlaneUrl: providerConnectionControlPlaneUrl(connection, this.options.env), + controlPlaneAudience: providerConnectionControlPlaneAudience(connection, this.options.env), credentialRef: connection.membershipCredentialRef, credentialId: connection.membershipCredentialId, minimumValidityMs, @@ -156,26 +154,26 @@ export class CapacityProviderCoordinator { async reconcileConnection(connection: ProviderConnectionConfig): Promise { if (connection.enabled === false) return { connectionId: connection.id, status: 'disabled' }; - const marketUrl = providerConnectionMarketUrl(connection, this.options.env); - const marketAudience = providerConnectionMarketAudience(connection, this.options.env); - const accessToken = await this.connectApproved({ connection, marketUrl, marketAudience, credentialRef: connection.membershipCredentialRef, credentialId: connection.membershipCredentialId }); - return { connectionId: connection.id, status: 'connected', teamId: connection.teamId, providerId: connection.providerId, membershipId: connection.membershipId, runtime: { connection, marketUrl, marketAudience, teamId: connection.teamId, providerId: connection.providerId, membershipId: connection.membershipId, credentialId: connection.membershipCredentialId, accessToken } }; + const controlPlaneUrl = providerConnectionControlPlaneUrl(connection, this.options.env); + const controlPlaneAudience = providerConnectionControlPlaneAudience(connection, this.options.env); + const accessToken = await this.connectApproved({ connection, controlPlaneUrl, controlPlaneAudience, credentialRef: connection.membershipCredentialRef, credentialId: connection.membershipCredentialId }); + return { connectionId: connection.id, status: 'connected', teamId: connection.teamId, providerId: connection.providerId, membershipId: connection.membershipId, runtime: { connection, controlPlaneUrl, controlPlaneAudience, teamId: connection.teamId, providerId: connection.providerId, membershipId: connection.membershipId, credentialId: connection.membershipCredentialId, accessToken } }; } async beginJoin(join: CapacityProviderJoinInput): Promise { if (this.loaded.manifest.connections.some((connection) => connection.id === join.id)) throw new Error(`Provider connection ${join.id} is already approved and configured.`); const existing = await readProviderConnectionState(this.dataDir, join.id); if (existing?.registrationRequestId) return this.pollRegistrationStatus(join.id); - const marketUrl = providerConnectionMarketUrl(join, this.options.env); - const marketAudience = providerConnectionMarketAudience(join, this.options.env); + const controlPlaneUrl = providerConnectionControlPlaneUrl(join, this.options.env); + const controlPlaneAudience = providerConnectionControlPlaneAudience(join, this.options.env); const identity = await this.providerIdentity(); - const client = this.client(marketUrl); + const client = this.client(controlPlaneUrl); const unsigned = unsignedRegistration(this.loaded.manifest, join, identity); - const proof = await this.proof({ audience: marketAudience, method: 'POST', path: '/v1/provider-registrations', body: unsigned, identity }); + const proof = await this.proof({ audience: controlPlaneAudience, method: 'POST', path: providerOperationPath(CONTROL_PLANE_OPERATIONS.providers.register), body: unsigned, identity }); const submission: ProviderRegistrationSubmission = { ...unsigned, proof }; const registrationKey = await resolveProviderSecret(join.registrationKeyRef, { env: this.options.env, baseDirectory: this.loaded.directory, dataDirectory: this.dataDir, resolver: this.options.secretResolver }); const request = await client.register(registrationKey, submission, `register:${join.id}`); - const state = nextState(join.id, marketUrl, null, { marketProfile: join.marketProfile ?? null, marketAudience, offer: join.offer, teamId: request.teamId, providerId: request.providerId, registrationRequestId: request.id, registrationStatus: request.status }); + const state = nextState(join.id, controlPlaneUrl, null, { serverProfile: join.serverProfile ?? null, controlPlaneAudience, offer: join.offer, teamId: request.teamId, providerId: request.providerId, registrationRequestId: request.id, registrationStatus: request.status }); await writeProviderConnectionState(this.dataDir, state); return { connectionId: join.id, status: 'pending-approval', teamId: request.teamId, providerId: request.providerId, requestId: request.id }; } @@ -184,8 +182,8 @@ export class CapacityProviderCoordinator { if (!state.teamId || !state.providerId || !state.membershipId || !state.credentialId || !state.generatedCredentialRef) throw new Error(`Provider join ${state.connectionId} is not ready to materialize.`); const connection: ProviderConnectionConfig = { id: state.connectionId, - ...(state.marketProfile ? { marketProfile: state.marketProfile } : { marketUrl: state.marketUrl }), - ...(state.marketAudience ? { marketAudience: state.marketAudience } : {}), + ...(state.serverProfile ? { serverProfile: state.serverProfile } : { controlPlaneUrl: state.controlPlaneUrl }), + ...(state.controlPlaneAudience ? { controlPlaneAudience: state.controlPlaneAudience } : {}), teamId: state.teamId, providerId: state.providerId, membershipId: state.membershipId, @@ -207,16 +205,16 @@ export class CapacityProviderCoordinator { let state = await readProviderConnectionState(this.dataDir, connectionId); if (!state?.registrationRequestId) throw new Error(`Provider join ${connectionId} has not been started.`); if (!state.offer) throw new Error(`Provider join ${connectionId} is missing its durable supply offer and cannot be recovered. Start a new signed join with an explicit offer.`); - const marketUrl = state.marketUrl; - const marketAudience = state.marketAudience ?? marketUrl; + const controlPlaneUrl = state.controlPlaneUrl; + const controlPlaneAudience = state.controlPlaneAudience ?? controlPlaneUrl; const identity = await this.providerIdentity(); - const client = this.client(marketUrl); - const statusPath = `/v1/provider-registrations/${encodeURIComponent(state.registrationRequestId)}`; - const statusProof = await this.proof({ audience: marketAudience, method: 'GET', path: statusPath, body: { requestId: state.registrationRequestId }, identity }); + const client = this.client(controlPlaneUrl); + const statusPath = providerOperationPath(CONTROL_PLANE_OPERATIONS.providers.registration, { requestId: state.registrationRequestId }); + const statusProof = await this.proof({ audience: controlPlaneAudience, method: 'GET', path: statusPath, body: { requestId: state.registrationRequestId }, identity }); const request = await client.registrationStatus(state.registrationRequestId, statusProof); - state = nextState(connectionId, marketUrl, state, { teamId: request.teamId, providerId: request.providerId, membershipId: request.membershipId, registrationStatus: request.status }); + state = nextState(connectionId, controlPlaneUrl, state, { teamId: request.teamId, providerId: request.providerId, membershipId: request.membershipId, registrationStatus: request.status }); await writeProviderConnectionState(this.dataDir, state); - return { state, request, marketUrl, marketAudience, identity, client }; + return { state, request, controlPlaneUrl, controlPlaneAudience, identity, client }; } async pollRegistrationStatus(connectionId: string): Promise { @@ -247,17 +245,17 @@ export class CapacityProviderCoordinator { if (existing?.generatedCredentialRef && existing.credentialId && existing.membershipId) { return this.reconcileConnection(await this.materializeApprovedConnection(existing)); } - const { state: polledState, request, marketUrl, marketAudience, identity, client } = await this.pollRegistrationRequest(connectionId); + const { state: polledState, request, controlPlaneUrl, controlPlaneAudience, identity, client } = await this.pollRegistrationRequest(connectionId); let state = polledState; if (request.status !== 'approved' || !request.membershipId) return { connectionId, status: request.status === 'pending' ? 'pending-approval' : request.status, teamId: request.teamId, providerId: request.providerId, requestId: request.id }; - const exchangePath = `/v1/provider-registrations/${encodeURIComponent(request.id)}/credential`; + const exchangePath = providerOperationPath(CONTROL_PLANE_OPERATIONS.providers.exchangeCredential, { requestId: request.id }); const exchangeIdempotencyKey = `credential:${request.id}`; - const exchangeProof = await this.proof({ audience: marketAudience, method: 'POST', path: exchangePath, body: { requestId: request.id, idempotencyKey: exchangeIdempotencyKey }, identity }); + const exchangeProof = await this.proof({ audience: controlPlaneAudience, method: 'POST', path: exchangePath, body: { requestId: request.id, idempotencyKey: exchangeIdempotencyKey }, identity }); const issued = await client.exchangeCredential(request.id, exchangeProof, exchangeIdempotencyKey); const generatedRef = generatedMembershipCredentialRef(connectionId); await writeProviderSecret(generatedRef, issued.credential, this.loaded.directory, this.dataDir); - state = nextState(connectionId, marketUrl, state, { teamId: issued.teamId, providerId: issued.providerId, membershipId: issued.membershipId, credentialId: issued.id, generatedCredentialRef: generatedRef, registrationStatus: 'approved' }); + state = nextState(connectionId, controlPlaneUrl, state, { teamId: issued.teamId, providerId: issued.providerId, membershipId: issued.membershipId, credentialId: issued.id, generatedCredentialRef: generatedRef, registrationStatus: 'approved' }); await writeProviderConnectionState(this.dataDir, state); const connected = await this.reconcileConnection(await this.materializeApprovedConnection(state)); return { ...connected, requestId: request.id }; @@ -291,7 +289,7 @@ export class CapacityProviderCoordinator { const result = await this.reconcileConnection(connection); if (!result.runtime) throw new Error(`Provider connection ${connectionId} is not connected.`); const idempotencyKey = `leave:${connectionId}:${result.runtime.membershipId}`; - membership = await this.client(result.runtime.marketUrl).leaveMembership(result.runtime.accessToken.accessToken, idempotencyKey); + membership = await this.client(result.runtime.controlPlaneUrl).leaveMembership(result.runtime.accessToken.accessToken, idempotencyKey); remoteRevocationConfirmed = true; } catch (error) { remoteError = error instanceof Error ? error.message : String(error); @@ -318,22 +316,22 @@ export class CapacityProviderCoordinator { const connected = await this.reconcileConnection(connection); if (!connected.runtime) throw new Error(`Provider connection ${connectionId} is not connected and cannot authorize credential rotation.`); const rotationIdempotencyKey = state.credentialRotationIdempotencyKey ?? `credential-rotation:${connectionId}:${randomUUID()}`; - state = nextState(connectionId, state.marketUrl, state, { + state = nextState(connectionId, state.controlPlaneUrl, state, { offer: connection.offer, credentialRotationIdempotencyKey: rotationIdempotencyKey, }); await writeProviderConnectionState(this.dataDir, state); - const authorization = await this.client(connected.runtime.marketUrl).authorizeCredentialRotation(connected.runtime.accessToken.accessToken, rotationIdempotencyKey); + const authorization = await this.client(connected.runtime.controlPlaneUrl).authorizeCredentialRotation(connected.runtime.accessToken.accessToken, rotationIdempotencyKey); const exchangeIdempotencyKey = state.credentialExchangeIdempotencyKey ?? `credential:${registrationRequestId}:generation:${authorization.generation}`; - state = nextState(connectionId, state.marketUrl, state, { credentialExchangeIdempotencyKey: exchangeIdempotencyKey }); + state = nextState(connectionId, state.controlPlaneUrl, state, { credentialExchangeIdempotencyKey: exchangeIdempotencyKey }); await writeProviderConnectionState(this.dataDir, state); const identity = await this.providerIdentity(); - const exchangePath = `/v1/provider-registrations/${encodeURIComponent(registrationRequestId)}/credential`; - const exchangeProof = await this.proof({ audience: connected.runtime.marketAudience, method: 'POST', path: exchangePath, body: { requestId: registrationRequestId, idempotencyKey: exchangeIdempotencyKey }, identity }); - const issued = await this.client(connected.runtime.marketUrl).exchangeCredential(registrationRequestId, exchangeProof, exchangeIdempotencyKey); + const exchangePath = providerOperationPath(CONTROL_PLANE_OPERATIONS.providers.exchangeCredential, { requestId: registrationRequestId }); + const exchangeProof = await this.proof({ audience: connected.runtime.controlPlaneAudience, method: 'POST', path: exchangePath, body: { requestId: registrationRequestId, idempotencyKey: exchangeIdempotencyKey }, identity }); + const issued = await this.client(connected.runtime.controlPlaneUrl).exchangeCredential(registrationRequestId, exchangeProof, exchangeIdempotencyKey); const credentialRef = state.generatedCredentialRef ?? connection.membershipCredentialRef; await writeProviderSecret(credentialRef, issued.credential, this.loaded.directory, this.dataDir); - state = nextState(connectionId, state.marketUrl, state, { credentialId: issued.id, generatedCredentialRef: credentialRef, credentialRotationIdempotencyKey: null, credentialExchangeIdempotencyKey: null }); + state = nextState(connectionId, state.controlPlaneUrl, state, { credentialId: issued.id, generatedCredentialRef: credentialRef, credentialRotationIdempotencyKey: null, credentialExchangeIdempotencyKey: null }); await writeProviderConnectionState(this.dataDir, state); await this.localState.removeToken(connectionId); return this.reconcileConnection(await this.materializeApprovedConnection(state)); @@ -349,11 +347,12 @@ export class CapacityProviderCoordinator { const currentIdentity = await this.providerIdentity(); const expectedIdentityVersion = connected.runtime.accessToken.identityVersion; const signedBody = { expectedIdentityVersion, newPublicJwk: nextPublicJwk }; - const oldProof = await signCapacityProviderProof({ privateJwk: currentIdentity.privateJwk, publicJwk: currentIdentity.publicJwk, identityVersion: expectedIdentityVersion, method: 'POST', path: '/v1/provider/identity/rotate', audience: connected.runtime.marketAudience, body: signedBody }); - const newProof = await signCapacityProviderProof({ privateJwk: nextPrivateJwk, publicJwk: nextPublicJwk, identityVersion: expectedIdentityVersion + 1, method: 'POST', path: '/v1/provider/identity/rotate', audience: connected.runtime.marketAudience, body: signedBody }); + const rotationPath = providerOperationPath(CONTROL_PLANE_OPERATIONS.providers.rotateIdentity); + const oldProof = await signCapacityProviderProof({ privateJwk: currentIdentity.privateJwk, publicJwk: currentIdentity.publicJwk, identityVersion: expectedIdentityVersion, method: 'POST', path: rotationPath, audience: connected.runtime.controlPlaneAudience, body: signedBody }); + const newProof = await signCapacityProviderProof({ privateJwk: nextPrivateJwk, publicJwk: nextPublicJwk, identityVersion: expectedIdentityVersion + 1, method: 'POST', path: rotationPath, audience: connected.runtime.controlPlaneAudience, body: signedBody }); const staged = await stageProviderSecret(this.loaded.manifest.identity.privateKeyRef, JSON.stringify(nextPrivateJwk), this.loaded.directory, this.dataDir); try { - const identity = await this.client(connected.runtime.marketUrl).rotateIdentity(connected.runtime.accessToken.accessToken, { ...signedBody, oldProof, newProof }, `identity-rotate:${expectedIdentityVersion + 1}`); + const identity = await this.client(connected.runtime.controlPlaneUrl).rotateIdentity(connected.runtime.accessToken.accessToken, { ...signedBody, oldProof, newProof }, `identity-rotate:${expectedIdentityVersion + 1}`); await staged.commit(); this.identity = null; await this.localState.clearTokens(); diff --git a/src/provider/coordination/lease-client.ts b/src/provider/coordination/lease-client.ts deleted file mode 100644 index a171b44b..00000000 --- a/src/provider/coordination/lease-client.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ProviderRuntimeEventInput } from '@treeseed/sdk/agent-capacity'; - -export interface ProviderAssignmentClient { - nextAssignment(request?: Record): Promise; - assignment?(assignmentId: string): Promise; - createAssignmentModeRun(assignmentId: string, request: Record): Promise; - createAssignmentEvent?(assignmentId: string, request: ProviderRuntimeEventInput): Promise; - publishAssignmentSignal?(assignmentId: string, request: Record): Promise; - preflightAssignmentCompletion?(assignmentId: string, request: Record): Promise; - completeAssignment(assignmentId: string, request: Record): Promise; - failAssignment(assignmentId: string, request: Record): Promise; - reportAssignmentUsage?(assignmentId: string, request: Record, idempotencyKey: string): Promise; - settleAssignment?(assignmentId: string, request: Record, idempotencyKey: string): Promise; - renewAssignment?(assignmentId: string, request: Record): Promise; - returnAssignment?(assignmentId: string, request: Record): Promise; - dispatchAssignmentWorkflowOperation?(assignmentId: string, operationId: string, request: Record): Promise; - getAssignmentWorkflowRun?(assignmentId: string, runId: string): Promise; -} - -// Settlement consumes the reservation before completion releases the lease. -// Close local renewal authority before that financial terminal boundary so a -// timer already queued for the same assignment cannot renew against a consumed -// reservation during the settlement-to-completion handoff. -const TERMINAL_METHODS = new Set(['settleAssignment', 'completeAssignment', 'failAssignment', 'returnAssignment']); - -export function providerAssignmentClientWithTerminalBoundary( - client: ProviderAssignmentClient, - onTerminalizing: () => void, -): ProviderAssignmentClient { - return new Proxy(client, { - get(target, property) { - const value = Reflect.get(target, property, target); - if (typeof value !== 'function') return value; - if (TERMINAL_METHODS.has(property)) { - return (...args: unknown[]) => { - onTerminalizing(); - return Reflect.apply(value, target, args); - }; - } - return value.bind(target); - }, - }); -} diff --git a/src/provider/coordination/lease-recovery.ts b/src/provider/coordination/lease-recovery.ts index 0307876a..196ccc3f 100644 --- a/src/provider/coordination/lease-recovery.ts +++ b/src/provider/coordination/lease-recovery.ts @@ -1,85 +1,41 @@ import type { ProviderConnectionRuntime } from './coordinator.ts'; -import { createProviderMarketClient } from './client.ts'; +import { createProviderControlPlaneClient } from './client.ts'; import type { ProviderHostRuntimeConfig } from '../configuration/config.ts'; import { ProviderLocalCapacityStore } from '../capacity/capacity-core/local-capacity-store.ts'; -import { listMaterializedAssignmentIds,releaseRecoveredAssignmentMaterialization } from '../projects/projects-core/project-materialization.ts'; -interface ProviderLeaseRecoveryClient { - assignment(assignmentId: string): Promise; - returnAssignment(assignmentId: string, request: Record): Promise; +function record(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; +export async function recoverProviderLocalLeases(input: { config: ProviderHostRuntimeConfig; connections: ProviderConnectionRuntime[]; store?: ProviderLocalCapacityStore; includeRunning?: boolean }) { + const store = input.store ?? new ProviderLocalCapacityStore(input.config.dataDir); + const results: Record[] = []; + for (const claim of await store.claimsForRecovery(input.includeRunning !== false)) { + const connection = input.connections.find((entry) => entry.connection.id === claim.connectionId); + if (!connection || !claim.assignmentId || !claim.leaseToken) { + await store.recordFailure(claim.id, 'Connection or lease identity is unavailable during recovery.'); + results.push({ claimId: claim.id, status: 'retained', reason: 'lease_authority_unavailable' }); + continue; + } + try { + const client = createProviderControlPlaneClient({ + controlPlaneUrl: connection.controlPlaneUrl, + accessToken: connection.accessToken.accessToken, + }); + const observed = record(await client.assignment(claim.assignmentId)); + const assignment = record(observed.data ?? observed.assignment ?? observed); + const status = textStatus(assignment.status); + if (status === 'leased' || status === 'running') await client.returnAssignment(claim.assignmentId, { leaseToken: claim.leaseToken, runnerId: claim.runnerId, code: 'provider_restart_recovery', reason: 'Provider restarted before durable completion.' }); + await store.finalize(claim.id, status === 'leased' || status === 'running' ? 'restart-return-confirmed' : `authoritative-${status || 'unknown'}`); + results.push({ claimId: claim.id, assignmentId: claim.assignmentId, status: 'released', observedStatus: status }); + } catch (error) { + await store.recordFailure(claim.id, error instanceof Error ? error.message : String(error)); + results.push({ claimId: claim.id, assignmentId: claim.assignmentId, status: 'retained', reason: 'control_plane_unavailable' }); + } + } + return results; } -export async function recoverProviderLocalLeases(input: { - config: ProviderHostRuntimeConfig; - connections: ProviderConnectionRuntime[]; - store?: ProviderLocalCapacityStore; - clientFactory?: (config: Parameters[0]) => ProviderLeaseRecoveryClient; - includeRunning?: boolean; -}) { - const store = input.store ?? new ProviderLocalCapacityStore(input.config.dataDir); - const clientFactory = input.clientFactory ?? createProviderMarketClient; - const claims = await store.claimsForRecovery(input.includeRunning !== false); - const results: Array> = []; - const observedAssignmentIds = new Set(); - for (const claim of claims) { - if (claim.assignmentId) observedAssignmentIds.add(claim.assignmentId); - const connection = input.connections.find((entry) => entry.connection.id === claim.connectionId); - if (!connection || !claim.assignmentId || !claim.leaseToken) { - await store.recordFailure(claim.id, 'Connection or lease identity is unavailable during recovery.'); - results.push({ claimId: claim.id, status: 'retained', reason: 'connection_or_lease_identity_unavailable' }); - continue; - } - const runtimeConfig = { - ...input.config, connectionId: connection.connection.id, marketUrl: connection.marketUrl, marketAudience: connection.marketAudience, - teamId: connection.teamId, providerId: connection.providerId, membershipId: connection.membershipId, accessToken: connection.accessToken.accessToken, - }; - try { - const client = clientFactory(runtimeConfig); - const observed = record(await client.assignment(claim.assignmentId)); - const assignment = record(observed.payload ?? observed.assignment ?? observed); - const status = String(assignment.status ?? ''); - if (status === 'leased') { - await client.returnAssignment(claim.assignmentId, { - leaseToken: claim.leaseToken, runnerId: claim.runnerId, - reason: 'Provider runner restarted before the prior lease completed.', code: 'provider_restart_recovery', - }); - } - const materialization = await releaseRecoveredAssignmentMaterialization(input.config, claim.assignmentId); - await store.finalize(claim.id, status === 'leased' ? 'restart-return-confirmed' : `authoritative-${status || 'unknown'}`); - results.push({ claimId: claim.id, assignmentId: claim.assignmentId, status: status === 'leased' ? 'returned' : 'released', observedStatus: status, materialization }); - } catch (error) { - await store.recordFailure(claim.id, error instanceof Error ? error.message : String(error)); - results.push({ claimId: claim.id, assignmentId: claim.assignmentId, status: 'retained', reason: 'control_plane_unavailable', error: error instanceof Error ? error.message : String(error) }); - } - } - const terminal = new Set(['completed', 'failed', 'cancelled', 'returned', 'expired']); - for (const assignmentId of await listMaterializedAssignmentIds(input.config)) { - if (observedAssignmentIds.has(assignmentId)) continue; - let observedStatus = ''; - for (const connection of input.connections) { - const runtimeConfig = { - ...input.config, connectionId: connection.connection.id, marketUrl: connection.marketUrl, marketAudience: connection.marketAudience, - teamId: connection.teamId, providerId: connection.providerId, membershipId: connection.membershipId, accessToken: connection.accessToken.accessToken, - }; - try { - const observed = record(await clientFactory(runtimeConfig).assignment(assignmentId)); - const assignment = record(observed.payload ?? observed.assignment ?? observed); - observedStatus = String(assignment.status ?? ''); - if (observedStatus) break; - } catch { - // The assignment may belong to another configured team connection. - } - } - if (!terminal.has(observedStatus)) { - results.push({ assignmentId, status: 'retained', observedStatus: observedStatus || 'unresolved', reason: 'orphan_authority_not_terminal' }); - continue; - } - const materialization = await releaseRecoveredAssignmentMaterialization(input.config, assignmentId); - results.push({ assignmentId, status: materialization.status === 'released' || materialization.status === 'absent' ? 'released' : 'retained', observedStatus, materialization }); - } - return results; +function textStatus(value: unknown) { + return typeof value === 'string' ? value : ''; } diff --git a/src/provider/execution/contracts.ts b/src/provider/execution/contracts.ts new file mode 100644 index 00000000..3a3d0b8b --- /dev/null +++ b/src/provider/execution/contracts.ts @@ -0,0 +1,35 @@ +export interface AgentExecutionRequest { + assignment: Record; + assignmentId: string; + leaseToken: string; + runnerId: string; + signal?: AbortSignal; +} + +export interface AgentExecutionResult { + status: 'completed' | 'failed' | 'returned'; + summary: string; + retryable?: boolean; + code?: string; + outputs?: Record; + usage?: Record[]; + artifacts?: Record[]; +} + +export interface AgentExecutorObservation { + available: boolean; + activeAssignments?: number; + capabilities?: string[]; + reason?: string; +} + +export interface AgentExecutor { + readonly id: string; + observe(): Promise; + execute(request: AgentExecutionRequest): Promise; + recover?(request: Pick): Promise; +} + +export interface AgentExecutorModule { + createAgentExecutor(input: { executionProviderId: string; environment: string }): AgentExecutor | Promise; +} diff --git a/src/provider/execution/execution-lifecycle.ts b/src/provider/execution/execution-lifecycle.ts deleted file mode 100644 index 636f8e99..00000000 --- a/src/provider/execution/execution-lifecycle.ts +++ /dev/null @@ -1,414 +0,0 @@ -import type { - AgentHandlerOutput, - ExecutionProviderAdapter, - ExecutionProviderInvocation, - ExecutionPreparationResult, - ExecutionProviderToolDescriptor, -} from '../../agents/runtime/runtime-types.ts'; -import type { - ExecutionArtifactRef, - ExecutionProviderObserveInput, - ExecutionRunRef, - ExecutionRunSnapshot, - ExecutionUsageActual, -} from '@treeseed/sdk/types/agents'; -import type { AgentModeRunStatus } from '@treeseed/sdk/agent-capacity'; -import { randomUUID } from 'node:crypto'; -import type { AssignmentToolCatalog } from '../commerce/catalog/assignment-tool-catalog.ts'; -import { record } from '../configuration/value-utils.ts'; - -const DEFAULT_ASYNC_POLL_INTERVAL_MS = 250; -const DEFAULT_ASYNC_MAX_POLLS = 20; - -function waitingResult(summary: string): AgentHandlerOutput { - return { - status: 'waiting', - summary, - }; -} - - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function isAsyncExecutionStatus(status: string) { - return status === 'accepted' || status === 'running' || status === 'waiting' || status === 'blocked'; -} - -function isRetryableReturnedSnapshot(snapshot: ExecutionRunSnapshot) { - return snapshot.status === 'blocked' && snapshot.retryable !== false; -} - -function modeRunStatusForExecutionSnapshot(snapshot: ExecutionRunSnapshot): AgentModeRunStatus { - if (snapshot.status === 'completed') return 'succeeded'; - if (snapshot.status === 'failed') return 'failed'; - if (snapshot.status === 'cancelled') return 'cancelled'; - return 'running'; -} - -function assignmentTerminalCodeForExecutionSnapshot(snapshot: ExecutionRunSnapshot) { - return snapshot.code ?? `execution_provider_${snapshot.status}`; -} - -interface LifecycleManagedExecutionProviderAdapterOptions { - adapter: ExecutionProviderAdapter; - assignmentId: string; - leaseToken: string | null; - runnerId: string; - leaseSeconds: number; - renewLease: () => Promise; - recordModeRun: (body: Record) => Promise; - modeRunId: string; - selectedInput: Record; - capacityEnvelope: Record; - tools?: ExecutionProviderToolDescriptor[]; - agentToolCatalog?: Omit; - pollIntervalMs?: number; - maxPolls?: number; -} - -export class LifecycleManagedExecutionProviderAdapter implements ExecutionProviderAdapter { - private readonly phaseIdSeed = randomUUID(); - private phaseCounter = 0; - - constructor(private readonly options: LifecycleManagedExecutionProviderAdapterOptions) {} - - describe() { - return this.options.adapter.describe(); - } - - observe(input: ExecutionProviderObserveInput) { - return this.options.adapter.observe(input); - } - - prepare(input: ExecutionProviderInvocation) { - return this.options.adapter.prepare?.(input) - ?? Promise.resolve({ - accepted: true, - summary: 'Execution provider accepted the invocation.', - }); - } - - async start(input: ExecutionProviderInvocation): Promise { - const invocation = { - ...input, - leaseToken: input.leaseToken ?? this.options.leaseToken, - runnerId: input.runnerId || this.options.runnerId, - tools: [ - ...(input.tools ?? []), - ...(this.options.tools ?? []), - ], - }; - const preparation = await this.prepare(invocation); - if (!preparation.accepted) { - const rejected = preparation as Exclude; - const snapshot: ExecutionRunSnapshot = { - status: rejected.retryable === false ? 'failed' : 'returned', - summary: rejected.summary, - runId: typeof invocation.metadata?.runId === 'string' ? invocation.metadata.runId : invocation.assignment.id, - retryable: rejected.retryable, - code: rejected.code ?? 'execution_provider_prepare_rejected', - metadata: rejected.metadata, - }; - await this.recordSnapshot(snapshot, 'start'); - return snapshot; - } - - await this.recordSnapshot({ - status: 'running', - summary: 'Execution provider invocation started.', - runId: typeof invocation.metadata?.runId === 'string' ? invocation.metadata.runId : invocation.assignment.id, - metadata: { - source: 'execution_provider_starting', - provider: invocation.assignment.executionProviderId ?? null, - toolCount: invocation.tools?.length ?? 0, - tools: invocation.tools ?? [], - agentToolCatalog: this.options.agentToolCatalog ?? null, - workPackage: invocation.workPackage, - agent: { - slug: invocation.agent.slug, - name: invocation.agent.slug, - handler: invocation.agent.handler, - execution: invocation.agent.execution, - contextQueryCount: invocation.agent.context?.queries?.length ?? 0, - }, - workspace: invocation.workspace, - projectAgentClass: invocation.projectAgentClass ?? null, - redactedParameters: { - assignmentId: invocation.assignment.id, - projectId: invocation.assignment.projectId, - mode: invocation.capacityEnvelope.mode, - executionProviderId: invocation.assignment.executionProviderId ?? null, - runnerId: invocation.runnerId ?? null, - leaseTokenPresent: Boolean(invocation.leaseToken), - }, - }, - }, 'start'); - - let snapshot = await this.options.adapter.start(invocation); - await this.recordSnapshot(snapshot, 'start'); - - if (isRetryableReturnedSnapshot(snapshot) || !isAsyncExecutionStatus(snapshot.status) || !this.options.adapter.poll) { - return this.withCollectedDetails(snapshot); - } - - const ref = this.snapshotRef(invocation, snapshot); - const maxPolls = this.options.maxPolls ?? DEFAULT_ASYNC_MAX_POLLS; - const pollIntervalMs = this.options.pollIntervalMs ?? DEFAULT_ASYNC_POLL_INTERVAL_MS; - try { - await this.options.renewLease(); - } catch (error) { - snapshot = { - status: 'failed', - summary: 'Assignment lease renewal failed after execution provider work was accepted.', - runId: snapshot.runId ?? ref.runId, - externalRef: snapshot.externalRef ?? ref.externalRef, - externalUrl: snapshot.externalUrl ?? ref.externalUrl, - retryable: true, - code: 'assignment_lease_renewal_failed', - metadata: { - ...(snapshot.metadata ?? {}), - error: error instanceof Error ? error.message : String(error), - }, - }; - await this.recordSnapshot(snapshot, 'poll'); - return snapshot; - } - - for (let pollIndex = 0; pollIndex < maxPolls && isAsyncExecutionStatus(snapshot.status); pollIndex += 1) { - try { - await this.options.renewLease(); - } catch (error) { - snapshot = { - status: 'failed', - summary: 'Assignment lease renewal failed while execution provider work was in progress.', - runId: snapshot.runId ?? ref.runId, - externalRef: snapshot.externalRef ?? ref.externalRef, - externalUrl: snapshot.externalUrl ?? ref.externalUrl, - retryable: true, - code: 'assignment_lease_renewal_failed', - metadata: { - ...(snapshot.metadata ?? {}), - error: error instanceof Error ? error.message : String(error), - }, - }; - await this.recordSnapshot(snapshot, 'poll'); - return snapshot; - } - await sleep(pollIntervalMs); - try { - snapshot = await this.options.adapter.poll({ - ...ref, - runId: snapshot.runId ?? ref.runId, - externalRef: snapshot.externalRef ?? ref.externalRef, - externalUrl: snapshot.externalUrl ?? ref.externalUrl, - metadata: { - ...(ref.metadata ?? {}), - ...(snapshot.metadata ?? {}), - pollIndex, - }, - }); - } catch (error) { - snapshot = { - status: 'failed', - summary: 'Execution provider polling failed.', - runId: snapshot.runId ?? ref.runId, - externalRef: snapshot.externalRef ?? ref.externalRef, - externalUrl: snapshot.externalUrl ?? ref.externalUrl, - retryable: true, - code: 'execution_provider_poll_failed', - metadata: { - ...(snapshot.metadata ?? {}), - error: error instanceof Error ? error.message : String(error), - }, - }; - } - await this.recordSnapshot(snapshot, 'poll'); - } - - if (isAsyncExecutionStatus(snapshot.status)) { - snapshot = { - ...snapshot, - status: 'waiting', - retryable: true, - code: snapshot.code ?? 'execution_provider_poll_incomplete', - summary: snapshot.summary || 'Execution provider work is still in progress.', - }; - await this.recordSnapshot(snapshot, 'poll'); - } - - return this.withCollectedDetails(snapshot); - } - - poll(input: ExecutionRunRef) { - return this.options.adapter.poll?.(input) - ?? Promise.resolve({ - status: 'failed' as const, - summary: 'Execution provider does not support polling.', - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: false, - code: 'execution_provider_poll_unsupported', - }); - } - - resume(input: ExecutionRunRef) { - return this.options.adapter.resume?.(input) ?? this.poll(input); - } - - cancel(input: ExecutionRunRef & { reason: string }) { - return this.options.adapter.cancel?.(input) - ?? Promise.resolve({ - status: 'cancelled' as const, - summary: input.reason, - runId: input.runId, - externalRef: input.externalRef, - externalUrl: input.externalUrl, - retryable: false, - code: 'execution_provider_cancelled', - }); - } - - collectUsage(input: ExecutionRunRef) { - return this.options.adapter.collectUsage?.(input) ?? Promise.resolve([{ - kind: 'execution_provider_usage', - unit: 'unsupported', - amount: 0, - source: 'provider_runner', - partial: true, - metadata: { - supported: false, - reason: 'adapter_collect_usage_not_implemented', - }, - }]); - } - - collectArtifacts(input: ExecutionRunRef) { - return this.options.adapter.collectArtifacts?.(input) ?? Promise.resolve([{ - kind: 'execution_provider_artifacts', - name: 'artifact-collection-not-implemented', - metadata: { - supported: false, - reason: 'adapter_collect_artifacts_not_implemented', - }, - }]); - } - - private snapshotRef(input: ExecutionProviderInvocation, snapshot: ExecutionRunSnapshot): ExecutionRunRef { - return { - assignmentId: input.assignment.id, - executionProviderId: input.assignment.executionProviderId ?? null, - runId: snapshot.runId ?? String(input.metadata?.runId ?? input.assignment.id), - externalRef: snapshot.externalRef ?? null, - externalUrl: snapshot.externalUrl ?? null, - leaseToken: input.leaseToken, - runnerId: input.runnerId, - metadata: { - assignmentId: input.assignment.id, - provider: snapshot.metadata?.provider ?? null, - }, - }; - } - - private async recordSnapshot(snapshot: ExecutionRunSnapshot, phase: 'start' | 'poll') { - this.phaseCounter += 1; - const source = snapshot.metadata?.source === 'execution_provider_starting' - ? 'execution_provider_starting' - : 'execution_provider_adapter_lifecycle'; - const lifecyclePhase = snapshot.metadata?.source === 'execution_provider_starting' - ? 'starting' - : `${phase}_${snapshot.status}`; - await this.options.recordModeRun({ - id: `${this.options.modeRunId}:${source}:${this.phaseIdSeed}:${this.phaseCounter}`, - status: modeRunStatusForExecutionSnapshot(snapshot), - selectedInput: this.options.selectedInput, - capacityEnvelope: this.options.capacityEnvelope, - outputs: { - status: snapshot.status, - summary: snapshot.summary, - outputs: snapshot.outputs ?? {}, - usage: snapshot.usage ?? [], - artifacts: snapshot.artifacts ?? [], - externalRef: snapshot.externalRef ?? null, - externalUrl: snapshot.externalUrl ?? null, - code: assignmentTerminalCodeForExecutionSnapshot(snapshot), - metadata: snapshot.metadata ?? {}, - }, - traceRefs: { - executionRunId: snapshot.runId ?? null, - externalRef: snapshot.externalRef ?? null, - externalUrl: snapshot.externalUrl ?? null, - }, - usageActual: snapshot.usage?.length - ? { nativeUsage: { executionUsage: snapshot.usage } } - : null, - fallbackReason: isRetryableReturnedSnapshot(snapshot) ? snapshot.summary : null, - startedAt: phase === 'start' ? new Date().toISOString() : null, - completedAt: snapshot.status === 'completed' ? new Date().toISOString() : null, - failedAt: snapshot.status === 'failed' ? new Date().toISOString() : null, - metadata: { - recordKind: 'telemetry', - source, - assignmentId: this.options.assignmentId, - runnerId: this.options.runnerId, - leaseSeconds: this.options.leaseSeconds, - executionStatus: snapshot.status, - executionRunId: snapshot.runId ?? null, - externalRef: snapshot.externalRef ?? null, - externalUrl: snapshot.externalUrl ?? null, - phase, - lifecyclePhase, - }, - }); - } - - private async withCollectedDetails(snapshot: ExecutionRunSnapshot): Promise { - const runId = snapshot.runId ?? this.options.assignmentId; - const ref: ExecutionRunRef = { - assignmentId: this.options.assignmentId, - runId, - externalRef: snapshot.externalRef ?? null, - externalUrl: snapshot.externalUrl ?? null, - leaseToken: this.options.leaseToken, - runnerId: this.options.runnerId, - metadata: snapshot.metadata, - }; - const [usage, artifacts] = await Promise.all([ - this.collectUsage(ref).catch((error): ExecutionUsageActual[] => [{ - kind: 'execution_provider_usage', - unit: 'unsupported', - amount: 0, - source: 'provider_runner', - partial: true, - metadata: { - supported: false, - reason: 'collect_usage_failed', - error: error instanceof Error ? error.message : String(error), - }, - }]), - this.collectArtifacts(ref).catch((error): ExecutionArtifactRef[] => [{ - kind: 'execution_provider_artifacts', - name: 'artifact-collection-unavailable', - metadata: { - supported: false, - reason: 'collect_artifacts_failed', - error: error instanceof Error ? error.message : String(error), - }, - }]), - ]); - const normalizedUsage = snapshot.usage ?? usage; - const normalizedArtifacts = snapshot.artifacts ?? artifacts; - return { - ...snapshot, - usage: normalizedUsage, - artifacts: normalizedArtifacts, - metadata: { - ...(snapshot.metadata ?? {}), - collectedUsageCount: normalizedUsage.length, - collectedArtifactCount: normalizedArtifacts.length, - }, - }; - } -} diff --git a/src/provider/execution/execution-support.ts b/src/provider/execution/execution-support.ts deleted file mode 100644 index 6529ae4c..00000000 --- a/src/provider/execution/execution-support.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { AgentTreeDxAdapter } from '../../agents/runtime/runtime-types.ts'; -import { createHash } from 'node:crypto'; -import { createUnifiedChangeset } from '@treeseed/sdk/treedx'; -import { assertRelativeContentPath } from '../../agents/content/content-artifacts.ts'; -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; -import { record, stringValue } from '../configuration/value-utils.ts'; - -const execFileAsync = promisify(execFile); - -export async function writeProviderContentArtifact(input: { - repoRoot: string; - relativePath: string; - content: string; - commitMessage: string; - treeDx: AgentTreeDxAdapter | null; - workspaceId: string | null; - baseCommitSha: string | null; - baseRef: string | null; -}) { - assertRelativeContentPath(input.repoRoot, input.relativePath); - if (!input.treeDx || !input.workspaceId || !input.baseCommitSha || !input.baseRef) { - throw new Error('TreeDX writable workspace is required for provider content artifact writes.'); - } - const existing = await input.treeDx.readWorkspaceFile({ workspaceId: input.workspaceId, path: input.relativePath }).catch(() => null); - const before = existing ? stringValue(record(existing).content, record(record(existing).file).content) ?? null : null; - const patch = createUnifiedChangeset([{ path: input.relativePath, before, after: input.content }]); - const patchSha256 = createHash('sha256').update(patch).digest('hex'); - const changeset = await input.treeDx.applyWorkspaceChangeset({ - workspaceId: input.workspaceId, - body: { contract: 'treedx.changeset/v1', baseCommitSha: input.baseCommitSha, baseRef: input.baseRef, - patch, patchSha256, idempotencyKey: createHash('sha256').update(`${input.workspaceId}:${patchSha256}`).digest('hex'), - expectedDestinationRefHead: input.baseCommitSha }, - }); - const commit = await input.treeDx.commitWorkspace({ - workspaceId: input.workspaceId, - message: input.commitMessage, - body: { author: { name: 'TreeSeed Agent Provider', email: 'agent-provider@treeseed.local' } }, - }); - return { - branchName: null, - commitMessage: input.commitMessage, - worktreePath: input.repoRoot, - commitSha: stringValue(record(commit).commitSha, record(record(commit).payload).commitSha), - changedPaths: [input.relativePath], - changeset, - }; -} - -export async function inspectProviderRepository(repoRoot: string) { - try { - const { stdout: branchStdout } = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { - cwd: repoRoot, - env: process.env, - }); - const { stdout: changedStdout } = await execFileAsync('git', ['status', '--short'], { - cwd: repoRoot, - env: process.env, - }); - const { stdout: shaStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { - cwd: repoRoot, - env: process.env, - }); - const changedPaths = changedStdout - .split('\n') - .map((entry) => entry.trim().replace(/^[MADRCU?! ]+\s+/, '').trim()) - .filter(Boolean); - return { - branchName: branchStdout.trim() || null, - changedPaths, - commitSha: shaStdout.trim() || null, - summary: `Inspected provider workspace with ${changedPaths.length} changed path(s).`, - }; - } catch (error) { - return { - branchName: null, - changedPaths: [], - commitSha: null, - summary: `Provider workspace inspection failed: ${error instanceof Error ? error.message : String(error)}`, - }; - } -} - -export async function runProviderVerification(input: { repoRoot: string; commands: string[]; cwd?: string }) { - if (!input.commands.length) { - return { - status: 'completed' as const, - summary: 'No verification commands were configured for this assignment.', - stdout: '', - stderr: '', - }; - } - const stdout: string[] = []; - const stderr: string[] = []; - for (const command of input.commands) { - try { - const result = await execFileAsync('/bin/bash', ['-lc', command], { - cwd: input.cwd ?? input.repoRoot, - env: process.env, - maxBuffer: 10 * 1024 * 1024, - }); - stdout.push(result.stdout); - stderr.push(result.stderr); - } catch (error) { - return { - status: 'failed' as const, - summary: `Verification command failed: ${command}`, - stdout: stdout.join('\n'), - stderr: error && typeof error === 'object' && 'stderr' in error - ? String((error as { stderr?: string }).stderr ?? '') - : String(error), - errorCategory: 'execution_error' as const, - }; - } - } - return { - status: 'completed' as const, - summary: `Verification completed for ${input.commands.length} command(s).`, - stdout: stdout.join('\n'), - stderr: stderr.join('\n'), - }; -} diff --git a/src/provider/execution/executor-loader.ts b/src/provider/execution/executor-loader.ts new file mode 100644 index 00000000..3e574a56 --- /dev/null +++ b/src/provider/execution/executor-loader.ts @@ -0,0 +1,27 @@ +import { pathToFileURL } from 'node:url'; +import { isAbsolute, resolve } from 'node:path'; +import type { ProviderHostRuntimeConfig } from '../configuration/config.ts'; +import type { AgentExecutor, AgentExecutorModule } from './contracts.ts'; + +const cache = new Map>(); + +async function loadModule(specifier: string) { + const key = isAbsolute(specifier) || specifier.startsWith('.') ? pathToFileURL(resolve(specifier)).href : specifier; + let loaded = cache.get(key); + if (!loaded) { + loaded = import(key).then((value) => value as AgentExecutorModule); + cache.set(key, loaded); + } + const module = await loaded; + if (typeof module.createAgentExecutor !== 'function') throw new Error('Agent executor module must export createAgentExecutor().'); + return module; +} + +export async function resolveAgentExecutor(config: ProviderHostRuntimeConfig, executionProviderId: string): Promise { + if (!config.executorModule) return null; + const executor = await (await loadModule(config.executorModule)).createAgentExecutor({ executionProviderId, environment: config.environment }); + if (!executor || typeof executor.execute !== 'function' || typeof executor.observe !== 'function') { + throw new Error('Agent executor module returned an invalid executor.'); + } + return executor; +} diff --git a/src/provider/execution/kernel-bridge-support.ts b/src/provider/execution/kernel-bridge-support.ts deleted file mode 100644 index 0ce9a16c..00000000 --- a/src/provider/execution/kernel-bridge-support.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { record, stringValue } from '../configuration/value-utils.ts'; - -export function assignmentModeRunId(assignment: Record, selectedInput: Record, capacityEnvelope: Record) { - const decisionInput = record(assignment.decisionInput); - return [ - stringValue(assignment.id) ?? 'assignment', - stringValue(assignment.mode, decisionInput.mode, capacityEnvelope.mode) ?? 'planning', - stringValue(assignment.agentId, decisionInput.agentId, selectedInput.agentSlug, selectedInput.agentId) ?? 'agent', - stringValue(assignment.handlerId, decisionInput.handlerId, selectedInput.handlerId) ?? 'handler', - ].join(':'); -} - -export function assignmentAgentTools(allowed: string[], allowedOutputs: Record) { - const publications = allowedOutputs.publishedSignals; - return Array.isArray(publications) && publications.length > 0 - ? [...new Set([...allowed, 'treeseed.publish_signal'])] - : allowed; -} diff --git a/src/provider/execution/kernel-bridge.ts b/src/provider/execution/kernel-bridge.ts deleted file mode 100644 index 1bd69e3b..00000000 --- a/src/provider/execution/kernel-bridge.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { AgentSdk } from '@treeseed/sdk/sdk'; -import { redactedProviderAssignmentCapabilityHandles,validateProviderAssignmentCapabilityHandles,type ProviderAssignment } from '@treeseed/sdk/agent-capacity'; -import { loadAllAgentSpecs } from '../../agents/support/spec-loader.ts'; -import { AgentKernel } from '../../agents/kernel/agents/agent-kernel.ts'; -import type { AgentTreeDxAdapter, ExecutionProviderAdapter } from '../../agents/runtime/runtime-types.ts'; -import { assignmentProjectContext, materializeAssignmentProject, providerProjectSiteRoot, providerProjectTreeDxOptions, releaseMaterializedAssignmentProject } from '../projects/projects-core/project-materialization.ts'; -import { createAssignmentToolCatalog } from '../commerce/catalog/assignment-tool-catalog.ts'; -import { loadAssignmentRawAgentSpecs } from '../capacity/assignments/assignment-agent-spec-loader.ts'; -import { createProviderMessageRecorder } from '../reporting/message-recorder.ts'; -import { codexEventMessage } from '../reporting/codex-event-message.ts'; -import { createAssignmentExecutionProviderAdapter, resolveAssignmentExecutionProvider } from '../capacity/providers/execution-provider-selection.ts'; -import { assignmentWorkflowOperationHandles, assignmentWorkspaceAccessMode, resolveAssignmentAgentToolPolicy } from '../capacity/assignments/assignment-tool-policy.ts'; -import { assignmentScopedTreeDxOptions, createAssignmentTreeDxAdapter } from '../treedx/graph/treedx-context-adapter.ts'; -import { LifecycleManagedExecutionProviderAdapter } from './execution-lifecycle.ts'; -import { inspectProviderRepository, runProviderVerification, writeProviderContentArtifact } from './execution-support.ts'; -import { recordEarlyModeRun } from '../reporting/mode-run-reporter.ts'; -import type { ProviderAssignmentExecutionInput } from '../operations/runner-contracts.ts'; -import { record, stringValue } from '../configuration/value-utils.ts'; -import { withTimeout } from './promise-timeout.ts'; -import { buildKernelProviderAssignment } from '../capacity/assignments/kernel-assignment.ts'; -import { assignmentTreeDxProxyHandle } from '../../agents/kernel/runtime/runtime-helpers.ts'; -import { assignmentAgentTools, assignmentModeRunId } from './kernel-bridge-support.ts'; -export async function prepareAssignmentKernelBridge(input: ProviderAssignmentExecutionInput & { - assignmentId: string; - membershipId: string; - stateVersion: number; - decisionInput: Record; - decisionPayload: Record; - capacityEnvelope: Record; - projectId: string; - agentSlug: string; -}): Promise< - | { ready: false; terminalResult: unknown } - | { - ready: true; - terminalResult: null; - kernel: Pick; - typedAssignment: ProviderAssignment; - workspaceMode: string | null; - modeRunId: string; - assignmentTreeDxAdapter: AgentTreeDxAdapter | null; - releaseAssignmentResources: ((outcome: 'completed' | 'returned' | 'failed' | 'expired' | 'cancelled') => Promise) | null; - } -> { - const { assignmentId, membershipId, stateVersion, decisionInput, decisionPayload, capacityEnvelope, projectId, agentSlug } = input; - const workspaceMode = assignmentWorkspaceAccessMode(input.assignment); - const governedExactBaseRef = stringValue(record(decisionPayload.input).exactBaseRef, decisionPayload.exactBaseRef); - const treedxProxyHandle = assignmentTreeDxProxyHandle(input.assignment as unknown as ProviderAssignment) ?? {}; - const assignedProject = assignmentProjectContext(input.assignment); - const project = assignedProject - ? await withTimeout(materializeAssignmentProject(input.config, assignedProject, { assignmentId, workspaceAccessMode: workspaceMode, requiresRepository: Boolean(governedExactBaseRef) && !Object.keys(treedxProxyHandle).length, exactRef: governedExactBaseRef }), 60_000, `Assignment project materialization exceeded 60000ms for ${assignmentId}.`) - : null; - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment: input.assignment, - selectedInput: decisionPayload, - capacityEnvelope, - status: 'running', - fallbackReason: '', - outputs: { - status: 'assignment_project_materialized', - summary: 'Provider runner materialized the exact project context carried by the governed assignment.', - metadata: { source: 'provider_runner_assignment_project_materialized', projectId, agentSlug, projectFound: Boolean(project), repository: project?.repository ?? null }, - }, - metadata: { source: 'provider_runner_assignment_project_materialized', projectId, agentSlug, projectFound: Boolean(project), repository: project?.repository ?? null }, - }); - if (!project?.repository.ok) { - const body = { - leaseToken: input.leaseToken, - runnerId: input.runnerId, - reason: assignedProject ? `Project ${projectId} could not be materialized from its assignment context.` : `Assignment ${assignmentId} does not contain canonical project context.`, - code: assignedProject ? 'provider_project_materialization_failed' : 'assignment_project_context_missing', - retryable: true, - metadata: { - projectId, - agentSlug, - }, }; - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment: input.assignment, - selectedInput: decisionPayload, - capacityEnvelope, - status: 'failed', - fallbackReason: body.code, - metadata: { - projectId, - agentSlug, - repository: project?.repository ?? null, - }, - }); - const terminalResult = input.client.returnAssignment - ? await input.client.returnAssignment(assignmentId, body) - : await input.client.failAssignment(assignmentId, { - ...body, - message: body.reason, - }); - if (project) await releaseMaterializedAssignmentProject(input.config, project); - return { ready: false, terminalResult }; - } - const projectSiteRoot = providerProjectSiteRoot(project, project.repository.path); - const projectTreeDx = providerProjectTreeDxOptions(project, input.treeDx); - const scopedTreeDx = assignmentScopedTreeDxOptions(projectTreeDx, treedxProxyHandle); - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment: input.assignment, - selectedInput: decisionPayload, - capacityEnvelope, - status: 'running', - fallbackReason: '', - outputs: { - status: 'repository_ready', - summary: 'Provider runner resolved repository, site root, and TreeDX proxy scope.', - metadata: { - source: 'provider_runner_repository_ready', - projectId, - agentSlug, - repositoryPath: project.repository.path, - projectSiteRoot, - treeDx: { - hasProxyHandle: Object.keys(treedxProxyHandle).length > 0, - workspaceId: stringValue(treedxProxyHandle.workspaceId), - repositoryId: stringValue(treedxProxyHandle.repositoryId, treedxProxyHandle.repoId), - }, - }, - }, - metadata: { - source: 'provider_runner_repository_ready', - projectId, - agentSlug, - repositoryPath: project.repository.path, - projectSiteRoot, - }, - }); - const localSdk = AgentSdk.createLocal({ - repoRoot: projectSiteRoot, - treeDx: scopedTreeDx, - }); - const capabilityHandles = redactedProviderAssignmentCapabilityHandles(record(input.assignment.capabilityHandles)); - const handleFallback = validateProviderAssignmentCapabilityHandles({ - assignment: { - ...input.assignment, - id: assignmentId, - teamId: stringValue(input.assignment.teamId, decisionInput.teamId, capacityEnvelope.teamId) ?? '', - projectId, - mode: stringValue(input.assignment.mode, decisionInput.mode, capacityEnvelope.mode) ?? 'planning', - capabilityHandles, - } as ProviderAssignment, - capabilityHandles, - }); - if (handleFallback) { - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment: input.assignment, - selectedInput: decisionPayload, - capacityEnvelope, - status: 'failed', - fallbackReason: handleFallback.code, - metadata: handleFallback.metadata, - }); - const terminalResult = await input.client.failAssignment(assignmentId, { - leaseToken: input.leaseToken, - runnerId: input.runnerId, - code: handleFallback.code, - message: handleFallback.reason, - retryable: handleFallback.retryable, - metadata: handleFallback.metadata, - }); - await releaseMaterializedAssignmentProject(input.config, project); - return { ready: false, terminalResult }; - } - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment: input.assignment, - selectedInput: decisionPayload, - capacityEnvelope, - status: 'running', - fallbackReason: '', - outputs: { - status: 'capability_handles_validated', - summary: 'Provider runner validated assignment capability handles and workspace access mode.', - metadata: { - source: 'provider_runner_capability_handles_validated', - projectId, - agentSlug, - workspaceMode, - capabilityHandles, - }, - }, - metadata: { - source: 'provider_runner_capability_handles_validated', - projectId, - agentSlug, - workspaceMode, - }, - }); - const assignmentTreeDxAdapter = ['workspace_write', 'brokered_workspace', 'trusted_direct', 'context_only'].includes(workspaceMode ?? '') - ? createAssignmentTreeDxAdapter({ - config: input.config, - projectId, - assignmentId, - treedxProxyHandle, - client: input.client, - mode: stringValue(input.assignment.mode, capacityEnvelope.mode) ?? 'planning', - capacityEnvelope, - decisionPayload, - runnerId: input.runnerId, - }) - : null; - const assignmentMetadata = record(input.assignment.metadata); - const useAssignmentTreeDxSpecLoader = input.assignment.synthesizedFrom === 'workday_demand'; - const providerNoop = async () => ({ ok: true, payload: null }); - const providerCreateMessage = createProviderMessageRecorder({ - recorder: input.client, - assignmentId, - mode: stringValue(input.assignment.mode, capacityEnvelope.mode) ?? 'planning', - selectedInput: decisionPayload, - capacityEnvelope, - runnerId: input.runnerId, - }); - const sdk = { - repoRoot: localSdk.repoRoot, - listRawAgentSpecs: async (options?: { enabled?: boolean }) => { - if (useAssignmentTreeDxSpecLoader) { - return await loadAssignmentRawAgentSpecs({ - treeDx: assignmentTreeDxAdapter, - assignmentId, - agentSlug, - agentContentPath: stringValue(assignmentMetadata.agentContentPath, record(decisionPayload).agentContentPath), - workspaceId: stringValue(treedxProxyHandle.workspaceId), - contentRoot: stringValue(assignmentMetadata.contentRoot, record(decisionPayload).contentRoot), - client: input.client, - mode: stringValue(input.assignment.mode, capacityEnvelope.mode) ?? 'planning', - capacityEnvelope, - decisionPayload, - runnerId: input.runnerId, - options, - }) ?? []; - } - return localSdk.listRawAgentSpecs(options); - }, - listAgentSpecs: localSdk.listAgentSpecs.bind(localSdk), - scopeForAgent(agent: Parameters[0]) { - const scoped = localSdk.scopeForAgent(agent) as unknown as Record; - const overrides: Record = { - recordRun: providerNoop, - ackMessage: providerNoop, - upsertCursor: providerNoop, - releaseAllLeases: providerNoop, - createMessage: providerCreateMessage, - }; - return new Proxy(scoped, { - get(target, property, receiver) { - if (property in overrides) return overrides[property]; - const value = Reflect.get(target, property, receiver); - return typeof value === 'function' ? value.bind(target) : value; - }, - }); - }, - recordRun: providerNoop, - ackMessage: providerNoop, - upsertCursor: providerNoop, - releaseAllLeases: providerNoop, - createMessage: providerCreateMessage, - } as unknown as AgentSdk; - const agentSpecLoad = input.kernel ? { specs: [] } : await loadAllAgentSpecs(sdk); - const agentSpec = agentSpecLoad.specs.find((spec) => spec.slug === agentSlug); - const assignmentMode = stringValue(input.assignment.mode, capacityEnvelope.mode) === 'acting' ? 'acting' : 'planning'; - const assignmentDecisionInput = record(input.assignment.decisionInput); - const assignmentActivityType = stringValue( - assignmentMetadata.activityType, - decisionPayload.activityType, - record(assignmentDecisionInput.metadata).activityType, - record(assignmentDecisionInput.input).activityType, - ); - const assignmentAgentPolicy = resolveAssignmentAgentToolPolicy(agentSpec, assignmentMode, assignmentActivityType); - const executionProvider = input.kernel - ? (input.config.executionProviders ?? []).find((provider) => provider.id === stringValue(input.assignment.executionProviderId)) ?? null - : resolveAssignmentExecutionProvider({ - assignment: input.assignment, - executionProviders: input.config.executionProviders ?? [], - defaultExecutionProviderId: input.config.defaultExecutionProviderId, - }); - const assignmentToolCatalog = createAssignmentToolCatalog({ - agentTools: assignmentAgentTools( - assignmentAgentPolicy?.tools.allowed ?? [], - record(input.assignment.allowedOutputs), - ), - projectId, - assignmentId, - agentSlug, - agentContentPath: stringValue(assignmentMetadata.agentContentPath, decisionPayload.agentContentPath), - environment: stringValue(input.assignment.environment, input.config.environment) ?? 'local', - treedxProxyHandle, - workspaceMode, - contentRoot: stringValue(assignmentMetadata.contentRoot, decisionPayload.contentRoot, assignedProject?.architecture?.contentPath), - permissionProjection: record(assignmentAgentPolicy).permissionProjection as never, - allowedProposalTypes: Array.isArray(record(input.assignment.allowedOutputs).proposalTypes) - ? record(input.assignment.allowedOutputs).proposalTypes as string[] : [], - researchNetworkPolicy: assignmentAgentPolicy?.activityProfiles?.[assignmentAgentPolicy.activityType ?? assignmentMode]?.permissions?.network, - providerResearchSourcePolicy: executionProvider?.researchSourcePolicy, - worktreeRoot: null, - providerManagesWorktree: executionProvider?.adapter === 'codex' - && assignmentAgentPolicy?.execution.sandboxMode === 'workspace_write', - allowedPaths: assignmentAgentPolicy?.execution.allowedPaths ?? [], - forbiddenPaths: assignmentAgentPolicy?.execution.forbiddenPaths ?? [], - }); - const assignmentToolDescriptors = assignmentToolCatalog.descriptors; - const baseExecutionAdapter: ExecutionProviderAdapter | null = input.kernel ? null : createAssignmentExecutionProviderAdapter({ - selection: executionProvider!.adapter, - executionProvider, - repoRoot: project.repository.path, - jira: input.config.jira, - githubIssues: input.config.githubIssues, - discord: input.config.discord, - accessToken: input.config.accessToken, - apiBaseUrl: input.config.marketUrl, - onCodexEvent: async (event) => { await providerCreateMessage(codexEventMessage(event)); }, - researchSourcePolicy: executionProvider?.researchSourcePolicy, - workflow: { - dispatchWorkflowOperation: input.client.dispatchAssignmentWorkflowOperation - ? async (workflowAssignmentId, operationId, body) => { - const response = await input.client.dispatchAssignmentWorkflowOperation!(workflowAssignmentId, operationId, body); - const responseRecord = record(response); - return { - ok: responseRecord.ok === undefined ? true : responseRecord.ok === true, - payload: record(responseRecord.payload ?? responseRecord), - }; - } - : undefined, - getWorkflowOperationRun: input.client.getAssignmentWorkflowRun - ? async (workflowAssignmentId, runId) => { - const response = await input.client.getAssignmentWorkflowRun!(workflowAssignmentId, runId); - const responseRecord = record(response); - return { ok: responseRecord.ok === undefined ? true : responseRecord.ok === true, - payload: record(responseRecord.payload ?? responseRecord) }; - } - : undefined, - }, - }); - const execution = input.kernel ? null : new LifecycleManagedExecutionProviderAdapter({ - adapter: baseExecutionAdapter!, - assignmentId, - leaseToken: input.leaseToken, - runnerId: input.runnerId, - leaseSeconds: input.leaseSeconds, - renewLease: input.renewLease, - recordModeRun: (body) => input.client.createAssignmentModeRun(assignmentId, body), - modeRunId: assignmentModeRunId(input.assignment, decisionPayload, capacityEnvelope), - selectedInput: decisionPayload, - capacityEnvelope, - tools: assignmentToolDescriptors, - agentToolCatalog: { - requested: assignmentToolCatalog.requested, - exposed: assignmentToolCatalog.exposed, - omitted: assignmentToolCatalog.omitted, - }, - pollIntervalMs: input.executionLifecycle?.pollIntervalMs, - maxPolls: input.executionLifecycle?.maxPolls, - }); - const kernel = input.kernel ?? new AgentKernel(sdk, project.repository.path, { - treeDx: assignmentTreeDxAdapter, - execution: execution!, - mutations: { - writeArtifact: async (artifact) => writeProviderContentArtifact({ - repoRoot: project.repository.path, - relativePath: artifact.relativePath, - content: artifact.content, - commitMessage: artifact.commitMessage, - treeDx: assignmentTreeDxAdapter, - workspaceId: stringValue(treedxProxyHandle.workspaceId), - baseCommitSha: stringValue(treedxProxyHandle.baseCommitSha), - baseRef: stringValue(treedxProxyHandle.baseRef), - }), - }, - repository: { - inspectBranch: async () => inspectProviderRepository(project.repository.path), - }, - verification: { - runChecks: async ({ commands, cwd }) => runProviderVerification({ - repoRoot: project.repository.path, - commands, - cwd, - }), - }, - notifications: { - deliver: async ({ agent, runId, recipients, subject, body }) => { - await providerCreateMessage({ - type: 'agent.notification', - payload: { - agentSlug: agent.slug, - runId, - recipients, - subject, - body, - }, - relatedModel: 'agent', - relatedId: agent.slug, - actor: 'agent', - }); - return { - status: 'completed', - summary: recipients.length - ? `Recorded notification for ${recipients.length} recipient(s).` - : 'Recorded notification event without direct recipients.', - deliveredCount: recipients.length, - }; - }, - }, - research: { - research: async ({ questionId, reason, runId }) => { - if (!assignmentTreeDxAdapter) return { status: 'waiting', summary: 'Research requires an assignment-scoped TreeDX proxy handle.', markdown: '', sources: [] }; - const graphResult = record(await assignmentTreeDxAdapter.buildContext({ repoId: '', query: questionId, body: { limit: 10, purpose: 'agent_research', reason, runId } })); - const items = [graphResult.items, graphResult.results, graphResult.entries, graphResult.matches].find(Array.isArray) ?? []; - return { - status: 'completed', - summary: `Prepared TreeDX-backed research for ${questionId}.`, - markdown: [ - '# Research Summary', - '', - `Question: ${questionId}`, - `Reason: ${reason ?? 'not provided'}`, - `Run: ${runId}`, - '', - items.length ? 'Relevant TreeDX context:' : 'No ranked TreeDX context was available. The question is recorded for follow-up.', - ...items.map((item) => { - const entry = record(item); - return `- ${String(entry.title ?? entry.id ?? 'context')}`; - }), - ].join('\n'), - sources: items.map((item) => { - const entry = record(item); - return String(entry.id ?? entry.title ?? ''); - }).filter(Boolean), - }; - }, - }, - operations: { - runOperation: async ({ request }) => { - const operationId = stringValue(record(request.input).workflowOperationId, record(request.input).operationId, request.operation); - const handleId = stringValue(record(request.input).workflowOperationHandleId, record(request.input).handleId); - const handle = assignmentWorkflowOperationHandles({ ...input.assignment, capabilityHandles }) - .find((entry) => stringValue(entry.operationId) === operationId && (!handleId || stringValue(entry.id) === handleId)); - if (!handle || !input.client.dispatchAssignmentWorkflowOperation) { - return { - operation: request.operation, - status: 'waiting', - summary: 'Provider assignment operation requires an assignment-scoped workflow operation handle.', - changedPaths: [], - stagedPaths: [], - commandsRun: [], - artifacts: [], - error: { - code: 'assignment_workflow_operation_denied', - message: 'No active workflow operation handle is available for this assignment.', - retryable: false, - }, - metadata: { operationId, handleId }, - }; - } - const result = record(await input.client.dispatchAssignmentWorkflowOperation(assignmentId, operationId ?? '', { - leaseToken: input.leaseToken, - handleId: stringValue(handle.id), - inputs: record(request.input).inputs ?? record(request.input), - wait: record(request.input).wait === true, - })); - return { - operation: request.operation, - status: 'completed', - summary: `Dispatched workflow operation ${operationId}.`, - changedPaths: [], - stagedPaths: [], - commandsRun: ['workflow_operation_dispatch'], - artifacts: [], - metadata: { - workflowOperationId: operationId, - workflowOperationHandleId: stringValue(handle.id), - dispatch: record(result.payload ?? result), - }, - }; - }, - }, - }); - const typedAssignment = buildKernelProviderAssignment({ assignment: input.assignment, assignmentId, membershipId, stateVersion, decisionInput, decisionPayload, capacityEnvelope, projectId, agentSlug, workspaceMode, treedxProxyHandle, capabilityHandles }); - return { ready: true, terminalResult: null, kernel, typedAssignment, workspaceMode, assignmentTreeDxAdapter, - modeRunId: assignmentModeRunId(input.assignment, decisionPayload, capacityEnvelope), - releaseAssignmentResources: async (outcome: 'completed' | 'returned' | 'failed' | 'expired' | 'cancelled') => { - try { - if (baseExecutionAdapter?.releaseAssignmentResources) await baseExecutionAdapter.releaseAssignmentResources({ assignmentId, outcome }); - } finally { - await releaseMaterializedAssignmentProject(input.config, project); - } - }, - }; -} diff --git a/src/provider/execution/promise-timeout.ts b/src/provider/execution/promise-timeout.ts deleted file mode 100644 index 71b1d8e1..00000000 --- a/src/provider/execution/promise-timeout.ts +++ /dev/null @@ -1,14 +0,0 @@ -export async function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { - let timer: ReturnType | null = null; - try { - return await Promise.race([ - promise, - new Promise((_resolve, reject) => { - timer = setTimeout(() => reject(new Error(message)), timeoutMs); - }), - ]); - } finally { - if (timer) clearTimeout(timer); - } -} - diff --git a/src/provider/lifecycle/entrypoint.ts b/src/provider/lifecycle/entrypoint.ts index 6ec01bc4..16774dd1 100644 --- a/src/provider/lifecycle/entrypoint.ts +++ b/src/provider/lifecycle/entrypoint.ts @@ -114,10 +114,6 @@ async function main() { if (['manager', 'runner'].includes(role) && mode === 'live' && !diagnostic && !config.manifestPath) { throw new Error('treeseed.capacity-provider.yaml (or TREESEED_CAPACITY_PROVIDER_MANIFEST) is required; legacy single-team provider credentials are no longer supported.'); } - if (requireConnection(role, mode) && !diagnostic && process.env.TREESEED_PROVIDER_CLASS !== 'platform-operation') { - const { materializeCodexAuthFromEnv } = await import('../../agents/adapters/accounts/codex-auth.ts'); - await materializeCodexAuthFromEnv(process.env); - } if (role === 'version') { emit(okPayload('version', { package: '@treeseed/agent', diff --git a/src/provider/lifecycle/lifecycle.ts b/src/provider/lifecycle/lifecycle.ts index 0479f431..612e3422 100644 --- a/src/provider/lifecycle/lifecycle.ts +++ b/src/provider/lifecycle/lifecycle.ts @@ -1,198 +1,92 @@ import { access, mkdir } from 'node:fs/promises'; import { constants } from 'node:fs'; import type { ProviderSupplyOffer } from '@treeseed/sdk/capacity-provider/contracts'; -import type { MinimumAssignmentDuration } from '@treeseed/sdk/capacity-provider/contracts'; import type { ProviderConnectionRuntimeContext, ProviderHostRuntimeConfig } from '../configuration/config.ts'; import { discoverProviderBudgets } from '../configuration/budgets.ts'; import { discoverProviderCapabilities } from '../configuration/capabilities.ts'; -import { createProviderMarketClient } from '../coordination/client.ts'; -import { compileConnectionAvailability } from '../projects/projects-core/availability-projection.ts'; +import { createProviderControlPlaneClient } from '../coordination/client.ts'; import { ProviderLocalCapacityStore } from '../capacity/capacity-core/local-capacity-store.ts'; import { observeProviderDiskCapacity } from '../runtime/disk-capacity.ts'; -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - export function okPayload(role: string, payload: Record = {}) { - return { - ok: true, - role, - ...payload, - }; + return { ok: true, role, ...payload }; } export async function checkProviderHealth(config: ProviderHostRuntimeConfig) { - let dataDirWritable = false; - try { - await mkdir(config.dataDir, { recursive: true }); - await access(config.dataDir, constants.W_OK); - dataDirWritable = true; - } catch { - dataDirWritable = false; - } - const diskCapacity = dataDirWritable - ? await observeProviderDiskCapacity({ path: config.dataDir, env: config.env }) - : null; + await mkdir(config.dataDir, { recursive: true }); + const writable = await access(config.dataDir, constants.W_OK).then(() => true, () => false); + const disk = writable ? await observeProviderDiskCapacity({ path: config.dataDir, env: config.env }) : null; return okPayload('healthcheck', { - status: dataDirWritable && diskCapacity?.ok ? 'ok' : 'degraded', + status: writable && disk?.ok ? 'ok' : 'degraded', environment: config.environment, - dataDir: config.dataDir, - dataDirWritable, - diskCapacity, + dataDirWritable: writable, + disk, manifestConfigured: Boolean(config.manifestPath), - codexReady: Boolean(config.codexAuthFile || config.codexAuthJsonB64), + executorConfigured: Boolean(config.executorModule), }); } export async function buildProviderPlan(config: ProviderHostRuntimeConfig) { - const base = { - environment: config.environment, + return okPayload('plan', { + mode: 'plan', dataDir: config.dataDir, capabilities: discoverProviderCapabilities(config), budgets: discoverProviderBudgets(config), + executorConfigured: Boolean(config.executorModule), redactedEnv: config.redactedEnv, - }; - return okPayload('plan', { - ...base, - mode: 'plan', }); } -interface ProviderManagerAvailability { +export interface ProviderAvailabilityProjection { offer?: ProviderSupplyOffer; - executionProviders?: Array<{ - id: string; - adapter: string; - nativeLimits: Record; - minimumAssignmentDuration?: MinimumAssignmentDuration; - capabilities?: string[]; - status?: 'available' | 'unavailable'; - nativeUnit?: string; - quotaVisibility?: string; - maxConcurrentRunners?: number; - activeRunners?: number; - observations?: Record; - }>; + executionProviders: Array>; activeRunners?: number; constraints?: Record; } -function positiveFinite(values: Array) { - return values.filter((value): value is number => typeof value === 'number' && Number.isFinite(value) && value > 0); -} - -function availableAgentSeconds(config: ProviderHostRuntimeConfig, availability?: ProviderManagerAvailability) { - const declared = positiveFinite(availability?.executionProviders?.map((provider) => Number(provider.nativeLimits.availableAgentSeconds)) ?? []); - return declared.length > 0 ? Math.min(...declared) : Math.max(1, config.maxConcurrentRunners) * 86_400; -} - -function availabilitySessionKey(config: ProviderConnectionRuntimeContext) { - return `${config.connectionId}|${config.teamId}|${config.providerId}`; -} - -async function publishAvailabilitySession( +export async function publishProviderAvailability( config: ProviderConnectionRuntimeContext, - client: ReturnType, - availability: ProviderManagerAvailability | undefined, - capabilities: string[], - localState: ProviderLocalCapacityStore, + availability: ProviderAvailabilityProjection, + localState = new ProviderLocalCapacityStore(config.dataDir), ) { - const budgets = discoverProviderBudgets(config); - const scopedProjection = availability?.offer - ? compileConnectionAvailability({ connection: { id: config.connectionId, marketUrl: config.marketUrl, teamId: config.teamId, providerId: config.providerId, membershipId: config.membershipId, membershipCredentialRef: 'runtime://redacted', membershipCredentialId: 'runtime-redacted', offer: availability.offer }, executionProviders: availability.executionProviders ?? [], hostMaxConcurrentRunners: config.maxConcurrentRunners, defaultExecutionProviderId: config.defaultExecutionProviderId }) - : { capabilities, executionProviders: availability?.executionProviders ?? [], maxConcurrentRunners: config.maxConcurrentRunners }; - const projection = { - ...scopedProjection, - executionProviders: scopedProjection.executionProviders.map((provider) => ({ - ...provider, - preferred: provider.id === config.defaultExecutionProviderId, - })), - }; - const maxConcurrentRunners = projection.maxConcurrentRunners; - const nativeLimits = { - ...budgets, - availableAgentSeconds: availableAgentSeconds(config, availability), - maxConcurrentRunners, - }; + const client = createProviderControlPlaneClient(config); + const key = `${config.connectionId}|${config.teamId}|${config.providerId}`; const snapshot = { ttlSeconds: 90, environment: config.environment, status: 'open', - availableFrom: availability?.offer?.availability?.availableFrom ?? new Date().toISOString(), - availableUntil: availability?.offer?.availability?.availableUntil ?? null, - executionProviders: projection.executionProviders, - capabilities: projection.capabilities, - nativeLimits, - runnerPressure: { - activeRunners: availability?.activeRunners ?? 0, - maxConcurrentRunners, - maxConcurrentWorkdays: config.maxConcurrentWorkdays, - }, - constraints: { - outboundOnly: true, - dataDir: config.dataDir, - availableAgentSeconds: nativeLimits.availableAgentSeconds, - maxConcurrentRunners, - ...(availability?.constraints ?? {}), - }, + executionProviders: availability.executionProviders, + capabilities: discoverProviderCapabilities(config), + nativeLimits: discoverProviderBudgets(config), + runnerPressure: { activeRunners: availability.activeRunners ?? 0, maxConcurrentRunners: config.maxConcurrentRunners }, + constraints: { outboundOnly: true, ...availability.constraints }, metadata: { source: '@treeseed/agent/provider-manager', sourceClosureDigest: config.env.TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST ?? null, }, }; - const key = availabilitySessionKey(config); - const existing = await localState.session(key); - if (existing) { - try { - const refreshed = await client.refreshAvailabilitySession(existing.id, { ...snapshot, expectedSequence: existing.sequence }); - const payload = record(refreshed.payload); - await localState.saveSession(key, { id: existing.id, sequence: Number(payload.sequence ?? existing.sequence + 1) }); - return refreshed; - } catch { + const prior = await localState.session(key); + const session = prior + ? await client.refreshAvailabilitySession(prior.id, { ...snapshot, expectedSequence: prior.sequence }).catch(async () => { await localState.removeSession(key); - } - } - const created = await client.createAvailabilitySession(snapshot); - const payload = record(created.payload); - const id = String(payload.id ?? ''); - if (!id) throw new Error('Capacity provider availability session response did not include a session id.'); - await localState.saveSession(key, { id, sequence: Number(payload.sequence ?? 1) }); - return created; -} - -export async function runManagerSkeleton(config: ProviderConnectionRuntimeContext, options: { mode?: 'plan' | 'live'; availability?: ProviderManagerAvailability; localState?: ProviderLocalCapacityStore } = {}) { - if (options.mode !== 'plan') { - const client = createProviderMarketClient(config); - const capabilities = [...new Set(discoverProviderCapabilities(config).flatMap((capability) => [ - capability.id, - ...(Array.isArray(capability.metadata?.capabilityAliases) - ? capability.metadata.capabilityAliases.map((entry) => String(entry ?? '').trim()).filter(Boolean) - : []), - ]).filter(Boolean))]; - const availabilitySession = await publishAvailabilitySession(config, client, options.availability, capabilities, options.localState ?? new ProviderLocalCapacityStore(config.dataDir)); - return okPayload('manager', { - action: 'availability-session-published', - mode: 'live', - availabilitySession: availabilitySession.payload, - }); - } - const plan = await buildProviderPlan(config); + return client.createAvailabilitySession(snapshot); + }) + : await client.createAvailabilitySession(snapshot); + await localState.saveSession(key, { id: session.id, sequence: session.sequence }); return okPayload('manager', { - action: 'availability-plan', - mode: 'plan', - plan, + action: 'availability-session-published', + session: { id: session.id, sequence: session.sequence, status: session.status }, }); } export function buildProviderRunnerPlan(config: ProviderHostRuntimeConfig) { return okPayload('runner', { mode: 'plan', + executorConfigured: Boolean(config.executorModule), flow: [ - 'claim a manager-created durable leased-assignment dispatch', - 'record provider-local mode-run telemetry', - 'complete or fail assignment without widening scope', + 'read API-issued assignment lease', + 'execute through trusted Agent executor', + 'report usage and exact terminal receipt', ], - assignmentRequest: { capabilities: discoverProviderCapabilities(config).map((capability) => capability.id) }, }); } diff --git a/src/provider/operations/runner-contracts.ts b/src/provider/operations/runner-contracts.ts deleted file mode 100644 index 2dfb47b8..00000000 --- a/src/provider/operations/runner-contracts.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { AgentSdkTreeDxOptions } from '@treeseed/sdk/sdk'; -import type { AgentKernel } from '../../agents/kernel/agents/agent-kernel.ts'; -import type { ProviderConnectionRuntimeContext } from '../configuration/config.ts'; -import type { ProviderAssignmentClient } from '../coordination/lease-client.ts'; - -export interface ProviderAssignmentExecutionInput { - config: ProviderConnectionRuntimeContext; - client: ProviderAssignmentClient; - assignment: Record; - leaseToken: string | null; - runnerId: string; - leaseSeconds: number; - renewLease: () => Promise; - signal?: AbortSignal; - kernel?: Pick; - treeDx?: AgentSdkTreeDxOptions; - executionLifecycle?: { - pollIntervalMs?: number; - maxPolls?: number; - }; - onAssignmentResourcesPrepared?: ( - release: ((outcome: 'completed' | 'returned' | 'failed' | 'expired' | 'cancelled') => Promise) | null, - ) => void; -} diff --git a/src/provider/operations/runner-lifecycle.ts b/src/provider/operations/runner-lifecycle.ts deleted file mode 100644 index 1b756b8a..00000000 --- a/src/provider/operations/runner-lifecycle.ts +++ /dev/null @@ -1,336 +0,0 @@ -import type { AgentSdkTreeDxOptions } from '@treeseed/sdk/sdk'; -import type { AgentKernel } from '../../agents/kernel/agents/agent-kernel.ts'; -import type { ProviderConnectionRuntimeContext } from '../configuration/config.ts'; -import { providerAssignmentClientWithTerminalBoundary, type ProviderAssignmentClient } from '../coordination/lease-client.ts'; -import { recordEarlyModeRun } from '../reporting/mode-run-reporter.ts'; -import { reportProviderRuntimeEvent } from '../reporting/runtime-event-reporter.ts'; -import { record, stringValue } from '../configuration/value-utils.ts'; -import { releaseTerminalAssignmentResources, runProviderAssignment } from './runner.ts'; -import { providerErrorDiagnostic,providerErrorIsRetryable } from '../reporting/error-diagnostics.ts'; - -export function isTerminalProviderAssignmentObservation(value: unknown): boolean { - const envelope = record(value); - const observed = record(envelope.payload ?? envelope.assignment ?? envelope); - const leaseState = stringValue(observed.leaseState); - return leaseState === 'released' - || ['completed', 'failed', 'cancelled'].includes(stringValue(observed.status) ?? ''); -} - -export interface ProviderAssignmentCancellationRequest { code:string;reason:string } -export function providerAssignmentCancellationRequest(value:unknown):ProviderAssignmentCancellationRequest|null { - const envelope=record(value); - const observed=record(envelope.payload??envelope.assignment??envelope); - const metadata=record(observed.metadata); - if(observed.executionKind!=='conversation'||metadata.cancellationRequested!==true)return null; - const code=stringValue(metadata.cancellationReason)??'communication_cancellation_requested'; - return {code,reason:code==='discussion_archived'?'The source Discussion was archived.':'Communication assignment cancellation was requested.'}; -} - -export function reportProviderLeaseRenewalFailure(input: { - terminalizing: () => boolean; - assignmentId: string; - runnerId: string; - error: unknown; -}) { - if (input.terminalizing()) return false; - console.error(JSON.stringify({ - level: 'error', - event: 'provider.assignment_lease_renew_failed', - assignmentId: input.assignmentId, - runnerId: input.runnerId, - message: input.error instanceof Error ? input.error.message : String(input.error), - })); - return true; -} - -export function createSingleFlightLeaseRenewal(attempt: () => Promise) { - let inFlight: Promise | null = null; - return () => { - if (inFlight) return inFlight; - const current = attempt().finally(() => { - if (inFlight === current) inFlight = null; - }); - inFlight = current; - return current; - }; -} - -export async function runProviderRunnerOnce(input: { - config: ProviderConnectionRuntimeContext; - client: ProviderAssignmentClient; - runnerId?: string; - kernel?: Pick; - treeDx?: AgentSdkTreeDxOptions; - executionLifecycle?: { - pollIntervalMs?: number; - maxPolls?: number; - }; - onLeaseRenewed?: (assignment: Record) => Promise; - leasedAssignment: unknown; -}) { - const runnerId = input.runnerId ?? `provider-runner-${process.pid}`; - const leaseStartedAt = new Date().toISOString(); - console.error(JSON.stringify({ - level: 'info', - event: 'provider.runner.durable_dispatch_started', - runnerId, - leaseStartedAt, - dispatchSource: 'provider-manager', - })); - const leased = input.leasedAssignment; - const leasedRecord = record(leased); - const assignment = record(leasedRecord.payload ?? leasedRecord.assignment); - if (!Object.keys(assignment).length) { - const leaseDiagnostics = record(leasedRecord.leaseDiagnostics ?? leasedRecord.diagnostics); - return { - ok: true, - role: 'runner', - mode: 'live', - assigned: 0, - result: null, - ...(Object.keys(leaseDiagnostics).length ? { leaseDiagnostics } : {}), - }; - } - console.error(JSON.stringify({ - level: 'info', - event: 'provider.runner.assignment_leased', - runnerId, - assignmentId: stringValue(assignment.id), - agentId: stringValue(assignment.agentId), - projectId: stringValue(assignment.projectId), - mode: stringValue(assignment.mode), - leaseStartedAt, - leasedAt: new Date().toISOString(), - })); - const leaseToken = stringValue(leasedRecord.leaseToken, assignment.leaseToken); - const leaseSeconds = Number(leasedRecord.leaseSeconds ?? 300); - await reportProviderRuntimeEvent({ client: input.client, assignmentId: String(assignment.id ?? ''), event: { - id: `runner-leased-${Date.now()}`, eventType: 'provider.runner.assignment_leased', status: 'active', component: 'provider-runner', - message: 'Provider runner leased the assignment.', createdAt: new Date().toISOString(), context: { runnerId, leaseStartedAt, leaseSeconds }, - } }); - let renewTimer: ReturnType | null = null; - let authorityTimer: ReturnType | null = null; - let terminalizing = false; - const executionAuthority = new AbortController(); - const cancellationState:{current:ProviderAssignmentCancellationRequest|null}={current:null}; - const stopLeaseRenewal = () => { - terminalizing = true; - if (renewTimer) clearInterval(renewTimer); - if (authorityTimer) clearInterval(authorityTimer); - renewTimer = null; - authorityTimer = null; - }; - const renewLeaseAttempt = async () => { - if (terminalizing || !leaseToken || !input.client.renewAssignment) return; - const assignmentId = String(assignment.id ?? ''); - try { - let renewed: unknown; - for (let attempt = 1; attempt <= 3; attempt += 1) { - try { - renewed = await input.client.renewAssignment(assignmentId, { leaseToken, runnerId, leaseSeconds }); - break; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (attempt === 3 || !/fetch failed|timed out|econnreset|socket|temporarily unavailable/iu.test(message)) throw error; - } - } - const renewedRecord = record(renewed); - const renewedAssignment = record(renewedRecord.payload ?? renewedRecord.assignment ?? renewedRecord); - Object.assign(assignment, renewedAssignment); - cancellationState.current=providerAssignmentCancellationRequest(renewedAssignment)??cancellationState.current; - if(cancellationState.current&&!executionAuthority.signal.aborted)executionAuthority.abort(Object.assign(new Error(cancellationState.current.reason),{code:cancellationState.current.code,status:409})); - await input.onLeaseRenewed?.(assignment); - await reportProviderRuntimeEvent({ client: input.client, assignmentId, event: { - id: `lease-renewed-${Date.now()}`, eventType: 'provider.assignment.lease_renewed', status: 'completed', component: 'lease', - message: 'Assignment lease renewed.', createdAt: new Date().toISOString(), context: { runnerId, leaseExpiresAt: assignment.leaseExpiresAt }, - } }); - } catch (error) { - if (terminalizing) return; - if (input.client.assignment) { - try { - if (isTerminalProviderAssignmentObservation(await input.client.assignment(assignmentId))) return; - } catch { - // Preserve the original renewal failure when authoritative observation is unavailable. - } - } - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Assignment lease cannot be renewed for ${assignmentId || ''} by ${runnerId}: ${message}`); - } - }; - const renewLease = createSingleFlightLeaseRenewal(renewLeaseAttempt); - if (leaseToken && input.client.renewAssignment) { - await renewLease(); - const renewEveryMs = Math.max(15_000, Math.min(leaseSeconds * 500, 120_000)); - renewTimer = setInterval(() => { - void renewLease().catch((error) => { - // Completion can begin after renewLease's authoritative observation but - // before this detached rejection handler runs. Re-check the terminal - // boundary here so a successfully terminalized assignment cannot emit a - // false active-lease failure. - const reported = reportProviderLeaseRenewalFailure({ - terminalizing: () => terminalizing, - assignmentId: String(assignment.id ?? ''), - runnerId, - error, - }); - if (reported) void reportProviderRuntimeEvent({ client: input.client, assignmentId: String(assignment.id ?? ''), event: { - id: `lease-renew-failed-${Date.now()}`, eventType: 'provider.assignment.lease_renew_failed', status: 'failed', component: 'lease', - message: error instanceof Error ? error.message : String(error), createdAt: new Date().toISOString(), context: { runnerId }, - } }); - }); - }, renewEveryMs); - } - if (input.client.assignment) { - authorityTimer = setInterval(() => { - if (terminalizing) return; - void input.client.assignment!(String(assignment.id ?? '')).then((observed) => { - cancellationState.current=providerAssignmentCancellationRequest(observed)??cancellationState.current; - if(cancellationState.current&&!executionAuthority.signal.aborted){ - executionAuthority.abort(Object.assign(new Error(cancellationState.current.reason),{code:cancellationState.current.code,status:409})); - return; - } - if (!isTerminalProviderAssignmentObservation(observed) || terminalizing) return; - executionAuthority.abort(new Error('Assignment execution authority was revoked by authoritative terminal state.')); - stopLeaseRenewal(); - }).catch(() => undefined); - }, 5_000); - authorityTimer.unref?.(); - } - let result; - const lifecycleClient = providerAssignmentClientWithTerminalBoundary(input.client, stopLeaseRenewal); - let releaseAssignmentResources: ((outcome: 'completed' | 'returned' | 'failed' | 'expired' | 'cancelled') => Promise) | null = null; - try { - const decisionInput = record(assignment.decisionInput); - const selectedInput = record(decisionInput.input); - const capacityEnvelope = record(assignment.capacityEnvelope); - await recordEarlyModeRun({ - client: input.client, - assignmentId: stringValue(assignment.id) ?? '', - assignment, - selectedInput, - capacityEnvelope, - status: 'running', - fallbackReason: '', - startedAt: new Date().toISOString(), - outputs: { - status: 'preparing', - summary: 'Provider runner leased the assignment and is preparing TreeDX context and execution-provider input.', - metadata: { - source: 'provider_runner_assignment_leased', - runnerId, - leaseSeconds, - leaseExpiresAt: stringValue(assignment.leaseExpiresAt), - }, - }, - traceRefs: { - assignmentId: stringValue(assignment.id) ?? null, - runnerId, - leaseToken: leaseToken ? '' : null, - }, - metadata: { - source: 'provider_runner_assignment_leased', - runnerId, - leaseSeconds, - leaseStartedAt, - }, - }); - await reportProviderRuntimeEvent({ client: input.client, assignmentId: String(assignment.id ?? ''), event: { - id: `execution-started-${Date.now()}`, eventType: 'provider.execution.started', status: 'active', component: 'execution-provider', - message: 'Execution provider invocation started.', createdAt: new Date().toISOString(), context: { runnerId, executionProviderId: assignment.executionProviderId }, - } }); - result = await runProviderAssignment({ - config: input.config, - client: lifecycleClient, - assignment, - leaseToken, - runnerId, - leaseSeconds, - renewLease, - kernel: input.kernel, - treeDx: input.treeDx, - executionLifecycle: input.executionLifecycle, - signal: executionAuthority.signal, - onAssignmentResourcesPrepared: (release) => { - releaseAssignmentResources = release; - }, - }); - await reportProviderRuntimeEvent({ client: input.client, assignmentId: String(assignment.id ?? ''), event: { - id: `execution-completed-${Date.now()}`, eventType: 'provider.execution.completed', status: 'completed', component: 'execution-provider', - message: 'Execution provider invocation completed.', createdAt: new Date().toISOString(), context: { runnerId, executionProviderId: assignment.executionProviderId }, - } }); - } catch (error) { - const assignmentId = stringValue(assignment.id) ?? ''; - const message = cancellationState.current?.reason??(error instanceof Error ? error.message : String(error)); - const diagnostic = providerErrorDiagnostic(error, 'assignment_processing'); - const retryable = cancellationState.current?false:providerErrorIsRetryable(error); - const failureCode = cancellationState.current?.code??diagnostic.code??'provider_assignment_processing_failed'; - console.error(JSON.stringify({ - level: 'error', - event: 'provider.runner.assignment_processing_failed', - assignmentId, - runnerId, - message, - diagnostic, - })); - await reportProviderRuntimeEvent({ client: input.client, assignmentId, event: { - id: `execution-failed-${Date.now()}`, eventType: 'provider.execution.failed', status: 'failed', component: 'execution-provider', - message, createdAt: new Date().toISOString(), context: { runnerId, executionProviderId: assignment.executionProviderId, diagnostic }, - } }); - let failureTelemetryError: unknown = null; - try { - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment, - selectedInput: record(record(assignment.decisionInput).input), - capacityEnvelope: record(assignment.capacityEnvelope), - status: 'failed', - fallbackReason: 'provider_assignment_processing_failed', - outputs: { - status: 'provider_assignment_processing_failed', - summary: message, - metadata: { source: 'provider_runner_assignment_processing_failed', runnerId, diagnostic }, - }, - metadata: { source: 'provider_runner_assignment_processing_failed', runnerId, message, diagnostic }, - }); - } catch (telemetryError) { - failureTelemetryError = telemetryError; - console.error(JSON.stringify({ - level: 'error', - event: 'provider.runner.required_telemetry_delivery_failed', - assignmentId, - runnerId, - message: telemetryError instanceof Error ? telemetryError.message : String(telemetryError), - })); - } - const lifecycleRequest = { - leaseToken, - runnerId, - reason: message, - code: failureCode, - retryable, - metadata: { - source: '@treeseed/agent/provider-runner', - telemetryDeliveryFailed: Boolean(failureTelemetryError), - telemetryDeliveryError: failureTelemetryError instanceof Error ? failureTelemetryError.message : failureTelemetryError ? String(failureTelemetryError) : null, - diagnostic, - }, - }; - stopLeaseRenewal(); - result = retryable && input.client.returnAssignment - ? await input.client.returnAssignment(assignmentId, lifecycleRequest) - : await input.client.failAssignment(assignmentId, { ...lifecycleRequest, message }); - } finally { - stopLeaseRenewal(); - } - await releaseTerminalAssignmentResources(result, releaseAssignmentResources); - return { - ok: true, - role: 'runner', - mode: 'live', - assigned: 1, - assignmentId: stringValue(assignment.id), - taskId: stringValue(assignment.taskId, assignment.id), - result, - }; -} diff --git a/src/provider/operations/runner.ts b/src/provider/operations/runner.ts index 763de552..b4a2f922 100644 --- a/src/provider/operations/runner.ts +++ b/src/provider/operations/runner.ts @@ -1,208 +1,53 @@ -import type { AgentKernel } from '../../agents/kernel/agents/agent-kernel.ts'; -import type { TreeDxProxyHandle } from '@treeseed/sdk/agent-capacity'; -import { - deriveAgentCapacityEnvelopeFromAssignment, - deriveDecisionExecutionInputFromAssignment, -} from '@treeseed/sdk/agent-capacity'; -import type { ProviderConnectionRuntimeContext } from '../configuration/config.ts'; -import { discoverProviderCapabilities } from '../configuration/capabilities.ts'; -import { record, stringValue } from '../configuration/value-utils.ts'; -import { recordEarlyModeRun } from '../reporting/mode-run-reporter.ts'; -import { reportProviderAssignmentResult } from '../capacity/assignments/assignment-result-reporter.ts'; -import type { ProviderAssignmentExecutionInput } from './runner-contracts.ts'; -import { prepareAssignmentKernelBridge } from '../execution/kernel-bridge.ts'; -import { validateWorkdayAssignmentAuthority, workdayAssignmentAuthorityProjection } from '../capacity/assignments/workday-assignment-authority.ts'; +import type { ProviderProtocolClient } from '@treeseed/sdk/capacity-provider'; +import type { AgentExecutor, AgentExecutionResult } from '../execution/contracts.ts'; - - - -export function providerRunnerCapabilities(config: ProviderConnectionRuntimeContext) { - const discovered = discoverProviderCapabilities(config); - return [...new Set(discovered.flatMap((capability) => [ - capability.id, - ...(Array.isArray(capability.metadata?.capabilityAliases) - ? capability.metadata.capabilityAliases.map((entry) => String(entry ?? '').trim()).filter(Boolean) - : []), - ]).filter(Boolean))]; +function record(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } -export function providerAssignmentLeaseSeconds(config: ProviderConnectionRuntimeContext) { - const configured = Number(config.env.TREESEED_PROVIDER_ASSIGNMENT_LEASE_SECONDS ?? process.env.TREESEED_PROVIDER_ASSIGNMENT_LEASE_SECONDS ?? ''); - if (Number.isFinite(configured) && configured > 0) return Math.max(30, Math.min(Math.floor(configured), 3600)); - return config.environment === 'local' ? 900 : 300; +function text(...values: unknown[]) { + return values.find((value) => typeof value === 'string' && value.trim()) as string | undefined; } -export async function releaseTerminalAssignmentResources( - reported: unknown, - release: ((outcome: 'completed' | 'returned' | 'failed' | 'expired' | 'cancelled') => Promise) | null, -) { - const reportEnvelope = record(reported); - const reportedAssignment = record(reportEnvelope.payload ?? reportEnvelope.assignment ?? reportEnvelope); - const status = stringValue(reportedAssignment.status); - if (!release || !status || !['completed', 'returned', 'failed', 'expired','cancelled'].includes(status)) return false; - await release(status as 'completed' | 'returned' | 'failed' | 'expired' | 'cancelled'); - return true; +export interface ProviderAssignmentRunInput { + client: Pick; + executor: AgentExecutor; + assignment: Record; + leaseToken: string; + runnerId: string; + signal?: AbortSignal; } -export async function refreshAssignmentAccessToken(config: ProviderConnectionRuntimeContext, assignment?: Record) { - if (!config.accessTokenProvider) return config; - const envelope = record(assignment?.capacityEnvelope); - const budget = record(envelope.budget); - const budgetTime = record(budget.time); - const deadlineAt = Date.parse(stringValue(budgetTime.authorityDeadlineAt, budgetTime.hardDeadlineAt, budget.deadline) ?? ''); - const minimumValidityMs = Number.isFinite(deadlineAt) - ? Math.max(5 * 60_000, deadlineAt - Date.now() + 30_000) - : Math.max(5 * 60_000, providerAssignmentLeaseSeconds(config) * 1_000); - const accessToken = (await config.accessTokenProvider(minimumValidityMs)).trim(); - if (!accessToken) throw new Error('Capacity provider access-token refresh returned an empty token.'); - return { ...config, accessToken }; +async function reportUsage(input: ProviderAssignmentRunInput, assignmentId: string, result: AgentExecutionResult) { + for (const [index, usage] of (result.usage ?? []).entries()) { + await input.client.reportAssignmentUsage(assignmentId, { leaseToken: input.leaseToken, runnerId: input.runnerId, ...usage }, `usage:${assignmentId}:${input.runnerId}:${index}`); + } } -export async function runProviderAssignment(input: ProviderAssignmentExecutionInput) { - input = { ...input, config: await refreshAssignmentAccessToken(input.config, input.assignment) }; - const assignmentId = stringValue(input.assignment.id) ?? ''; - const workdayAuthorityDiagnostics = validateWorkdayAssignmentAuthority(input.assignment); - if (workdayAuthorityDiagnostics.length > 0) { - return input.client.failAssignment(assignmentId, { - leaseToken: input.leaseToken, - runnerId: input.runnerId, - code: 'workday_assignment_authority_invalid', - message: 'Provider rejected incomplete or conflicting API-issued workday authority before execution.', - retryable: false, - metadata: { diagnostics: workdayAuthorityDiagnostics }, - }); - } - const workdayAuthority = workdayAssignmentAuthorityProjection(input.assignment); - const membershipId = stringValue(input.assignment.membershipId); - const stateVersion = Number(input.assignment.stateVersion); - const decisionInput = record(input.assignment.decisionInput); - const decisionPayload = record(decisionInput.input); - const capacityEnvelope = record(input.assignment.capacityEnvelope); - const projectId = stringValue(input.assignment.projectId, decisionInput.projectId, capacityEnvelope.projectId); - const agentSlug = stringValue(input.assignment.agentId, decisionInput.agentId, decisionPayload.agentSlug, decisionPayload.agentId); - const governanceProvenanceMissing = !membershipId || !Number.isInteger(stateVersion) || stateVersion < 1; - if (governanceProvenanceMissing || !projectId || !agentSlug) { - const fallbackReason = governanceProvenanceMissing - ? 'assignment_governance_provenance_missing' - : 'assignment_missing_project_or_agent'; - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment: input.assignment, - selectedInput: decisionPayload, - capacityEnvelope, - status: 'failed', - fallbackReason, - metadata: { membershipId, stateVersion: input.assignment.stateVersion ?? null, projectId, agentSlug }, - }); - return input.client.failAssignment(assignmentId, { - leaseToken: input.leaseToken, - runnerId: input.runnerId, - code: fallbackReason, - message: governanceProvenanceMissing - ? 'Provider assignment requires membershipId and a positive stateVersion.' - : 'Provider assignment requires projectId and agentId.', - retryable: false, - }); - } - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment: input.assignment, - selectedInput: decisionPayload, - capacityEnvelope, - status: 'running', - fallbackReason: '', - outputs: { - status: 'provider_preparation_started', - summary: 'Provider runner started assignment preparation before AgentKernel execution.', - metadata: { - source: 'provider_runner_preparation_started', - projectId, - agentSlug, - runnerId: input.runnerId, - }, - }, - metadata: { - source: 'provider_runner_preparation_started', - projectId, - agentSlug, - runnerId: input.runnerId, - workdayAuthority, - }, - }); - const prepared = await prepareAssignmentKernelBridge({ - ...input, - assignmentId, - membershipId, - stateVersion, - decisionInput, - decisionPayload, - capacityEnvelope, - projectId, - agentSlug, - }); - if (!prepared.ready) return prepared.terminalResult; - const { kernel, typedAssignment, workspaceMode, modeRunId, assignmentTreeDxAdapter, releaseAssignmentResources } = prepared; - input.onAssignmentResourcesPrepared?.(releaseAssignmentResources); - let fallbackOutput: Record | null = null; - await recordEarlyModeRun({ - client: input.client, - assignmentId, - assignment: input.assignment, - selectedInput: decisionPayload, - capacityEnvelope, - status: 'running', - fallbackReason: '', - outputs: { - status: 'agent_kernel_starting', - summary: 'Provider runner is handing the prepared assignment to AgentKernel.', - metadata: { - source: 'provider_runner_agent_kernel_starting', - projectId, - agentSlug, - handlerId: typedAssignment.handlerId, - workspaceMode, - }, - }, - metadata: { - source: 'provider_runner_agent_kernel_starting', - projectId, - agentSlug, - handlerId: typedAssignment.handlerId, - workspaceMode, - }, - }); - const modeResult = await kernel.runAssignment({ - signal: input.signal, - assignment: typedAssignment, - modeRunId, - capacityEnvelope: deriveAgentCapacityEnvelopeFromAssignment(typedAssignment), - decisionInput: deriveDecisionExecutionInputFromAssignment(typedAssignment), - leaseToken: input.leaseToken, - runnerId: input.runnerId, - readiness: record(input.assignment.readiness ?? record(decisionInput.metadata).readiness) as Parameters[0]['readiness'], - treedxProxyHandle: (typedAssignment.treedxProxyHandle ?? null) as TreeDxProxyHandle | null, - recordModeRun: (body) => input.client.createAssignmentModeRun(assignmentId, body as unknown as Record), - recordFallbackOutput: async (output) => { - fallbackOutput = output; - return output; - }, - }); - const reported = await reportProviderAssignmentResult({ - client: input.client, - assignmentId, - assignment: typedAssignment, - modeResult, - capacityEnvelope, - leaseToken: input.leaseToken, - runnerId: input.runnerId, - projectId, - agentSlug, - fallbackOutput, - closeWorkspace: assignmentTreeDxAdapter && stringValue(typedAssignment.treedxProxyHandle?.workspaceId) - ? () => assignmentTreeDxAdapter.closeWorkspace({ workspaceId: stringValue(typedAssignment.treedxProxyHandle?.workspaceId) ?? '' }) - : null, - }); - return reported; +export async function runProviderAssignment(input: ProviderAssignmentRunInput) { + const assignmentId = text(input.assignment.id); + if (!assignmentId) throw new Error('Catalogued assignment lease omitted its stable id.'); + await input.client.startAssignmentExecution(assignmentId, { leaseToken: input.leaseToken, runnerId: input.runnerId, executorId: input.executor.id }); + let result: AgentExecutionResult; + try { + result = await input.executor.execute({ assignment: input.assignment, assignmentId, leaseToken: input.leaseToken, runnerId: input.runnerId, signal: input.signal }); + } catch (error) { + result = { status: 'failed', code: 'agent_executor_failed', summary: error instanceof Error ? error.message : String(error), retryable: true }; + } + await reportUsage(input, assignmentId, result); + if (result.status === 'returned') { + return input.client.returnAssignment(assignmentId, { leaseToken: input.leaseToken, runnerId: input.runnerId, code: result.code ?? 'agent_executor_returned', reason: result.summary, retryable: result.retryable ?? true }); + } + if (result.status === 'failed') { + return input.client.failAssignment(assignmentId, { leaseToken: input.leaseToken, runnerId: input.runnerId, code: result.code ?? 'agent_executor_failed', message: result.summary, retryable: result.retryable ?? false }); + } + await input.client.startAssignmentCloseout(assignmentId, { leaseToken: input.leaseToken, runnerId: input.runnerId }); + const completion = { + leaseToken: input.leaseToken, + runnerId: input.runnerId, + summary: { text: result.summary }, + output: { ...record(result.outputs), artifacts: result.artifacts ?? [] }, + }; + await input.client.preflightAssignmentCompletion(assignmentId, completion); + return input.client.completeAssignment(assignmentId, completion); } diff --git a/src/provider/projects/projects-core/availability-projection.ts b/src/provider/projects/projects-core/availability-projection.ts deleted file mode 100644 index 78081212..00000000 --- a/src/provider/projects/projects-core/availability-projection.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { ProviderConnectionConfig } from '@treeseed/sdk/capacity-provider/contracts'; -import type { MinimumAssignmentDuration } from '@treeseed/sdk/capacity-provider/contracts'; - -interface ExecutionProviderAvailability { - id: string; - adapter: string; - nativeLimits: Record; - minimumAssignmentDuration?: MinimumAssignmentDuration; - capabilities?: string[]; - status?: 'available' | 'unavailable'; - nativeUnit?: string; - quotaVisibility?: string; - maxConcurrentRunners?: number; - activeRunners?: number; - observations?: Record; - lanes?: Array<{ - id: string; - purpose: 'communication' | 'operation'; - maxConcurrentRunners: number; - minimumAssignmentDuration?: MinimumAssignmentDuration; - capabilities?: string[]; - nativeLimits?: Record; - }>; -} - -function intersection(values: string[] | undefined, allowed: Set) { - return values?.filter((value) => allowed.has(value)); -} - -function narrowedNativeLimits(limits: Record, maxConcurrentRunners: number) { - const declared = Number(limits.maxConcurrentRunners); - return { - ...limits, - ...(Number.isFinite(declared) ? { maxConcurrentRunners: Math.min(declared, maxConcurrentRunners) } : {}), - }; -} - -/** Projects provider-global facts into the strict capability scope offered to one team. */ -export function compileConnectionAvailability(input: { - connection: ProviderConnectionConfig; - executionProviders: ExecutionProviderAvailability[]; - hostMaxConcurrentRunners: number; - defaultExecutionProviderId?: string; -}) { - const allowed = new Set(input.connection.offer.capabilities); - const maxConcurrentRunners = Math.max(0, Math.min( - input.hostMaxConcurrentRunners, - input.connection.offer.maxConcurrentRunners ?? input.hostMaxConcurrentRunners, - )); - return { - capabilities: [...allowed].sort(), - maxConcurrentRunners, - executionProviders: input.executionProviders.map((provider) => ({ - ...provider, - preferred: provider.id === input.defaultExecutionProviderId, - nativeLimits: narrowedNativeLimits(provider.nativeLimits, maxConcurrentRunners), - ...(provider.capabilities ? { capabilities: intersection(provider.capabilities, allowed) } : {}), - ...(provider.lanes ? { - lanes: provider.lanes.map((lane) => ({ - ...lane, - maxConcurrentRunners: Math.min(lane.maxConcurrentRunners, maxConcurrentRunners), - ...(lane.nativeLimits ? { nativeLimits: narrowedNativeLimits(lane.nativeLimits, maxConcurrentRunners) } : {}), - ...(lane.capabilities ? { capabilities: intersection(lane.capabilities, allowed) } : {}), - })), - } : {}), - })), - }; -} diff --git a/src/provider/projects/projects-core/project-materialization.ts b/src/provider/projects/projects-core/project-materialization.ts deleted file mode 100644 index 28d97e94..00000000 --- a/src/provider/projects/projects-core/project-materialization.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { execFile } from 'node:child_process'; -import { existsSync, realpathSync } from 'node:fs'; -import { mkdir,readdir,rm } from 'node:fs/promises'; -import { dirname, relative, resolve, sep } from 'node:path'; -import { promisify } from 'node:util'; -import type { AgentSdkTreeDxOptions } from '@treeseed/sdk/sdk'; -import { resolveRepositoryIdentity } from '@treeseed/sdk'; -import type { ProviderHostRuntimeConfig } from '../../configuration/config.ts'; - -const execFileAsync = promisify(execFile); - -export interface AssignmentProjectContext { - id: string; - slug: string; - name: string; - architecture?: Record | null; - agentSpecs?: { root?: string; testsRoot?: string }; - repository: { - provider?: string; - role?: string | null; - owner?: string; - name?: string; - defaultBranch?: string; - currentBranch?: string | null; - cloneUrl: string; - checkoutPath?: string | null; - submodulePath?: string | null; - webUrl?: string | null; - }; -} - -export interface MaterializedAssignmentProject extends AssignmentProjectContext { - repository: AssignmentProjectContext['repository'] & { - ok: boolean; - path: string; - branch: string; - commitSha: string | null; - materialization: 'context' | 'clone'; - mirrorPath?: string; - error?: string; - }; -} - -export interface AssignmentProjectMaterializationOptions { - workspaceAccessMode?: string | null; - requiresRepository?: boolean; - exactRef?: string | null; - assignmentId?: string | null; -} - -function architectureString(project: AssignmentProjectContext, key: string) { - const value = project.architecture?.[key]; - return typeof value === 'string' && value.trim() ? value.trim() : null; -} - -function normalizedRelativePath(value: string | null) { - if (!value || value === '.') return ''; - return value.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, ''); -} - -function safeSegment(value: string) { - return value.replace(/[^A-Za-z0-9_.-]+/gu, '-').replace(/^-+|-+$/gu, '') || 'unknown'; -} - -function repositoryPaths(config: ProviderHostRuntimeConfig, project: AssignmentProjectContext, assignmentId: string) { - const identity = resolveRepositoryIdentity(project.repository.cloneUrl); - const repositoryKey = safeSegment(identity.canonicalKey); - return { - mirror: resolve(config.dataDir, 'repositories', repositoryKey, 'mirror.git'), - checkout: resolve(config.dataDir, 'assignments', safeSegment(assignmentId), 'checkout'), - }; -} - -function contextPath(config: ProviderHostRuntimeConfig, assignmentId: string) { - return resolve(config.dataDir, 'assignment-contexts', safeSegment(assignmentId)); -} - -export function assertProviderOwnedRepositoryPath(dataDir: string, candidate: string) { - const root = existsSync(dataDir) ? realpathSync(dataDir) : resolve(dataDir); - let ancestor = resolve(candidate); - const missing: string[] = []; - while (!existsSync(ancestor)) { - missing.unshift(ancestor.split(sep).at(-1)!); - const parent = dirname(ancestor); - if (parent === ancestor) break; - ancestor = parent; - } - const target = resolve(existsSync(ancestor) ? realpathSync(ancestor) : ancestor, ...missing); - const child = relative(root, target); - if (child === '..' || child.startsWith(`..${sep}`) || child.startsWith(sep)) { - throw new Error('Assignment repository path escapes capacity-provider custody.'); - } -} - -function requiresProviderRepository(workspaceAccessMode: string | null | undefined) { - return workspaceAccessMode === undefined - || workspaceAccessMode === null - || workspaceAccessMode === 'full_workspace_no_credentials' - || workspaceAccessMode === 'trusted_direct'; -} - -async function runGit(args: string[], cwd?: string) { - return execFileAsync('git', args, { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, maxBuffer: 8 * 1024 * 1024 }); -} - -async function gitSha(repoRoot: string) { - try { return (await runGit(['rev-parse', 'HEAD'], repoRoot)).stdout.trim() || null; } catch { return null; } -} - -async function ensureContextRepository(path: string) { - if (!existsSync(resolve(path, '.git'))) { - await runGit(['init', '--initial-branch=context'], path); - await runGit(['config', 'user.name', 'Agent Provider'], path); - await runGit(['config', 'user.email', 'provider@localhost'], path); - await runGit(['commit', '--allow-empty', '-m', 'Initialize isolated assignment context'], path); - } - return gitSha(path); -} - -async function resolveGitRef(repoRoot: string, ref: string) { - try { - return (await runGit(['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`], repoRoot)).stdout.trim() || null; - } catch { - return null; - } -} - -async function ensureExactRef(repoRoot: string, exactRef: string | null | undefined, allowFetch: boolean) { - const requested = exactRef?.trim(); - if (!requested) return null; - let resolved = await resolveGitRef(repoRoot, requested); - if (resolved || !allowFetch) return resolved; - try { await runGit(['fetch', 'origin', requested], repoRoot); } catch { /* Fall through to the complete branch-ref fetch. */ } - resolved = await resolveGitRef(repoRoot, requested); - if (resolved) return resolved; - try { await runGit(['fetch', 'origin', '+refs/heads/*:refs/remotes/origin/*', '--prune'], repoRoot); } catch { /* Report the immutable ref failure below. */ } - return resolveGitRef(repoRoot, requested); -} - -export async function materializeAssignmentProject( - config: ProviderHostRuntimeConfig, - project: AssignmentProjectContext, - options: AssignmentProjectMaterializationOptions = {}, -): Promise { - const assignmentId = options.assignmentId?.trim() || `manual-${project.id}`; - await mkdir(config.dataDir, { recursive: true }); - if (!options.requiresRepository && !requiresProviderRepository(options.workspaceAccessMode)) { - const path = contextPath(config, assignmentId); - assertProviderOwnedRepositoryPath(config.dataDir, path); - await mkdir(path, { recursive: true }); - const commitSha = await ensureContextRepository(path); - return { - ...project, - repository: { - ...project.repository, - ok: true, - path, - branch: project.repository.currentBranch || project.repository.defaultBranch || 'context', - commitSha, - materialization: 'context', - }, - }; - } - const paths = repositoryPaths(config, project, assignmentId); - assertProviderOwnedRepositoryPath(config.dataDir, paths.mirror); - assertProviderOwnedRepositoryPath(config.dataDir, paths.checkout); - const path = paths.checkout; - const branch = project.repository.currentBranch || project.repository.defaultBranch || 'main'; - await mkdir(dirname(paths.mirror), { recursive: true }); - await mkdir(dirname(path), { recursive: true }); - try { - if (!existsSync(paths.mirror)) { - await runGit(['clone', '--mirror', project.repository.cloneUrl, paths.mirror]); - } else { - await runGit(['remote', 'set-url', 'origin', project.repository.cloneUrl], paths.mirror); - await runGit(['fetch', 'origin', '+refs/heads/*:refs/heads/*', '--prune'], paths.mirror); - } - if (!existsSync(resolve(path, '.git'))) { - await runGit(['clone', '--no-checkout', paths.mirror, path]); - await runGit(['remote', 'set-url', 'origin', project.repository.cloneUrl], path); - } else { - await runGit(['fetch', paths.mirror, '+refs/heads/*:refs/remotes/provider/*', '--prune'], path); - } - const exactRefSha = await ensureExactRef(paths.mirror, options.exactRef, true); - if (options.exactRef?.trim() && !exactRefSha) throw new Error(`Cloned project does not contain governed exact ref ${options.exactRef}.`); - const checkoutRef = exactRefSha ?? branch; - await runGit(['checkout', '--detach', checkoutRef], path); - await runGit(['reset', '--hard', checkoutRef], path); - return { ...project, repository: { ...project.repository, ok: true, path, branch, commitSha: await gitSha(path), materialization: 'clone', mirrorPath: paths.mirror } }; - } catch (error) { - return { ...project, repository: { ...project.repository, ok: false, path, branch, commitSha: await gitSha(path), materialization: 'clone', mirrorPath: paths.mirror, error: error instanceof Error ? error.message : String(error) } }; - } -} - -export async function releaseMaterializedAssignmentProject( - config: ProviderHostRuntimeConfig, - project: MaterializedAssignmentProject, -) { - const repositoryPath = resolve(project.repository.path); - assertProviderOwnedRepositoryPath(config.dataDir, repositoryPath); - const ownedRoot = project.repository.materialization === 'clone' && repositoryPath.endsWith(`${sep}checkout`) - ? dirname(repositoryPath) - : repositoryPath; - assertProviderOwnedRepositoryPath(config.dataDir, ownedRoot); - await rm(ownedRoot, { recursive: true, force: true }); -} - -export async function listMaterializedAssignmentIds(config: ProviderHostRuntimeConfig) { - const roots = [resolve(config.dataDir, 'assignment-contexts'), resolve(config.dataDir, 'assignments')]; - const ids = new Set(); - for (const root of roots) { - assertProviderOwnedRepositoryPath(config.dataDir, root); - for (const entry of await readdir(root, { withFileTypes: true }).catch(() => [])) { - if (entry.isDirectory()) ids.add(entry.name); - } - } - return [...ids].sort(); -} - -export async function releaseRecoveredAssignmentMaterialization( - config: ProviderHostRuntimeConfig, - assignmentId: string, -) { - const safeId = safeSegment(assignmentId); - const contextRoot = contextPath(config, safeId); - const checkoutRoot = resolve(config.dataDir, 'assignments', safeId, 'checkout'); - if (existsSync(contextRoot)) { - assertProviderOwnedRepositoryPath(config.dataDir, contextRoot); - await rm(contextRoot, { recursive: true, force: true }); - return { status: 'released' as const, kind: 'context' as const }; - } - if (!existsSync(checkoutRoot)) return { status: 'absent' as const }; - assertProviderOwnedRepositoryPath(config.dataDir, checkoutRoot); - const changed = (await runGit(['status', '--porcelain=v1'], checkoutRoot)).stdout.trim(); - if (changed) return { status: 'blocked' as const, kind: 'clone' as const, reason: 'repository_changes_present' as const }; - await rm(dirname(checkoutRoot), { recursive: true, force: true }); - return { status: 'released' as const, kind: 'clone' as const }; -} - -export function providerProjectSiteRoot(project: AssignmentProjectContext, repositoryPath: string) { - const sitePath = normalizedRelativePath(architectureString(project, 'sitePath')); - return sitePath ? resolve(repositoryPath, sitePath) : repositoryPath; -} - -export function providerProjectTreeDxOptions(project: AssignmentProjectContext, treeDx?: AgentSdkTreeDxOptions) { - if (!treeDx) return undefined; - const contentPath = architectureString(project, 'contentPath') ?? 'src/content'; - const repositoryName = `treeseed-${project.slug.replace(/[^A-Za-z0-9_.-]+/gu, '-').replace(/^-+|-+$/gu, '').toLowerCase()}`; - return { - ...treeDx, - repoId: undefined, - repositoryHints: [{ name: repositoryName, purpose: 'project_content' }, { name: repositoryName }, ...(treeDx.repositoryHints ?? [])], - contentPathMap: Object.fromEntries(['agent', 'objective', 'note', 'knowledge', 'decision', 'proposal', 'question'].map((model) => [model, `${contentPath.replace(/\/+$/u, '')}/${model === 'knowledge' ? 'knowledge' : `${model}s`}`])), - } satisfies AgentSdkTreeDxOptions; -} - -export function assignmentProjectContext(assignment: Record): AssignmentProjectContext | null { - const workspace = assignment.workspaceContext && typeof assignment.workspaceContext === 'object' && !Array.isArray(assignment.workspaceContext) ? assignment.workspaceContext as Record : {}; - const project = workspace.project && typeof workspace.project === 'object' && !Array.isArray(workspace.project) ? workspace.project as AssignmentProjectContext : null; - return project?.id && project?.slug && project.repository?.cloneUrl ? project : null; -} diff --git a/src/provider/reporting/codex-event-message.ts b/src/provider/reporting/codex-event-message.ts deleted file mode 100644 index 35cae1d9..00000000 --- a/src/provider/reporting/codex-event-message.ts +++ /dev/null @@ -1,40 +0,0 @@ -function record(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; -} - -function text(value: unknown) { - return typeof value === 'string' ? value : null; -} - -function activityItem(item: Record) { - const type = text(item.type); - if (type === 'agent_message' || type === 'reasoning') return { id: text(item.id), type, text: text(item.text) }; - if (type === 'command_execution') return { id: text(item.id), type, command: text(item.command), status: text(item.status), exitCode: item.exit_code ?? null }; - if (type === 'file_change') return { id: text(item.id), type, status: text(item.status), changes: item.changes ?? [] }; - if (type === 'mcp_tool_call') return redactCodexTraceRecord(item); - if (type === 'web_search') return { id: text(item.id), type, query: text(item.query) }; - if (type === 'todo_list') return { id: text(item.id), type, items: item.items ?? [] }; - if (type === 'error') return { id: text(item.id), type, message: text(item.message) }; - return Object.keys(item).length ? { id: text(item.id), type } : null; -} - -export function codexEventMessage(event: Record) { - const eventType = text(event.type) ?? 'unknown'; - const item = activityItem(record(event.item)); - const digest = createHash('sha256').update(JSON.stringify(event)).digest('hex'); - return { - type: 'agent.execution.activity', - payload: { - provider: 'codex', - eventType, - threadId: text(event.thread_id), - item, - usage: eventType === 'turn.completed' ? record(event.usage) : null, - error: eventType === 'turn.failed' ? record(event.error) : eventType === 'error' ? { message: text(event.message) } : null, - redactionStatus: 'sanitized', - payloadDigest: digest, - }, - }; -} -import { createHash } from 'node:crypto'; -import { redactCodexTraceRecord } from '../../agents/adapters/codex/execution-codex-redaction.ts'; diff --git a/src/provider/reporting/error-diagnostics.ts b/src/provider/reporting/error-diagnostics.ts deleted file mode 100644 index d597d32a..00000000 --- a/src/provider/reporting/error-diagnostics.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { record, stringValue } from '../configuration/value-utils.ts'; - -export interface ProviderErrorDiagnostic { - phase: string; - name: string; - message: string; - code: string | null; - status: number | null; - operation: string | null; - path: string | null; - timeoutMs: number | null; - missingRequirements: string[]; - cause: Omit | null; -} - -function finiteNumber(value: unknown) { - const number = Number(value); - return Number.isFinite(number) ? number : null; -} - -function diagnosticCore(error: unknown): Omit { - const value = record(error); - const payload = record(value.payload); - const details = record(value.details); - return { - name: error instanceof Error ? error.name : stringValue(value.name) ?? 'Error', - message: error instanceof Error ? error.message : String(error), - code: stringValue(value.code, payload.code, details.code), - status: finiteNumber(value.status), - operation: stringValue(value.operation, payload.operation, details.operation), - path: stringValue(payload.path, details.path, details.contentPath), - timeoutMs: finiteNumber(payload.timeoutMs ?? details.timeoutMs), - missingRequirements: Array.isArray(details.missingRequirements) - ? details.missingRequirements.filter((entry): entry is string => typeof entry === 'string') - : [], - }; -} - -export function providerErrorIsRetryable(error: unknown) { - const status = finiteNumber(record(error).status); - return status == null || status === 0 || status >= 500; -} - -export function providerErrorDiagnostic(error: unknown, phase: string): ProviderErrorDiagnostic { - const value = record(error); - const cause = error instanceof Error ? error.cause : value.cause; - return { - phase, - ...diagnosticCore(error), - cause: cause == null ? null : diagnosticCore(cause), - }; -} diff --git a/src/provider/reporting/message-recorder.ts b/src/provider/reporting/message-recorder.ts deleted file mode 100644 index 6c43cc0c..00000000 --- a/src/provider/reporting/message-recorder.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { deliverProviderModeRunTelemetry, type AssignmentModeRunRecorder } from './mode-run-telemetry.ts'; - -export function createProviderMessageRecorder(input: { - recorder: AssignmentModeRunRecorder; - assignmentId: string; - mode: string; - selectedInput: Record; - capacityEnvelope: Record; - runnerId: string; -}) { - let deliveredSequence = 0; - let pending: { sequence: number; message: Record } | null = null; - return async (request: Record) => { - if (!pending) { - const sequence = deliveredSequence + 1; - pending = { - sequence, - message: { - id: `provider-message-${input.assignmentId}-${sequence}`, - ...request, - actor: request.actor ?? 'agent', - createdAt: new Date().toISOString(), - }, - }; - } - const { sequence, message } = pending; - await deliverProviderModeRunTelemetry({ - recorder: input.recorder, - assignmentId: input.assignmentId, - eventId: `message:${message.id}`, - request: { - mode: input.mode, - status: 'running', - selectedInput: input.selectedInput, - capacityEnvelope: input.capacityEnvelope, - outputs: { - status: 'message_recorded', - summary: `Recorded provider assignment message ${message.id}.`, - metadata: { source: 'provider_runner_message', message }, - }, - metadata: { - source: 'provider_runner_message', - assignmentId: input.assignmentId, - runnerId: input.runnerId, - messageId: message.id, - }, - }, - }); - deliveredSequence = sequence; - pending = null; - return { ok: true, model: 'message', action: 'create', payload: message }; - }; -} diff --git a/src/provider/reporting/mode-run-reporter.ts b/src/provider/reporting/mode-run-reporter.ts deleted file mode 100644 index b4b5636d..00000000 --- a/src/provider/reporting/mode-run-reporter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { AgentModeRunStatus } from '@treeseed/sdk/agent-capacity'; -import type { ProviderAssignmentClient } from '../coordination/lease-client.ts'; -import { deliverProviderModeRunTelemetry } from './mode-run-telemetry.ts'; -import { stringValue } from '../configuration/value-utils.ts'; - -export async function recordEarlyModeRun(input: { - client: ProviderAssignmentClient; - assignmentId: string; - assignment: Record; - selectedInput: Record; - capacityEnvelope: Record; - status: AgentModeRunStatus; - fallbackReason: string; - metadata?: Record; - outputs?: Record; - traceRefs?: Record; - startedAt?: string | null; -}) { - if (!input.assignmentId) return null; - const source = stringValue(input.metadata?.source) ?? 'provider_runner_early_exit'; - const outputStatus = stringValue(input.outputs?.status) ?? input.fallbackReason ?? input.status; - return deliverProviderModeRunTelemetry({ - recorder: input.client, - assignmentId: input.assignmentId, - eventId: `runner:${source}:${outputStatus}`, - request: { - mode: stringValue(input.assignment.mode, input.capacityEnvelope.mode) ?? 'planning', - status: input.status, - selectedInput: input.selectedInput, - capacityEnvelope: input.capacityEnvelope, - outputs: input.outputs ?? {}, - traceRefs: input.traceRefs ?? {}, - fallbackReason: input.fallbackReason, - startedAt: input.startedAt ?? null, - metadata: { source, ...(input.metadata ?? {}) }, - }, - }); -} diff --git a/src/provider/reporting/mode-run-telemetry.ts b/src/provider/reporting/mode-run-telemetry.ts deleted file mode 100644 index 9ab2021e..00000000 --- a/src/provider/reporting/mode-run-telemetry.ts +++ /dev/null @@ -1,110 +0,0 @@ -export interface AssignmentModeRunRecorder { - createAssignmentModeRun(assignmentId: string, request: Record): Promise; -} - -export interface ProviderModeRunTelemetryInput { - recorder: AssignmentModeRunRecorder; - assignmentId: string; - eventId: string; - request: Record; - maxAttempts?: number; - timeoutMs?: number; -} - -export class ProviderModeRunTelemetryError extends Error { - readonly code = 'provider_mode_run_telemetry_delivery_failed'; - readonly retryable = true; - readonly assignmentId: string; - readonly eventId: string; - readonly attempts: number; - - constructor(input: { assignmentId: string; eventId: string; attempts: number; cause: unknown }) { - super( - `Provider mode-run telemetry ${input.eventId} for assignment ${input.assignmentId} failed after ${input.attempts} attempts: ${input.cause instanceof Error ? input.cause.message : String(input.cause)}`, - { cause: input.cause }, - ); - this.name = 'ProviderModeRunTelemetryError'; - this.assignmentId = input.assignmentId; - this.eventId = input.eventId; - this.attempts = input.attempts; - } -} - -function required(value: string, field: string): string { - const normalized = value.trim(); - if (!normalized) throw new Error(`${field} is required for provider mode-run telemetry.`); - return normalized; -} - -function boundedInteger(value: number | undefined, fallback: number, minimum: number, maximum: number): number { - if (value === undefined) return fallback; - if (!Number.isInteger(value) || value < minimum || value > maximum) { - throw new Error(`Provider mode-run telemetry value must be an integer from ${minimum} through ${maximum}.`); - } - return value; -} - -function safeId(value: string): string { - return value - .trim() - .toLowerCase() - .replace(/[^a-z0-9_.:-]+/gu, '-') - .replace(/^-+|-+$/gu, '') || 'event'; -} - -export function providerModeRunTelemetryId(assignmentId: string, eventId: string): string { - return `telemetry:${safeId(required(assignmentId, 'assignmentId'))}:${safeId(required(eventId, 'eventId'))}`; -} - -async function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { - let timer: ReturnType | null = null; - try { - return await Promise.race([ - promise, - new Promise((_resolve, reject) => { - timer = setTimeout(() => reject(new Error(message)), timeoutMs); - }), - ]); - } finally { - if (timer) clearTimeout(timer); - } -} - -function assertAccepted(result: unknown): void { - if (result && typeof result === 'object' && !Array.isArray(result) && (result as Record).ok === false) { - throw new Error('Provider mode-run telemetry endpoint rejected the event.'); - } -} - -export async function deliverProviderModeRunTelemetry(input: ProviderModeRunTelemetryInput): Promise { - const assignmentId = required(input.assignmentId, 'assignmentId'); - const eventId = required(input.eventId, 'eventId'); - const maxAttempts = boundedInteger(input.maxAttempts, 3, 1, 10); - const timeoutMs = boundedInteger(input.timeoutMs, 10_000, 1, 60_000); - const request = { - ...input.request, - id: providerModeRunTelemetryId(assignmentId, eventId), - metadata: { - ...((input.request.metadata && typeof input.request.metadata === 'object' && !Array.isArray(input.request.metadata)) - ? input.request.metadata as Record - : {}), - recordKind: 'telemetry', - telemetryEventId: eventId, - }, - }; - let lastError: unknown = new Error('Provider mode-run telemetry was not attempted.'); - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - try { - const result = await withTimeout( - input.recorder.createAssignmentModeRun(assignmentId, request), - timeoutMs, - `Provider mode-run telemetry ${eventId} exceeded ${timeoutMs}ms.`, - ); - assertAccepted(result); - return result; - } catch (error) { - lastError = error; - } - } - throw new ProviderModeRunTelemetryError({ assignmentId, eventId, attempts: maxAttempts, cause: lastError }); -} diff --git a/src/provider/reporting/runtime-event-reporter.ts b/src/provider/reporting/runtime-event-reporter.ts deleted file mode 100644 index be2ff98a..00000000 --- a/src/provider/reporting/runtime-event-reporter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ProviderRuntimeEventInput } from '@treeseed/sdk/agent-capacity'; -import type { ProviderAssignmentClient } from '../coordination/lease-client.ts'; - -export async function reportProviderRuntimeEvent(input: { - client: ProviderAssignmentClient; - assignmentId: string; - event: ProviderRuntimeEventInput; - maxAttempts?: number; -}) { - if (!input.client.createAssignmentEvent || !input.assignmentId) return false; - const attempts = Math.max(1, Math.min(input.maxAttempts ?? 3, 5)); - let lastError: unknown = null; - for (let attempt = 1; attempt <= attempts; attempt += 1) { - try { - await input.client.createAssignmentEvent(input.assignmentId, input.event); - return true; - } catch (error) { - lastError = error; - } - } - console.error(JSON.stringify({ - level: 'error', event: 'provider.runtime_event_delivery_failed', assignmentId: input.assignmentId, - runtimeEventType: input.event.eventType, message: lastError instanceof Error ? lastError.message : String(lastError), attempts, - })); - return false; -} diff --git a/src/provider/teams/multi-team-runtime.ts b/src/provider/teams/multi-team-runtime.ts index 066e5cf2..e0ca8746 100644 --- a/src/provider/teams/multi-team-runtime.ts +++ b/src/provider/teams/multi-team-runtime.ts @@ -1,354 +1,180 @@ -import type { ProviderConnectionRuntimeContext, ProviderHostRuntimeConfig } from '../configuration/config.ts'; -import { CapacityProviderCoordinator, type ProviderConnectionResult } from '../coordination/coordinator.ts'; -import { ProviderGlobalSlotScheduler, providerManifestConcurrency } from '../coordination/connection-scheduler.ts'; import { loadProviderManifest } from '../configuration/manifest.ts'; -import { ProviderLocalCapacityStore, type ProviderLocalSlotClaim } from '../capacity/capacity-core/local-capacity-store.ts'; +import type { ProviderConnectionRuntimeContext, ProviderHostRuntimeConfig } from '../configuration/config.ts'; +import { CapacityProviderCoordinator, type ProviderConnectionRuntime } from '../coordination/coordinator.ts'; +import { createProviderControlPlaneClient } from '../coordination/client.ts'; import { recoverProviderLocalLeases } from '../coordination/lease-recovery.ts'; -import { discoverProviderBudgets } from '../configuration/budgets.ts'; -import { compileProviderLocalNativeLimit } from '../capacity/capacity-core/native-capacity-limits.ts'; -import { createAssignmentExecutionProviderAdapter } from '../capacity/providers/execution-provider-selection.ts'; -import { createProviderMarketClient, PROVIDER_ASSIGNMENT_POLLING_TTL_MS } from '../coordination/client.ts'; -import { observeProviderDiskCapacity } from '../runtime/disk-capacity.ts'; - -const managerSchedulers = new Map>>(); +import { ProviderLocalCapacityStore } from '../capacity/capacity-core/local-capacity-store.ts'; +import { publishProviderAvailability, buildProviderRunnerPlan } from '../lifecycle/lifecycle.ts'; +import { resolveAgentExecutor } from '../execution/executor-loader.ts'; +import { runProviderAssignment } from '../operations/runner.ts'; function record(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } -function stringValue(...values: unknown[]) { - for (const value of values) if (typeof value === 'string' && value.trim()) return value.trim(); - return undefined; +function text(...values: unknown[]) { + return values.find((value) => typeof value === 'string' && value.trim()) as string | undefined; } -function numberValue(...values: unknown[]) { - for (const value of values) { - const parsed = typeof value === 'number' ? value : Number(value); - if (Number.isFinite(parsed)) return parsed; - } - return undefined; -} - -async function observeExecutionProviders( - config: ProviderConnectionRuntimeContext, - executionProviders: NonNullable, -) { - const client = createProviderMarketClient(config); - return Promise.all(executionProviders.map(async (provider) => { - try { - const adapter = createAssignmentExecutionProviderAdapter({ - selection: provider.adapter, - repoRoot: process.cwd(), - jira: config.jira, - githubIssues: config.githubIssues, - discord: config.discord, - accessToken: config.accessToken, - apiBaseUrl: config.marketUrl, - researchSourcePolicy: provider.researchSourcePolicy, - workflow: { - dispatchWorkflowOperation: async (assignmentId, operationId, body) => { - const response = await client.dispatchAssignmentWorkflowOperation(assignmentId, operationId, body); - const value = record(response); - return { ok: value.ok === undefined ? true : value.ok === true, payload: record(value.payload ?? value) }; - }, - getWorkflowOperationRun: async (assignmentId, runId) => { - const response = await client.getAssignmentWorkflowRun(assignmentId, runId); - const value = record(response); - return { ok: value.ok === undefined ? true : value.ok === true, payload: record(value.payload ?? value) }; - }, - }, - }); - const observation = await adapter.observe({ - capacityProviderId: config.providerId, - executionProviderId: provider.id, - activeAssignmentIds: [], - }); - const descriptor = observation.descriptor ?? await adapter.describe(); - return { - ...provider, - status: observation.available === true ? 'available' as const : 'unavailable' as const, - capabilities: provider.capabilities ?? descriptor.capabilities, - nativeUnit: descriptor.nativeUnit, - quotaVisibility: descriptor.quotaVisibility, - maxConcurrentRunners: Math.min( - Number(provider.nativeLimits.maxConcurrentRunners ?? descriptor.maxConcurrentAssignments), - descriptor.maxConcurrentAssignments, - ), - activeRunners: observation.activeAssignmentCount ?? 0, - observations: { - available: observation.available === true, - pressure: observation.pressure ?? 'exhausted', - activeAssignmentCount: observation.activeAssignmentCount ?? 0, - }, - }; - } catch { - return { - ...provider, - status: 'unavailable' as const, - activeRunners: 0, - observations: { available: false, pressure: 'exhausted', activeAssignmentCount: 0 }, - }; - } - })); -} - -export async function createCapacityProviderCoordinator(config: ProviderHostRuntimeConfig) { - if (!config.manifestPath) throw new Error('TREESEED_CAPACITY_PROVIDER_MANIFEST is required for multi-team provider runtime.'); - return new CapacityProviderCoordinator(await loadProviderManifest(config.manifestPath), config.dataDir); -} - -function connectionRuntimeContext( +function context( config: ProviderHostRuntimeConfig, - connection: NonNullable, - executionProviders?: ProviderConnectionRuntimeContext['executionProviders'], + runtime: ProviderConnectionRuntime, + executionProviders: ProviderConnectionRuntimeContext['executionProviders'], defaultExecutionProviderId?: string, - accessTokenProvider?: (minimumValidityMs?: number) => Promise, ): ProviderConnectionRuntimeContext { return { ...config, - connectionId: connection.connection.id, - marketUrl: connection.marketUrl, - marketAudience: connection.marketAudience, - teamId: connection.teamId, - providerId: connection.providerId, - membershipId: connection.membershipId, - accessToken: connection.accessToken.accessToken, - ...(accessTokenProvider ? { accessTokenProvider } : {}), + connectionId: runtime.connection.id, + controlPlaneUrl: runtime.controlPlaneUrl, + controlPlaneAudience: runtime.controlPlaneAudience, + teamId: runtime.teamId, + providerId: runtime.providerId, + membershipId: runtime.membershipId, + accessToken: runtime.accessToken.accessToken, executionProviders, defaultExecutionProviderId, - env: { - ...config.env, - TREESEED_API_BASE_URL: connection.marketUrl, - TREESEED_MARKET_ID: connection.teamId, - }, + env: { ...config.env, TREESEED_API_BASE_URL: runtime.controlPlaneUrl }, }; } +export async function createCapacityProviderCoordinator(config: ProviderHostRuntimeConfig) { + if (!config.manifestPath) throw new Error('A capacity provider manifest is required.'); + return new CapacityProviderCoordinator(await loadProviderManifest(config.manifestPath), config.dataDir); +} + export async function reconcileProviderConnections(config: ProviderHostRuntimeConfig) { return (await createCapacityProviderCoordinator(config)).reconcileAll(); } export async function recoverMultiTeamProviderRunners(config: ProviderHostRuntimeConfig) { - const connections = (await reconcileProviderConnections(config)).flatMap((connection) => connection.runtime ? [connection.runtime] : []); + const connections = (await reconcileProviderConnections(config)).flatMap((entry) => entry.runtime ? [entry.runtime] : []); return recoverProviderLocalLeases({ config, connections }); } -export async function runMultiTeamProviderManager(config: ProviderHostRuntimeConfig, options: { mode?: 'plan' | 'live' } = {}) { +export async function runMultiTeamProviderManager( + config: ProviderHostRuntimeConfig, + options: { mode?: 'plan' | 'live' } = {}, +) { + const loaded = await loadProviderManifest(config.manifestPath ?? ''); if (options.mode === 'plan') { - const loaded = await loadProviderManifest(config.manifestPath ?? ''); - return { ok: true, role: 'manager', mode: 'plan', providerClass: loaded.manifest.providerClass, connections: loaded.manifest.connections.map((connection) => ({ id: connection.id, teamId: connection.teamId ?? null, marketProfile: connection.marketProfile ?? null, marketUrl: connection.marketUrl ?? null, enabled: connection.enabled !== false, offer: connection.offer })) }; + return { + ok: true, + role: 'manager', + mode: 'plan', + connections: loaded.manifest.connections.map(({ id, serverProfile, controlPlaneUrl, enabled }) => ({ + id, + serverProfile: serverProfile ?? null, + controlPlaneUrl: controlPlaneUrl ?? null, + enabled: enabled !== false, + })), + }; } - const loaded = await loadProviderManifest(config.manifestPath ?? ''); - const diskCapacity = await observeProviderDiskCapacity({ path: config.dataDir, env: config.env }); - const connections = await reconcileProviderConnections(config); const localState = new ProviderLocalCapacityStore(config.dataDir); - await recoverProviderLocalLeases({ - config, - connections: connections.flatMap((connection) => connection.runtime ? [connection.runtime] : []), - store: localState, - includeRunning: false, - }); - const localCapacity = await localState.snapshot(); - const { runManagerSkeleton } = await import('../lifecycle/lifecycle.ts'); + const connections = await reconcileProviderConnections(config); const results = await Promise.all(connections.map(async (connection) => { - if (!connection.runtime) return { ok: connection.status !== 'error', role: 'manager', connectionId: connection.connectionId, status: connection.status, ...(connection.error ? { error: connection.error } : {}) }; - try { - const runtimeConfig = connectionRuntimeContext(config, connection.runtime, loaded.manifest.executionProviders, loaded.manifest.defaultExecutionProviderId); - const observedExecutionProviders = await observeExecutionProviders(runtimeConfig, loaded.manifest.executionProviders); - const advertisedExecutionProviders = diskCapacity.ok ? observedExecutionProviders : observedExecutionProviders.map((provider) => ({ - ...provider, - status: 'unavailable' as const, - observations: { ...provider.observations, available: false, pressure: 'exhausted', diskCapacity: 'insufficient' }, - })); - return await runManagerSkeleton(runtimeConfig, { - availability: { - offer: connection.runtime.connection.offer, - executionProviders: advertisedExecutionProviders, - activeRunners: localCapacity.claims.filter((claim) => claim.connectionId === connection.runtime?.connection.id).length, - constraints: { diskCapacity }, - }, - localState, - }); - } catch (error) { - return { ok: false, role: 'manager', connectionId: connection.connectionId, status: 'error', error: error instanceof Error ? error.message : String(error) }; + if (!connection.runtime) { + return { ok: connection.status !== 'error', connectionId: connection.connectionId, status: connection.status }; } + const runtime = context(config, connection.runtime, loaded.manifest.executionProviders, loaded.manifest.defaultExecutionProviderId); + const providers = await Promise.all(loaded.manifest.executionProviders.map(async (provider) => { + const executor = await resolveAgentExecutor(config, provider.id).catch(() => null); + const observation = executor + ? await executor.observe().catch((error) => ({ available: false, reason: error instanceof Error ? error.message : String(error) })) + : { available: false, reason: 'executor_not_configured' }; + return { + id: provider.id, + adapter: provider.adapter, + nativeLimits: provider.nativeLimits, + capabilities: provider.capabilities ?? [], + status: observation.available ? 'available' : 'unavailable', + observations: observation, + }; + })); + return publishProviderAvailability(runtime, { + offer: connection.runtime.connection.offer, + executionProviders: providers, + activeRunners: (await localState.snapshot()).claims.length, + }, localState); })); - await Promise.all(connections.map((connection, index) => localState.setConnectionStatus( - connection.connectionId, - Boolean(connection.runtime && results[index]?.ok), - results[index]?.ok ? undefined : String(record(results[index]).error ?? connection.error ?? connection.status), - ))); - const connected = connections.flatMap((connection, index) => connection.runtime && results[index]?.ok ? [connection.runtime] : []); - if (!diskCapacity.ok) return { - ok: true, - role: 'manager', - mode: 'multi-team', - providerClass: loaded.manifest.providerClass, - connections: results, - dispatches: [], - admission: diskCapacity, - }; - const concurrency = providerManifestConcurrency({ executionProviders: loaded.manifest.executionProviders, hostLimit: config.maxConcurrentRunners }); - let scheduler = managerSchedulers.get(loaded.path); - if (!scheduler || scheduler.maxConcurrentSlots !== concurrency) { - scheduler = new ProviderGlobalSlotScheduler(concurrency); - managerSchedulers.set(loaded.path, scheduler); - } - scheduler.updateConnections(connected); - const budgets = discoverProviderBudgets(config); - const providerLimits = new Map(loaded.manifest.executionProviders.map((provider) => [provider.id, compileProviderLocalNativeLimit({ executionProviderId: provider.id, nativeLimits: provider.nativeLimits, budgets })])); - // Legacy manifests remain operation-capable, but never become communication-ready - // by inference. Newly generated manifests declare the reserved 1+1 topology. - const providerLanes = loaded.manifest.executionProviders.map((provider) => ({ - provider, - lanes: provider.lanes?.length ? provider.lanes : [{ - id: `${provider.id}-operation`, purpose: 'operation' as const, - maxConcurrentRunners: Math.max(1, numberValue(provider.nativeLimits?.maxConcurrentRunners) ?? 1), - capabilities: [] as string[], nativeLimits: {}, - }], - })); - const laneLimits = new Map(providerLanes.flatMap(({lanes}) => lanes.map((lane) => [lane.id, { ...compileProviderLocalNativeLimit({ nativeLimits: lane.nativeLimits }), maxConcurrentRunners: lane.maxConcurrentRunners }] as const))); - const laneSlots = providerLanes.flatMap(({provider,lanes}) => lanes.flatMap((lane) => - Array.from({ length: Math.max(1, lane.maxConcurrentRunners) }, () => ({ executionProviderId: provider.id, lane })))); - const { providerAssignmentLeaseSeconds, providerRunnerCapabilities } = await import('../operations/runner.ts'); - const dispatches: Array> = []; - await Promise.all(Array.from({length:concurrency},async(_,index)=>{ - const laneSlot = laneSlots[index]; - if (!laneSlot) return; - const schedulerLease = scheduler.acquire(); - if (!schedulerLease) return; - const runtime = schedulerLease.connection; - const connectionLimit = runtime.connection.offer.maxConcurrentRunners ?? concurrency; + return { ok: results.every((entry) => entry.ok !== false), role: 'manager', connections: results }; +} + +export async function runMultiTeamProviderRunners( + config: ProviderHostRuntimeConfig, + options: { mode?: 'plan' | 'live'; background?: boolean } = {}, +) { + if (options.mode === 'plan') return buildProviderRunnerPlan(config); + const loaded = await loadProviderManifest(config.manifestPath ?? ''); + const connections = (await reconcileProviderConnections(config)).flatMap((entry) => entry.runtime ? [entry.runtime] : []); + const localState = new ProviderLocalCapacityStore(config.dataDir); + const results: Record[] = []; + for (const connection of connections) { + const runtime = context(config, connection, loaded.manifest.executionProviders, loaded.manifest.defaultExecutionProviderId); + const executionProviderId = loaded.manifest.defaultExecutionProviderId ?? loaded.manifest.executionProviders[0]?.id; + if (!executionProviderId) { + results.push({ connectionId: connection.connection.id, status: 'idle', reason: 'no_execution_provider' }); + continue; + } + const executor = await resolveAgentExecutor(config, executionProviderId); + if (!executor || !(await executor.observe()).available) { + results.push({ connectionId: connection.connection.id, status: 'idle', reason: 'executor_unavailable' }); + continue; + } const claim = await localState.claim({ - connectionId: runtime.connection.id, - globalLimit: concurrency, - connectionLimit, - pollingTtlMs: PROVIDER_ASSIGNMENT_POLLING_TTL_MS, + connectionId: connection.connection.id, + globalLimit: config.maxConcurrentRunners, + connectionLimit: connection.connection.offer.maxConcurrentRunners ?? config.maxConcurrentRunners, }); - if (!claim) { schedulerLease.release(); return; } - const connectionConfig = connectionRuntimeContext(config, runtime, loaded.manifest.executionProviders, loaded.manifest.defaultExecutionProviderId); - const client = (await import('../coordination/client.ts')).createProviderMarketClient(connectionConfig); - let leasedRecovery: { assignmentId: string; leaseToken: string; leaseExpiresAt: string; dispatchEnvelope: unknown } | null = null; + if (!claim) { + results.push({ connectionId: connection.connection.id, status: 'idle', reason: 'local_capacity_exhausted' }); + continue; + } + const client = createProviderControlPlaneClient(runtime); try { - const leased = await client.nextAssignment({ runnerId: claim.runnerId, capabilities: [...new Set([...providerRunnerCapabilities(connectionConfig), ...(laneSlot.lane.capabilities ?? [])])], leaseSeconds: providerAssignmentLeaseSeconds(connectionConfig), laneId: laneSlot.lane.id, lanePurpose: laneSlot.lane.purpose,waitSeconds:20 }); - const leasedRecord = record(leased); - const assignment = record(leasedRecord.payload ?? leasedRecord.assignment); - if (!Object.keys(assignment).length) { + const leased = record(await client.nextAssignment({ + runnerId: claim.runnerId, + capabilities: connection.connection.offer.capabilities, + leaseSeconds: 300, + })); + const assignment = record(leased.assignment ?? leased.data ?? leased.payload); + const assignmentId = text(assignment.id); + const leaseToken = text(leased.leaseToken, assignment.leaseToken); + if (!assignmentId || !leaseToken) { await localState.release(claim.id); - dispatches.push({ connectionId: runtime.connection.id, laneId: laneSlot.lane.id, lanePurpose: laneSlot.lane.purpose, assigned: 0 }); - return; + results.push({ connectionId: connection.connection.id, status: 'idle', reason: 'no_assignment' }); + continue; } - const leaseToken = stringValue(leasedRecord.leaseToken, assignment.leaseToken); - const assignmentId = stringValue(assignment.id); - if (!leaseToken || !assignmentId) throw new Error('Assignment lease response omitted its assignment id or lease token.'); - const leaseSeconds = numberValue(leasedRecord.leaseSeconds) ?? 300; - const leaseExpiresAt = stringValue(assignment.leaseExpiresAt) ?? new Date(Date.now() + leaseSeconds * 1000).toISOString(); - leasedRecovery = { assignmentId, leaseToken, leaseExpiresAt, dispatchEnvelope: leased }; - const executionProviderId = stringValue(assignment.executionProviderId); - const laneId = stringValue(assignment.laneId); - const capacityEnvelope = record(assignment.capacityEnvelope); - const nativeUnit = stringValue(capacityEnvelope.nativeUnit); - const requestedNativeAmount = numberValue(capacityEnvelope.reservedNativeAmount); + const leaseExpiresAt = text(assignment.leaseExpiresAt) ?? new Date(Date.now() + 300_000).toISOString(); await localState.attachLease(claim.id, { - assignmentId, leaseToken, dispatchEnvelope: leased, + assignmentId, + leaseToken, leaseExpiresAt, - ...(executionProviderId ? { executionProviderId, executionProviderLimit: providerLimits.get(executionProviderId) } : {}), - ...(laneId ? { laneId, laneLimit: laneLimits.get(laneId) } : {}), - ...(numberValue(capacityEnvelope.reservedSeconds, assignment.requestedSeconds) !== undefined ? { requestedSeconds: numberValue(capacityEnvelope.reservedSeconds, assignment.requestedSeconds) } : {}), - ...(nativeUnit ? { nativeUnit } : {}), - ...(requestedNativeAmount !== undefined ? { requestedNativeAmount } : {}), + executionProviderId, + dispatchEnvelope: leased, }); - dispatches.push({ connectionId: runtime.connection.id, laneId: laneSlot.lane.id, lanePurpose: laneSlot.lane.purpose, assignmentId, status: 'ready' }); - } catch (error) { - if (leasedRecovery) { - let recoveryPersistenceError: unknown = null; - try { - await localState.retainLease(claim.id, leasedRecovery); - } catch (retainError) { - recoveryPersistenceError = retainError; - } - try { - const reason = [error, recoveryPersistenceError] - .filter(Boolean) - .map((value) => value instanceof Error ? value.message : String(value)) - .join(' | '); - await client.returnAssignment(leasedRecovery.assignmentId, { leaseToken: leasedRecovery.leaseToken, runnerId: claim.runnerId, reason, code: 'provider_local_capacity_exhausted' }); - await localState.finalize(claim.id, 'capacity-rejected-return-confirmed'); - } catch (returnError) { - await localState.recordFailure(claim.id, returnError instanceof Error ? returnError.message : String(returnError)); - } - } else await localState.release(claim.id); - dispatches.push({ connectionId: runtime.connection.id, status: 'error', error: error instanceof Error ? error.message : String(error) }); - } finally { - schedulerLease.release(); - } - })); - return { ok: true, role: 'manager', mode: 'multi-team', providerClass: loaded.manifest.providerClass, connections: results, scheduler: scheduler.snapshot(), dispatches }; -} - -export async function runMultiTeamProviderRunners(config: ProviderHostRuntimeConfig, options: { - mode?: 'plan' | 'live'; - background?: boolean; -} = {}) { - if (options.mode === 'plan') { - const loaded = await loadProviderManifest(config.manifestPath ?? ''); - return { ok: true, role: 'runner', mode: 'plan', providerClass: loaded.manifest.providerClass, connections: loaded.manifest.connections.map((connection) => ({ id: connection.id, teamId: connection.teamId ?? null, enabled: connection.enabled !== false, maxConcurrentRunners: connection.offer.maxConcurrentRunners ?? null, capabilities: connection.offer.capabilities })) }; - } - const loaded = await loadProviderManifest(config.manifestPath ?? ''); - const diskCapacity = await observeProviderDiskCapacity({ path: config.dataDir, env: config.env }); - if (!diskCapacity.ok) return { ok: true, role: 'runner', mode: 'multi-team', providerClass: loaded.manifest.providerClass, dispatched: 0, results: [], admission: diskCapacity }; - const coordinator = await createCapacityProviderCoordinator(config); - const connections = await coordinator.reconcileAll(); - const runtimeIds = new Set(connections.flatMap((connection) => connection.runtime ? [connection.runtime.connection.id] : [])); - const connectedIds = (await new ProviderLocalCapacityStore(config.dataDir).schedulableConnections()).filter((connectionId) => runtimeIds.has(connectionId)); - const concurrency = providerManifestConcurrency({ executionProviders: loaded.manifest.executionProviders, hostLimit: config.maxConcurrentRunners }); - const localCapacity = new ProviderLocalCapacityStore(config.dataDir); - const leases: ProviderLocalSlotClaim[] = []; - for (let index = 0; index < concurrency; index += 1) { - const localClaim = await localCapacity.claimDispatch(connectedIds); - if (!localClaim) break; - leases.push(localClaim); - } - const { runProviderRunnerOnce } = await import('../operations/runner-lifecycle.ts'); - const execute = async (localClaim: ProviderLocalSlotClaim) => { - const runtime = connections.find((entry) => entry.runtime?.connection.id === localClaim.connectionId)?.runtime; - if (!runtime) { - await localCapacity.recordFailure(localClaim.id, 'Manager-created dispatch references an unavailable connection.'); - return { ok: false, role: 'runner', connectionId: localClaim.connectionId, error: 'Manager-created dispatch references an unavailable connection.' }; - } - const connectionConfig = connectionRuntimeContext( - config, - runtime, - loaded.manifest.executionProviders, - loaded.manifest.defaultExecutionProviderId, - async (minimumValidityMs) => (await coordinator.accessTokenForConnection(runtime.connection, minimumValidityMs)).accessToken, - ); - const client = (await import('../coordination/client.ts')).createProviderMarketClient(connectionConfig); - try { - const result = await runProviderRunnerOnce({ - config: connectionConfig, + await localState.claimDispatch([connection.connection.id]); + const terminal = await runProviderAssignment({ client, - runnerId: localClaim.runnerId, - leasedAssignment: localClaim.dispatchEnvelope, - onLeaseRenewed: async (assignment) => { - const assignmentId = stringValue(assignment.id, localClaim.assignmentId); - const leaseExpiresAt = stringValue(assignment.leaseExpiresAt, localClaim.leaseExpiresAt); - if (!assignmentId || !leaseExpiresAt) throw new Error('Renewed assignment omitted its durable identity or lease expiry.'); - await localCapacity.renewLease(localClaim.id, { assignmentId, leaseExpiresAt }); - }, - ...(connectionConfig.treeDx ? { treeDx: connectionConfig.treeDx } : {}), + executor, + assignment, + leaseToken, + runnerId: claim.runnerId, }); - await localCapacity.finalize(localClaim.id, 'assignment-lifecycle-confirmed'); - return result; + await localState.finalize(claim.id, 'terminal-receipt-confirmed'); + results.push({ connectionId: connection.connection.id, assignmentId, status: 'settled', terminal }); } catch (error) { - await localCapacity.recordFailure(localClaim.id, error instanceof Error ? error.message : String(error)); - return { ok: false, role: 'runner', connectionId: localClaim.connectionId, error: error instanceof Error ? error.message : String(error) }; + await localState.recordFailure(claim.id, error instanceof Error ? error.message : String(error)); + results.push({ + connectionId: connection.connection.id, + status: 'recovery', + error: error instanceof Error ? error.message : String(error), + }); } - }; - if (options.background) { - for (const lease of leases) void execute(lease); - return { ok: true, role: 'runner', mode: 'multi-team', providerClass: loaded.manifest.providerClass, background: true, dispatched: leases.length, pendingConnections: connections.filter((entry) => !entry.runtime) }; } - const results = await Promise.all(leases.map(execute)); - return { ok: true, role: 'runner', mode: 'multi-team', providerClass: loaded.manifest.providerClass, dispatched: leases.length, results, pendingConnections: connections.filter((entry) => !entry.runtime) }; + return { + ok: results.every((entry) => entry.status !== 'recovery'), + role: 'runner', + background: options.background === true, + results, + }; } diff --git a/src/provider/treedx/graph/treedx-context-adapter.ts b/src/provider/treedx/graph/treedx-context-adapter.ts deleted file mode 100644 index b96821bc..00000000 --- a/src/provider/treedx/graph/treedx-context-adapter.ts +++ /dev/null @@ -1,287 +0,0 @@ -import type { AgentTreeDxAdapter } from '../../../agents/runtime/runtime-types.ts'; -import type { AgentSdkTreeDxOptions } from '@treeseed/sdk/sdk'; -import { buildCapacityProviderAuthHeaders } from '@treeseed/sdk/capacity-provider'; -import { evaluateTreeDxProxyHandleAccess } from '@treeseed/sdk/agent-capacity'; -import type { ProviderConnectionRuntimeContext } from '../../configuration/config.ts'; -import type { ProviderAssignmentClient } from '../../coordination/lease-client.ts'; -import { deliverProviderModeRunTelemetry } from '../../reporting/mode-run-telemetry.ts'; -import { normalizeTreeDxProxyHandle } from '../repositories/treedx-handle.ts'; -import { positiveNumberValue, record, stringValue } from '../../configuration/value-utils.ts'; - -const DEFAULT_TREEDX_PROXY_REQUEST_TIMEOUT_MS = 30_000; - -function normalizeBaseUrl(value: string) { - return value.replace(/\/+$/, ''); -} - -export function createAssignmentTreeDxAdapter(input: { - config: ProviderConnectionRuntimeContext; - projectId: string; - assignmentId: string; - treedxProxyHandle: Record; - client?: ProviderAssignmentClient; - mode?: string; - capacityEnvelope?: Record; - decisionPayload?: Record; - runnerId?: string; -}): AgentTreeDxAdapter | null { - const handleId = stringValue(input.treedxProxyHandle.id); - if (!input.config.marketUrl || (!input.config.accessToken && !input.config.accessTokenProvider) || !handleId) return null; - const baseUrl = normalizeBaseUrl(input.config.marketUrl); - const defaultRepoId = stringValue(input.treedxProxyHandle.repositoryId); - const defaultWorkspaceId = stringValue(input.treedxProxyHandle.workspaceId); - const defaultRepositoryRef = stringValue(input.treedxProxyHandle.baseCommitSha) || stringValue(input.treedxProxyHandle.baseRef); - const scopedHandle = normalizeTreeDxProxyHandle(input.treedxProxyHandle); - if (!scopedHandle) return null; - const checkScope = (request: { repoId?: string | null; workspaceId?: string | null; operation?: string | null; path?: string | null }) => { - const result = evaluateTreeDxProxyHandleAccess(scopedHandle, { - projectId: input.projectId, - assignmentId: input.assignmentId, - repositoryId: request.repoId ?? defaultRepoId, - workspaceId: request.workspaceId ?? defaultWorkspaceId, - operation: request.operation ?? null, - path: request.path ?? null, - }); - if (!result.ok) { - throw new Error(result.reason ?? 'TreeDX proxy handle does not allow this request.'); - } - }; - const requestHeaders = async () => { - const accessToken = input.config.accessTokenProvider - ? (await input.config.accessTokenProvider()).trim() - : input.config.accessToken.trim(); - return { - accept: 'application/json', - 'content-type': 'application/json', - 'x-treeseed-assignment-id': input.assignmentId, - 'x-treeseed-treedx-proxy-handle-id': handleId, - ...buildCapacityProviderAuthHeaders(accessToken), - }; - }; - let proxyRequestSequence = 0; - const recordTreeDxProxyEvent = async (phase: 'started' | 'completed' | 'failed', event: Record) => { - if (!input.client) return; - await deliverProviderModeRunTelemetry({ - recorder: input.client, - assignmentId: input.assignmentId, - eventId: `treedx:${event.requestSequence ?? 'request'}:${phase}`, - request: { - mode: input.mode ?? 'planning', - status: phase === 'failed' ? 'failed' : 'running', - selectedInput: input.decisionPayload ?? {}, - capacityEnvelope: input.capacityEnvelope ?? {}, - outputs: { - status: `treedx_proxy_${phase}`, - summary: phase === 'started' - ? `TreeDX proxy ${event.operation ?? 'request'} started.` - : phase === 'completed' - ? `TreeDX proxy ${event.operation ?? 'request'} completed in ${event.durationMs ?? 'n/a'}ms.` - : `TreeDX proxy ${event.operation ?? 'request'} failed after ${event.durationMs ?? 'n/a'}ms.`, - metadata: { source: 'provider_runner_treedx_proxy_request', phase, ...event }, - }, - metadata: { - source: 'provider_runner_treedx_proxy_request', - phase, - assignmentId: input.assignmentId, - runnerId: input.runnerId ?? null, - operation: event.operation ?? null, - path: event.path ?? null, - durationMs: event.durationMs ?? null, - }, - }, - }); - }; - const requestOnce = async (method: 'GET' | 'POST' | 'PUT', path: string, body?: Record, operation = 'request') => { - proxyRequestSequence += 1; - const startedAt = Date.now(); - const timeoutMs = positiveNumberValue(process.env.TREESEED_PROVIDER_TREEDX_REQUEST_TIMEOUT_MS) ?? DEFAULT_TREEDX_PROXY_REQUEST_TIMEOUT_MS; - const requestSnapshot = { - requestSequence: proxyRequestSequence, - method, - path, - operation, - bodyKeys: body ? Object.keys(body).sort() : [], - bodyPreview: body - ? { - paths: Array.isArray(body.paths) ? body.paths.map(String) : undefined, - path: typeof body.path === 'string' ? body.path : undefined, - query: typeof body.query === 'string' ? body.query.slice(0, 500) : undefined, - limit: typeof body.limit === 'number' ? body.limit : undefined, - } - : null, - }; - await recordTreeDxProxyEvent('started', requestSnapshot); - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); - try { - const response = await fetch(`${baseUrl}${path}`, { - method, - headers: await requestHeaders(), - body: body === undefined ? undefined : JSON.stringify(body), - signal: controller.signal, - }); - const payload = await response.json().catch((error) => { - if (controller.signal.aborted) throw error; - return {}; - }); - const durationMs = Date.now() - startedAt; - if (!response.ok) { - const error = record(payload).error; - const code = typeof record(error).code === 'string' ? String(record(error).code) : null; - const rawDetails = record(error).details; - const details = rawDetails && typeof rawDetails === 'object' && !Array.isArray(rawDetails) ? rawDetails as Record : {}; - const message = typeof error === 'string' - ? error - : record(error).message && typeof record(error).message === 'string' - ? String(record(error).message) - : `TreeDX proxy request failed with ${response.status}.`; - await recordTreeDxProxyEvent('failed', { - ...requestSnapshot, - httpStatus: response.status, - durationMs, - errorCode: code, - errorMessage: message, - details, - }); - const detailsText = Object.keys(details).length > 0 ? ` details=${JSON.stringify(details)}` : ''; - throw new Error(code ? `${message} (${code}, ${response.status})${detailsText}` : `${message} (${response.status})${detailsText}`); - } - const envelope = record(payload); - const proxiedPayload = record(envelope.payload); - const result = Object.keys(proxiedPayload).length > 0 ? proxiedPayload : envelope; - await recordTreeDxProxyEvent('completed', { - ...requestSnapshot, - httpStatus: response.status, - durationMs, - resultKeys: Object.keys(result).sort(), - fileCount: Array.isArray(result.files) ? result.files.length : undefined, - entryCount: Array.isArray(result.entries) ? result.entries.length : undefined, - resultCount: Array.isArray(result.results) ? result.results.length : undefined, - }); - return result; - } catch (error) { - const requestError = controller.signal.aborted - ? Object.assign(new Error(`TreeDX proxy request timed out after ${timeoutMs}ms (${operation}).`, { cause: error }), { code: 'treedx_proxy_timeout', operation, path, timeoutMs }) - : error; - if (requestError instanceof Error && !requestError.message.includes('TreeDX proxy request failed')) { - await recordTreeDxProxyEvent('failed', { - ...requestSnapshot, - durationMs: Date.now() - startedAt, - errorMessage: requestError.message, - }); - } - if (requestError instanceof Error && !('operation' in requestError)) { - Object.assign(requestError, { operation, path }); - } - throw requestError; - } finally { - clearTimeout(timeout); - } - }; - const request = async (method: 'GET' | 'POST' | 'PUT', path: string, body?: Record, operation = 'request') => { - let lastError: unknown = new Error('TreeDX proxy request was not attempted.'); - for (let attempt = 1; attempt <= 3; attempt += 1) { - try { - return await requestOnce(method, path, body, operation); - } catch (error) { - lastError = error; - const message = error instanceof Error ? error.message : String(error); - const transient = /fetch failed|timed out|econnreset|econnrefused|socket|temporarily unavailable|\(429\)|\(5\d\d\)/iu.test(message); - if (!transient || attempt === 3) throw error; - } - } - throw lastError; - }; - return { - buildContext: ({ repoId, query, paths, body }) => { - const effectiveRepoId = repoId || defaultRepoId; - if (!effectiveRepoId) throw new Error('TreeDX repository id is required for context build.'); - checkScope({ repoId: effectiveRepoId, operation: 'files:read', path: paths?.[0] ?? null }); - const contextBody = body ?? {}; - const requestedLimit = positiveNumberValue(contextBody.limit) || 8; - return request('POST', `/v1/dx/projects/${encodeURIComponent(input.projectId)}/repos/${encodeURIComponent(effectiveRepoId)}/context/build`, { - query, - paths, - ...contextBody, - ref: stringValue(contextBody.ref) || defaultRepositoryRef || undefined, - budget: record(contextBody.budget).maxTokens || record(contextBody.budget).maxNodes - ? record(contextBody.budget) - : { maxNodes: Math.min(8, requestedLimit), maxTokens: 1_800 }, - }, 'context.build'); - }, - listRepositoryPaths: ({ repoId, path, ref, body }) => { - const effectiveRepoId = repoId || defaultRepoId; - if (!effectiveRepoId) throw new Error('TreeDX repository id is required for path listing.'); - checkScope({ repoId: effectiveRepoId, operation: 'files:read', path }); - return request('POST', `/v1/dx/projects/${encodeURIComponent(input.projectId)}/repos/${encodeURIComponent(effectiveRepoId)}/paths/list`, { - path, - ref: ref || defaultRepositoryRef || undefined, - ...(body ?? {}), - }, 'paths.list'); - }, - readRepositoryFiles: async ({ repoId, paths, ref, body }) => { - const effectiveRepoId = repoId || defaultRepoId; - if (!effectiveRepoId) throw new Error('TreeDX repository id is required for file read.'); - for (const path of paths) checkScope({ repoId: effectiveRepoId, operation: 'files:read', path }); - const files = (await Promise.all(paths.map(async (path) => { - const requestBody = body ?? {}; - const response = await request('POST', `/v1/dx/projects/${encodeURIComponent(input.projectId)}/repos/${encodeURIComponent(effectiveRepoId)}/files/read`, { - path, - ref: ref || defaultRepositoryRef || undefined, - ...requestBody, - maxBytes: Math.max(1, Math.min(196_608, Number(requestBody.maxBytes) || 131_072)), - offsetBytes: Math.max(0, Number(requestBody.offsetBytes) || 0), - }, 'files.read'); - const file = record(response).file; - return file && typeof file === 'object' && !Array.isArray(file) ? file as Record : null; - }))).filter((file): file is Record => Boolean(file)); - return { files, file: files[0] ?? null }; - }, - searchWorkspace: ({ workspaceId, query, body }) => { - const effectiveWorkspaceId = workspaceId || defaultWorkspaceId; - if (!effectiveWorkspaceId) throw new Error('TreeDX workspace id is required for workspace search.'); - checkScope({ workspaceId: effectiveWorkspaceId, operation: 'files:search' }); - return request('POST', `/v1/dx/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(effectiveWorkspaceId)}/search`, { - query, - ...(body ?? {}), - }, 'workspace.search'); - }, - readWorkspaceFile: ({ workspaceId, path }) => { - const effectiveWorkspaceId = workspaceId || defaultWorkspaceId; - if (!effectiveWorkspaceId) throw new Error('TreeDX workspace id is required for workspace file read.'); - checkScope({ workspaceId: effectiveWorkspaceId, operation: 'files:read', path }); - return request('GET', `/v1/dx/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(effectiveWorkspaceId)}/files?path=${encodeURIComponent(path)}`, undefined, 'workspace.files.read'); - }, - applyWorkspaceChangeset: ({ workspaceId, body }) => { - const effectiveWorkspaceId = workspaceId || defaultWorkspaceId; - if (!effectiveWorkspaceId) throw new Error('TreeDX workspace id is required for a changeset.'); - checkScope({ workspaceId: effectiveWorkspaceId, operation: 'files:write' }); - return request('POST', `/v1/dx/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(effectiveWorkspaceId)}/changesets`, body, 'workspace.changeset'); - }, - commitWorkspace: ({ workspaceId, message, body }) => { - const effectiveWorkspaceId = workspaceId || defaultWorkspaceId; - if (!effectiveWorkspaceId) throw new Error('TreeDX workspace id is required for workspace commit.'); - checkScope({ workspaceId: effectiveWorkspaceId, operation: 'git:commit' }); - return request('POST', `/v1/dx/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(effectiveWorkspaceId)}/commit`, { - message, - ...(body ?? {}), - }, 'workspace.commit'); - }, - closeWorkspace: ({ workspaceId }) => { - const effectiveWorkspaceId = workspaceId || defaultWorkspaceId; - if (!effectiveWorkspaceId) throw new Error('TreeDX workspace id is required for workspace close.'); - return request('POST', `/v1/dx/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(effectiveWorkspaceId)}/close`, {}, 'workspace.close'); - }, - }; -} - -export function assignmentScopedTreeDxOptions(base: AgentSdkTreeDxOptions | undefined, handle: Record) { - if (!base) return undefined; - const repositoryId = stringValue(handle.repositoryId); - const workspaceId = stringValue(handle.workspaceId); - return { - ...base, - ...(repositoryId ? { repoId: repositoryId } : {}), - ...(workspaceId ? { workspaceId } : {}), - } satisfies AgentSdkTreeDxOptions; -} diff --git a/src/provider/treedx/repositories/treedx-handle.ts b/src/provider/treedx/repositories/treedx-handle.ts deleted file mode 100644 index 04e1e6be..00000000 --- a/src/provider/treedx/repositories/treedx-handle.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { TreeDxProxyHandle } from '@treeseed/sdk/agent-capacity'; -import { record, stringValue } from '../../configuration/value-utils.ts'; - -export type ScopedTreeDxProxyHandle = TreeDxProxyHandle & { - allowedReadPaths?: string[]; - allowedWritePaths?: string[]; -}; - -function strings(value: unknown): string[] { - return Array.isArray(value) ? value.map(String).filter(Boolean) : []; -} - -function optionalString(value: unknown): string | null { - return typeof value === 'string' && value ? value : null; -} - -export function normalizeTreeDxProxyHandle(value: unknown): ScopedTreeDxProxyHandle | null { - const input = record(value); - const id = stringValue(input.id); - const teamId = stringValue(input.teamId); - const projectId = stringValue(input.projectId); - if (!id || !teamId || !projectId) return null; - return { - id, - teamId, - projectId, - assignmentId: optionalString(input.assignmentId), - repositoryId: optionalString(input.repositoryId), - workspaceId: optionalString(input.workspaceId), - baseCommitSha: optionalString(input.baseCommitSha), - baseRef: optionalString(input.baseRef), - branchName: optionalString(input.branchName), - status: stringValue(input.status) ?? 'issued', - scopes: strings(input.scopes), - allowedOperations: strings(input.allowedOperations), - allowedPaths: strings(input.allowedPaths), - allowedReadPaths: strings(input.allowedReadPaths), - allowedWritePaths: strings(input.allowedWritePaths), - expiresAt: optionalString(input.expiresAt), - issuedAt: optionalString(input.issuedAt), - revokedAt: optionalString(input.revokedAt), - token: optionalString(input.token), - tokenHash: optionalString(input.tokenHash), - metadata: record(input.metadata), - }; -} diff --git a/src/services/agent-worktrees.ts b/src/services/agent-worktrees.ts deleted file mode 100644 index fa4386a1..00000000 --- a/src/services/agent-worktrees.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { execFile } from 'node:child_process'; -import { mkdir } from 'node:fs/promises'; -import { existsSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { promisify } from 'node:util'; -import { prepareAgentWorktree, releaseAgentWorktree } from '@treeseed/sdk/operations/agent-tools'; -import { resolveProcessingDataDir, resolveRunnerRepositoryPaths } from './runtime-paths.ts'; - -const execFileAsync = promisify(execFile); - -export interface AgentWorktreeManagerOptions { - now?: () => Date; - exec?: typeof execFileAsync; - env?: NodeJS.ProcessEnv; - repositoryId?: string; -} - -function sanitizeRefSegment(value: string) { - return value.replace(/[^A-Za-z0-9._/-]+/gu, '-').replace(/^\/+|\/+$/gu, '') || 'task'; -} - -function normalizePath(value: string) { - return value.replace(/\\/gu, '/').replace(/^\.?\//u, '').replace(/\/+/gu, '/'); -} - -function processingParityEnabled(env: NodeJS.ProcessEnv) { - const value = env.TREESEED_PROCESSING_PARITY?.trim().toLowerCase(); - return Boolean(value && !['0', 'false', 'off', 'no'].includes(value)); -} - -function matchesPattern(path: string, pattern: string) { - const normalizedPath = normalizePath(path); - const normalizedPattern = normalizePath(pattern); - if (normalizedPattern === '**' || normalizedPattern === '*') return true; - if (normalizedPattern.endsWith('/**')) { - const prefix = normalizedPattern.slice(0, -3); - return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`); - } - if (normalizedPattern.endsWith('/')) return normalizedPath.startsWith(normalizedPattern); - return normalizedPath === normalizedPattern || normalizedPath.startsWith(`${normalizedPattern}/`); -} - -export function changedPathViolations(input: { changedPaths: string[]; allowedPaths: string[]; forbiddenPaths: string[] }) { - return input.changedPaths.filter((changedPath) => input.forbiddenPaths.some((pattern) => matchesPattern(changedPath, pattern)) - || (input.allowedPaths.length > 0 && !input.allowedPaths.some((pattern) => matchesPattern(changedPath, pattern)))); -} - -export class AgentWorktreeManager { - private readonly exec; - private readonly env; - private readonly repositoryId; - - constructor(private readonly repoRoot: string, options: AgentWorktreeManagerOptions = {}) { - this.exec = options.exec ?? execFileAsync; - this.env = options.env ?? process.env; - this.repositoryId = options.repositoryId ?? sanitizeRefSegment(this.env.TREESEED_REPOSITORY_ID?.trim() || this.env.TREESEED_PROJECT_ID?.trim() || repoRoot.split(/[\\/]/u).filter(Boolean).pop() || 'repository'); - } - - plannedWorktreePath(featureBranch: string, taskId = featureBranch) { - if (processingParityEnabled(this.env)) { - return resolveRunnerRepositoryPaths({ - volumeRoot: resolveProcessingDataDir(this.env), repositoryId: this.repositoryId, - taskId: sanitizeRefSegment(taskId).replace(/\//gu, '-'), - }).worktree; - } - return join(this.repoRoot, '.agent-worktrees', sanitizeRefSegment(featureBranch)); - } - - async createOrResumeWorktree(featureBranch: string, taskId = featureBranch, baseRef = 'HEAD') { - const branchName = sanitizeRefSegment(featureBranch); - const worktreeRoot = this.plannedWorktreePath(branchName, taskId); - await mkdir(dirname(worktreeRoot), { recursive: true }); - return prepareAgentWorktree({ - repoRoot: this.repoRoot, worktreeRoot, branchName, baseRef, exists: existsSync(worktreeRoot), - }, { exec: this.exec }); - } - - async releaseWorktree(worktreeRoot: string) { - return releaseAgentWorktree({ repoRoot: this.repoRoot, worktreeRoot }, { exec: this.exec }); - } -} diff --git a/src/services/approval-policy.ts b/src/services/approval-policy.ts deleted file mode 100644 index a9a14b58..00000000 --- a/src/services/approval-policy.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { AgentOperationGrant, AgentOperationName } from '@treeseed/sdk/operations/agent-tools'; - -export const PROMOTION_APPROVAL_DECISIONS = [ - 'approve_as_book_content', - 'request_more_research', - 'reject', -] as const; - -export const RELEASE_APPROVAL_DECISIONS = [ - 'approve_release', - 'reject_release', -] as const; - -export type PromotionApprovalDecision = typeof PROMOTION_APPROVAL_DECISIONS[number]; -export type ReleaseApprovalDecision = typeof RELEASE_APPROVAL_DECISIONS[number]; -export type AgentApprovalDecision = PromotionApprovalDecision | ReleaseApprovalDecision; -export type AgentApprovalKind = 'promote_knowledge_draft' | 'release_staged_knowledge'; - -export function defaultReleaseGrant(input: { - taskId: string; - projectId: string; - environment: string; - approvalId: string; -}) { - return { - id: `grant:knowledge-release:${input.taskId}`, - state: 'active', - operations: ['release'] as AgentOperationName[], - modes: ['mutating'], - agentRoles: ['releaser'], - taskKinds: ['release_staged_knowledge_request'], - projectIds: [input.projectId], - environments: [input.environment], - metadata: { - source: 'staged_knowledge_release_request', - approvalId: input.approvalId, - }, - } satisfies AgentOperationGrant; -} diff --git a/src/services/report-paths.ts b/src/services/report-paths.ts deleted file mode 100644 index 8b1bb63a..00000000 --- a/src/services/report-paths.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { existsSync } from 'node:fs'; -import { dirname, isAbsolute, resolve } from 'node:path'; - -export function findWorkspaceRoot(start = process.cwd()) { - let current = resolve(start); - for (;;) { - if (existsSync(resolve(current, 'treeseed.site.yaml')) && existsSync(resolve(current, 'package.json'))) { - return current; - } - const parent = dirname(current); - if (parent === current) { - return resolve(start); - } - current = parent; - } -} - -export function resolveWorkspaceReportPath(path: string) { - if (isAbsolute(path)) { - return path; - } - return resolve(findWorkspaceRoot(), path); -} diff --git a/src/services/runtime-paths.ts b/src/services/runtime-paths.ts deleted file mode 100644 index 76005c51..00000000 --- a/src/services/runtime-paths.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { join } from 'node:path'; - -export const PROCESSING_DATA_DIR = '/data'; - -function envValue(env: NodeJS.ProcessEnv, name: string) { - return env[name]?.trim() || ''; -} - -export function resolveProcessingDataDir(env: NodeJS.ProcessEnv = process.env) { - return envValue(env, 'TREESEED_DATA_DIR') - || envValue(env, 'TREESEED_RUNNER_VOLUME_ROOT') - || envValue(env, 'RAILWAY_VOLUME_MOUNT_PATH') - || (envValue(env, 'TREESEED_PROCESSING_PARITY') ? PROCESSING_DATA_DIR : '.treeseed-runner'); -} - -export function resolveRunnerRoot(volumeRoot: string, runnerId: string) { - return join(volumeRoot, 'runners', runnerId); -} - -export function resolveRunnerTmpRoot(volumeRoot: string) { - return join(volumeRoot, 'tmp'); -} - -export function resolveRunnerWorkspaceRoot(volumeRoot: string, workspaceId: string) { - return join(volumeRoot, 'workspaces', workspaceId); -} - -export function resolveRunnerRepositoryPaths(input: { - volumeRoot: string; - repositoryId: string; - taskId: string; -}) { - const repositoryRoot = join(input.volumeRoot, 'repositories', input.repositoryId); - return { - repositoryRoot, - bareGit: join(repositoryRoot, 'bare.git'), - bare: join(repositoryRoot, 'bare.git'), - worktree: join(repositoryRoot, 'worktrees', input.taskId), - }; -} - -export function summarizeProcessingStorage(input: { - volumeRoot: string; - repositoryId?: string; - taskId?: string; - runnerId?: string; -}) { - const repositoryId = input.repositoryId ?? ''; - const taskId = input.taskId ?? ''; - const runnerId = input.runnerId ?? ''; - const paths = resolveRunnerRepositoryPaths({ - volumeRoot: input.volumeRoot, - repositoryId, - taskId, - }); - return { - dataDir: input.volumeRoot, - repositoryBarePath: paths.bare, - repositoryWorktreePath: paths.worktree, - runnerPath: resolveRunnerRoot(input.volumeRoot, runnerId), - tmpPath: resolveRunnerTmpRoot(input.volumeRoot), - }; -} diff --git a/templates/github/deploy-capacity-provider.workflow.yml b/templates/github/deploy-capacity-provider.workflow.yml index 2c42ef1a..5caaba99 100644 --- a/templates/github/deploy-capacity-provider.workflow.yml +++ b/templates/github/deploy-capacity-provider.workflow.yml @@ -24,4 +24,4 @@ jobs: docker build --target agent-runner -t treeseed/agent-runner:${{ inputs.environment }} . - name: Deployment handoff run: | - echo "Deploy manager and runner role images with Market-managed host secrets." + echo "Deploy manager and runner images with trusted provider-host secrets." diff --git a/templates/railway/capacity-provider.yml b/templates/railway/capacity-provider.yml deleted file mode 100644 index d6febb5a..00000000 --- a/templates/railway/capacity-provider.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Railway service template for hosted capacity provider deployments. -# Secrets are injected by Market deployment flows or `trsd capacity`; do not render plaintext env files. - -services: - manager: - name: treeseed-capacity-provider-manager - image: ghcr.io/treeseed-ai/agent:capacity-provider - startCommand: node ./dist/provider/lifecycle/entrypoint.js manager - variables: - TREESEED_PROVIDER_DATA_DIR: /data - TREESEED_PROVIDER_STARTUP_MODE: live - runner: - name: treeseed-capacity-provider-runner - image: ghcr.io/treeseed-ai/agent:capacity-provider - startCommand: node ./dist/provider/lifecycle/entrypoint.js runner - variables: - TREESEED_PROVIDER_DATA_DIR: /data - TREESEED_PROVIDER_STARTUP_MODE: live diff --git a/tests/acceptance/provider/provider-acceptance.test.ts b/tests/acceptance/provider/provider-acceptance.test.ts deleted file mode 100644 index a300442b..00000000 --- a/tests/acceptance/provider/provider-acceptance.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; -import { recoverOrphanedAcceptanceClaims, resolveLiveAcceptanceExecutionProvider, runWithAcceptanceAvailabilityHeartbeat, waitForAcceptanceDispatches } from '../../../src/provider-acceptance.ts'; -import { ProviderLocalCapacityStore } from '../../../src/provider/capacity/capacity-core/local-capacity-store.ts'; - -describe('live capacity acceptance provider compilation', () => { - const roots: string[] = []; - - afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); - }); - - async function manifest(sourcePolicy = true) { - const root = await mkdtemp(join(tmpdir(), 'treeseed-provider-acceptance-test-')); - roots.push(root); - await writeFile(join(root, 'treeseed.capacity-provider.yaml'), [ - 'schemaVersion: 2', - 'providerClass: agent', - 'ownership:', - ' type: external', - 'configuration:', - ' generation: acceptance-test-v1', - 'supplyCeilings:', - ' maxConcurrentAssignments: 4', - 'identity:', - ' privateKeyRef: data://identity.json', - ' displayName: Acceptance test provider', - 'executionProviders:', - ' - id: codex', - ' adapter: codex', - ...(sourcePolicy ? [ - ' researchSourcePolicy:', - ' schemaVersion: 1', - ' allowedDomains: [example.com, iana.org]', - ' requestTimeoutMs: 20000', - ' maxResponseBytes: 1000000', - ' maxRedirects: 3', - ' allowedContentTypes: [text/html, text/plain]', - ] : []), - ' nativeLimits: { maxConcurrentRunners: 4, availableAgentSeconds: 3600 }', - ' capabilities: [engineering, research]', - 'connections: []', - ].join('\n')); - return root; - } - - it('preserves the canonical research source policy in the isolated acceptance provider', async () => { - const cwd = await manifest(); - const provider = await resolveLiveAcceptanceExecutionProvider({ - cwd, - env: {}, - executionProviderId: 'codex', - capabilities: ['planning', 'research'], - }); - - expect(provider.researchSourcePolicy).toMatchObject({ - allowedDomains: ['example.com', 'iana.org'], - requestTimeoutMs: 20000, - }); - expect(provider.nativeLimits).toEqual({ maxConcurrentRunners: 1, availableAgentSeconds: 3_600 }); - expect(provider.capabilities).toEqual(['planning', 'research']); - }); - - it('fails closed when a research acceptance provider has no source policy', async () => { - const cwd = await manifest(false); - await expect(resolveLiveAcceptanceExecutionProvider({ - cwd, - env: {}, - executionProviderId: 'codex', - capabilities: ['research'], - })).rejects.toThrow('requires researchSourcePolicy'); - }); - - it('bounds the isolated provider to the explicitly requested two-runner concurrency', async () => { - const cwd = await manifest(); - const provider = await resolveLiveAcceptanceExecutionProvider({ - cwd, env: {}, executionProviderId: 'codex', capabilities: ['planning'], maxConcurrentRunners: 2, - }); - expect(provider.nativeLimits).toEqual({ maxConcurrentRunners: 2, availableAgentSeconds: 3_600 }); - }); - - it('retries the production manager cycle until delayed demand becomes dispatchable', async () => { - let calls = 0; - const result = await waitForAcceptanceDispatches({ - expectedAssignmentIds: ['assignment-delayed'], - expectedDispatchCount: 1, - attempts: 3, - intervalMs: 0, - runManager: async () => { - calls += 1; - return { - ok: true, - role: 'manager', - mode: 'multi-team', - connections: [], - scheduler: {}, - dispatches: calls === 1 - ? [{ connectionId: 'acceptance', assigned: 0 }] - : [{ connectionId: 'acceptance', assignmentId: 'assignment-delayed', status: 'ready' }], - }; - }, - }); - expect(calls).toBe(2); - expect(result.dispatches).toEqual([ - { connectionId: 'acceptance', assigned: 0 }, - { connectionId: 'acceptance', assignmentId: 'assignment-delayed', status: 'ready' }, - ]); - }); - - it('uses a refresh-only heartbeat instead of leaving a background manager free to pre-lease the next assignment', async () => { - const source = await readFile(new URL('../../../src/provider-acceptance.ts', import.meta.url), 'utf8'); - expect(source).not.toContain('setInterval('); - expect(source).not.toContain('managerRefresh'); - expect(source).toContain('heartbeatOnly: true'); - }); - - it('keeps availability fresh during execution and stops heartbeats before returning', async () => { - let heartbeats = 0; - const result = await runWithAcceptanceAvailabilityHeartbeat({ - intervalMs: 2, - heartbeat: async () => { heartbeats += 1; }, - operation: async () => { - await new Promise((resolveDelay) => setTimeout(resolveDelay, 12)); - return 'completed'; - }, - }); - const stoppedAt = heartbeats; - await new Promise((resolveDelay) => setTimeout(resolveDelay, 8)); - expect(result).toBe('completed'); - expect(stoppedAt).toBeGreaterThan(0); - expect(heartbeats).toBe(stoppedAt); - }); - - it('recovers only claims orphaned by prior isolated acceptance connections', async () => { - const root = await mkdtemp(join(tmpdir(), 'treeseed-provider-acceptance-claims-')); - roots.push(root); - const store = new ProviderLocalCapacityStore(root); - const stale = await store.claim({ connectionId: 'acceptance-prior-run', globalLimit: 3, connectionLimit: 1 }); - const active = await store.claim({ connectionId: 'acceptance-current-run', globalLimit: 3, connectionLimit: 1 }); - const durable = await store.claim({ connectionId: 'primary-team', globalLimit: 3, connectionLimit: 1 }); - expect(await recoverOrphanedAcceptanceClaims(store, ['acceptance-current-run'])).toEqual([stale?.id]); - expect((await store.snapshot()).claims.map((claim) => claim.id).sort()).toEqual([active?.id, durable?.id].sort()); - }); - - it('retains a claim from another executor in the same live run family', async () => { - const root = await mkdtemp(join(tmpdir(), 'treeseed-provider-acceptance-family-')); - roots.push(root); - const store = new ProviderLocalCapacityStore(root); - const sameRun = await store.claim({ connectionId: 'acceptance-20260722020032-engineering-graph-6', globalLimit: 2, connectionLimit: 1 }); - const priorRun = await store.claim({ connectionId: 'acceptance-20260721231723-engineering-graph-6', globalLimit: 2, connectionLimit: 1 }); - expect(await recoverOrphanedAcceptanceClaims(store, ['acceptance-20260722020032-engineering-graph-7'])).toEqual([priorRun?.id]); - expect((await store.snapshot()).claims.map((claim) => claim.id)).toEqual([sameRun?.id]); - }); -}); diff --git a/tests/contract/agents/agent-contracts.test.ts b/tests/contract/agents/agent-contracts.test.ts deleted file mode 100644 index 30673e7a..00000000 --- a/tests/contract/agents/agent-contracts.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { runAgentContractChecks } from '../../../src/agents/testing/agent-contracts.ts'; - -describe('agent contract tests', () => { - it('validates integrated Market Markdown agent specs and writes a readable report', async () => { - const repoRoot = resolve(__dirname, '../../../../..'); - const result = await runAgentContractChecks({ - repoRoot, - now: new Date('2026-05-19T00:00:00.000Z'), - }); - expect(result.ok).toBe(true); - expect(existsSync(result.reportPath)).toBe(true); - const report = readFileSync(result.reportPath, 'utf8'); - expect(report).toContain('# Agent Contract Test Report'); - if (existsSync(resolve(repoRoot, 'src/content/agents'))) { - expect(result.agents.map((agent) => agent.slug)).toEqual(expect.arrayContaining([ - 'architect', - 'technical-writer', - 'tester', - 'engineer', - 'reporter', - ])); - expect(report).toContain('## architect'); - } else { - expect(result.agents).toEqual([]); - } - }, 30_000); -}); diff --git a/tests/contract/agents/agent-test-catalog.test.ts b/tests/contract/agents/agent-test-catalog.test.ts deleted file mode 100644 index de6364b2..00000000 --- a/tests/contract/agents/agent-test-catalog.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { existsSync, mkdtempSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { runAgentTestCatalogChecks } from '../../../src/agents/testing/agent-test-catalog.ts'; -import { - compileEngineeringAgentTestDefinition, - compileEngineeringWorkflowPromotionConfig, - EngineeringAgentTestDefinitionError, -} from '../../../src/agents/testing/engineering-agent-test-definition.ts'; -import { - compileResearchAgentTestDefinition, - ResearchAgentTestDefinitionError, -} from '../../../src/agents/testing/research-agent-test-definition.ts'; - -describe('agent test catalog', () => { - it('validates Markdown-backed agent test specs and fixture paths', async () => { - const repoRoot = resolveRepoRoot(); - const result = await runAgentTestCatalogChecks({ - repoRoot, - now: new Date('2026-05-19T00:00:00.000Z'), - }); - - expect(result.ok).toBe(true); - if (existsSync(resolve(repoRoot, 'src/content/agent-tests'))) { - expect(result.entries.map((entry) => entry.agent)).toEqual(expect.arrayContaining([ - 'architect', - 'researcher', - 'technical-writer', - 'engineer', - 'reporter', - ])); - } else { - expect(result.entries).toEqual([]); - } - expect(existsSync(result.reportPath)).toBe(true); - expect(existsSync(result.jsonPath)).toBe(true); - }); - - it('validates first-party starter agent test catalogs', async () => { - const repoRoot = resolveRepoRoot(); - const starterRoots = [ - resolve(repoRoot, 'starters/research/template'), - resolve(repoRoot, 'starters/engineering/template'), - ]; - const availableStarterRoots = starterRoots - .filter((starterRoot) => existsSync(resolve(starterRoot, 'src/content/agent-tests'))); - - if (availableStarterRoots.length === 0) { - expect(starterRoots.some((starterRoot) => existsSync(starterRoot))).toBe(false); - return; - } - expect(availableStarterRoots).toHaveLength(starterRoots.length); - - for (const starterRoot of availableStarterRoots) { - const reportRoot = mkdtempSync(join(tmpdir(), 'treeseed-starter-agent-tests-')); - const result = await runAgentTestCatalogChecks({ - repoRoot: starterRoot, - reportPath: resolve(reportRoot, 'agent-test-catalog.md'), - now: new Date('2026-06-02T00:00:00.000Z'), - }); - - expect(result.ok).toBe(true); - expect(result.entries.length).toBeGreaterThan(0); - expect(result.entries.every((entry) => Object.keys(entry.trigger).length > 0 && Object.keys(entry.expect).length > 0)).toBe(true); - if (starterRoot.endsWith('/engineering/template')) { - expect(result.entries[0]).toMatchObject({ trigger: { workflowKind: 'engineering-test-first', approvedDecision: 'normalize-release-channel-inputs' } }); - const definition = compileEngineeringAgentTestDefinition(result.entries[0]!); - expect(definition).toMatchObject({ - workflowKind: 'engineering-test-first', - objectiveId: 'ship-the-first-guided-change', - approvedDecisionId: 'normalize-release-channel-inputs', - exactBaseRef: 'fixture-head', - requireRevisionCycle: true, - }); - expect(compileEngineeringWorkflowPromotionConfig(definition, { - projectId: 'project-engineering', resolvedExactBaseRef: '0123456789abcdef', - roles: { tester: 'testing', engineer: 'engineering', reviewer: 'review', technicalWriter: 'technical-writing', releaser: 'release', researcher: 'research', architect: 'architecture' }, - })).toMatchObject({ - schemaVersion: 1, id: 'engineering-parent-root', projectId: 'project-engineering', - decisionId: 'normalize-release-channel-inputs', objectiveId: 'ship-the-first-guided-change', - exactBaseRef: '0123456789abcdef', includeResearch: true, includeArchitecture: true, requireLinkedProposal: true, - }); - } else if (starterRoot.endsWith('/research/template')) { - expect(compileResearchAgentTestDefinition(result.entries[0]!)).toMatchObject({ - workflowKind: 'research-citation-review', - questionId: 'what-should-this-research-map-first', - sourcePolicyId: 'source-quality-criteria', - minimumIndependentSources: 2, - maxRevisionCycles: 3, - requireUnsupportedClaimRevision: true, - finalArtifactModel: 'knowledge', - requiredAgents: ['researcher', 'reviewer', 'technical-writer', 'reporter'], - }); - } - } - }); - - it('rejects an incomplete engineering workflow contract before service execution', () => { - expect(() => compileEngineeringAgentTestDefinition({ - id: 'invalid-engineering', agent: 'architect', kind: 'workday', fixture: 'src/lib/example.ts', - trigger: { workflowKind: 'engineering-test-first', objective: 'objective-a' }, - expect: { requiredAgents: ['architect'], requiredSequence: ['failing-test'], assertions: [] }, - sourcePath: '/fixture/invalid-engineering.mdx', status: 'PASS', issues: [], - })).toThrow(EngineeringAgentTestDefinitionError); - }); - - it('rejects an incomplete research workflow contract before service execution', () => { - expect(() => compileResearchAgentTestDefinition({ - id: 'invalid-research', agent: 'researcher', kind: 'workday', fixture: 'src/content/questions/question.mdx', - trigger: { workflowKind: 'research-citation-review', question: 'question-a' }, - expect: { requiredAgents: ['researcher'], requiredArtifacts: ['linked_note:evidence'], requiredSequence: ['question-decomposition'], assertions: [] }, - sourcePath: '/fixture/invalid-research.mdx', status: 'PASS', issues: [], - })).toThrow(ResearchAgentTestDefinitionError); - }); -}); - -function resolveRepoRoot() { - return new URL('../../..', import.meta.url).pathname; -} diff --git a/tests/contract/agents/content-tool-validation-atomicity.test.ts b/tests/contract/agents/content-tool-validation-atomicity.test.ts deleted file mode 100644 index 5cb916f3..00000000 --- a/tests/contract/agents/content-tool-validation-atomicity.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { callContentTool } from '../../../src/agents/tools/content-tool-runtime.ts'; - -function descriptor() { - return { - id: 'treeseed.content.create', handleId: 'handle-a', projectId: 'project-a', - assignmentId: 'assignment-a', workspaceId: 'workspace-a', - allowedOperations: ['files:write', 'files:read'], allowedPaths: ['src/content/**'], - routes: { - applyWorkspaceChangeset: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/changesets', - readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path', - }, - metadata: { contentAction: 'create', contentRoot: 'src/content', baseCommitSha: 'abc123', baseRef: 'refs/heads/main' }, - } as never; -} - -function scopedProposalDescriptor() { - return { - ...descriptor(), - metadata: { ...descriptor().metadata, allowedProposalTypes: ['implementation'] }, - } as never; -} - -function modelScopedDescriptor() { - return { - ...descriptor(), - metadata: { - ...descriptor().metadata, - permissionSummary: { - readModels: ['objective'], readActions: ['describe', 'query', 'read'], readPaths: ['src/content/objectives/**'], - writeModels: ['note'], writeActions: ['create', 'update', 'link', 'validate'], writePaths: ['src/content/notes/**'], - commitAllowed: false, - }, - }, - } as never; -} - -const validProposalFields = { - description: 'A bounded self-hosting milestone.', - date: '2026-08-12', - status: 'planned', - summary: 'Prove one complete governed loop.', - proposalType: 'implementation', - motivation: 'The internal development loop needs live evidence.', - primaryContributor: 'self-hosting-architect', -}; - -describe('model-aware content mutation atomicity', () => { - it('rejects a call-time model outside the immutable per-model permission snapshot', async () => { - const fetchImpl = vi.fn(async () => { throw new Error('unauthorized model must not reach TreeDX'); }); - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: modelScopedDescriptor(), fetchImpl: fetchImpl as typeof fetch, - input: { - model: 'decision', slug: 'forbidden-decision', title: 'Forbidden decision', - fields: {}, body: 'This mutation is outside the profile authority.', - }, - }); - expect(result).toEqual({ - ok: false, - code: 'content_model_operation_denied', - message: 'Content create for decision is outside the immutable assignment permission snapshot.', - metadata: { - action: 'create', model: 'decision', - allowedActions: ['create', 'update', 'link', 'validate'], allowedModels: ['note'], - }, - }); - expect(fetchImpl).not.toHaveBeenCalled(); - }); - - it('returns field-addressable Zod diagnostics without staging an invalid create', async () => { - const fetchImpl = vi.fn(async () => { throw new Error('invalid content must not reach TreeDX'); }); - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: descriptor(), fetchImpl: fetchImpl as typeof fetch, - input: { - model: 'proposal', slug: 'governed-loop', title: 'Governed loop', body: 'Implement and verify the bounded loop.', - fields: { ...validProposalFields, status: 'draft', proposalType: 'structural', motivation: '' }, - }, - }); - expect(result).toMatchObject({ - ok: false, code: 'content_validation_failed', - metadata: { diagnostics: expect.arrayContaining([ - expect.objectContaining({ code: 'content_zod_invalid_enum_value', field: 'status' }), - expect.objectContaining({ field: 'motivation' }), - ]) }, - }); - expect(fetchImpl).not.toHaveBeenCalled(); - }); - - it('does not stage a Zod-valid proposal until its submission plan is complete', async () => { - const fetchImpl = vi.fn(async () => { throw new Error('incomplete proposal must not reach TreeDX'); }); - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: descriptor(), fetchImpl: fetchImpl as typeof fetch, - input: { model: 'proposal', slug: 'governed-loop', title: 'Governed loop', body: 'Implement the loop.', fields: validProposalFields }, - }); - expect(result).toMatchObject({ ok: false, code: 'proposal_plan_incomplete' }); - expect(fetchImpl).not.toHaveBeenCalled(); - }); - - it('does not stage a record whose relation target is not the current canonical identity', async () => { - const calls: string[] = []; - const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { - calls.push(`${init?.method ?? 'GET'} ${String(url)}`); - if (String(url).includes('/search')) return Response.json({ ok: true, payload: { results: [] } }); - throw new Error('invalid relation must not reach a TreeDX changeset'); - }); - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { - ...descriptor(), allowedOperations: ['files:write', 'files:read', 'files:search'], - routes: { ...descriptor().routes, searchWorkspace: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/search' }, - } as never, - fetchImpl: fetchImpl as typeof fetch, - input: { - model: 'note', slug: 'invalid-link', title: 'Invalid link', body: 'Must remain unstaged.', - relations: [{ field: 'relatedObjectives', targetModel: 'objective', targetSlug: 'objective:missing' }], - }, - }); - expect(result).toMatchObject({ ok: false, code: 'content_relation_invalid' }); - expect(calls.some((call) => call.includes('/changesets'))).toBe(false); - }); - - it('rejects unfrozen proposal types before staging a TreeDX mutation', async () => { - const fetchImpl = vi.fn(async () => { throw new Error('semantically unauthorized content must not reach TreeDX'); }); - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: scopedProposalDescriptor(), fetchImpl: fetchImpl as typeof fetch, - input: { - model: 'proposal', slug: 'governed-loop', title: 'Governed loop', body: 'Implement and verify the bounded loop.', - fields: { - ...validProposalFields, - proposalTypes: ['implementation', 'documentation-automation'], - plan: { - desiredOutcome: 'One governed loop works end to end.', currentProblem: 'Repository outcomes are not yet proven.', - proposedApproach: 'Implement and verify one bounded change.', scope: ['one project'], nonGoals: ['hosted release'], - deliverables: ['validated change'], acceptanceCriteria: ['repository read-back passes'], risks: ['stale authority'], - dependencies: ['accepted decision'], alternatives: ['remain fail-closed'], verification: ['inspect the exact ref'], - }, - }, - }, - }); - expect(result).toMatchObject({ - ok: false, code: 'proposal_type_not_allowed', - metadata: { unsupported: ['documentation-automation'], allowedProposalTypes: ['implementation'] }, - }); - expect(fetchImpl).not.toHaveBeenCalled(); - }); -}); diff --git a/tests/contract/agents/execution-content-contract.test.ts b/tests/contract/agents/execution-content-contract.test.ts deleted file mode 100644 index 306cd78a..00000000 --- a/tests/contract/agents/execution-content-contract.test.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { renderContentRecord } from '@treeseed/sdk/content-operations'; -import { contentModelSupportsArtifactKind, executionAgentForAccess } from '../../../src/agents/handlers/execution-content.ts'; -import { executionContentRoot, resolveExecutionTreeDxContext } from '../../../src/agents/handlers/execution-content-context.ts'; -import { buildExecutionContentInstructions } from '../../../src/agents/handlers/execution-content-prompt.ts'; -import { callContentTool } from '../../../src/agents/tools/content-tool-runtime.ts'; -import { collectExecutionContentArtifactReceipts } from '../../../src/agents/handlers/execution-content-artifacts.ts'; -import { writerHandler } from '../../../src/agents/handlers/writer.ts'; -describe('execution content artifact contract', () => { - it('does not treat a blocking question as the required planning note', () => { - expect(contentModelSupportsArtifactKind('question', 'planning_note')).toBe(false); - expect(contentModelSupportsArtifactKind('question', 'planning_question')).toBe(true); - }); - it('maps semantic note outputs only to note content models', () => { - for (const artifactKind of [ - 'planning_note', - 'proposal_feedback_note', - 'proposal_estimate', - 'question_answer', - 'decision_feedback', - 'workday_summary', - ]) { - expect(contentModelSupportsArtifactKind('note', artifactKind)).toBe(true); - expect(contentModelSupportsArtifactKind('proposal', artifactKind)).toBe(false); - } - expect(contentModelSupportsArtifactKind('proposal', 'planning_proposal')).toBe(true); - expect(contentModelSupportsArtifactKind('knowledge', 'knowledge_page')).toBe(true); - expect(contentModelSupportsArtifactKind('knowledge', 'knowledge_update')).toBe(true); - expect(contentModelSupportsArtifactKind('note', 'knowledge_update')).toBe(false); - }); - it('coalesces repeated TreeDX receipts by path and preserves the final subject relation', () => { - const result = collectExecutionContentArtifactReceipts({ - agent: { slug: 'tester' }, capacity: { assignmentId: 'assignment-a' }, - } as never, { - artifacts: [{ - kind: 'treedx_content_receipt', metadata: { contentRef: { model: 'note', path: 'notes/dummy.mdx' } }, - }, { - kind: 'treedx_content_receipt', metadata: { contentRef: { model: 'note', path: 'notes/dummy.mdx', subjectId: 'decision-a', subjectField: 'relatedDecisions', commitSha: 'abc123', ref: 'refs/heads/assignment-a' } }, - }], - } as never, 'revision_verification'); - expect(result).toEqual([expect.objectContaining({ - contentPath: 'notes/dummy.mdx', subjectId: 'decision-a', subjectField: 'relatedDecisions', commitSha: 'abc123', - })]); - }); - it('preserves configured source-write authority only for acting handlers', () => { - const context = { agent: { execution: { sandboxMode: 'workspace_write', allowedPaths: ['src/**'] } } } as never; - expect(executionAgentForAccess(context, 'configured').execution).toMatchObject({ sandboxMode: 'workspace_write', allowedPaths: ['src/**'] }); - expect(executionAgentForAccess(context, 'read_only').execution).toMatchObject({ sandboxMode: 'read_only', allowedPaths: [] }); - }); - it('passes configured workspace authority to the writer execution provider', async () => { - let executionAgent: Record | null = null; - const context = { agent: { slug: 'guide-steward', execution: { sandboxMode: 'workspace_write', allowedPaths: ['src/content/**'] } }, - capacity: { assignmentId: 'assignment-a', assignment: { id: 'assignment-a' }, envelope: {}, decisionInput: {}, workspaceAccessMode: 'workspace_write' }, - execution: { start: async (input: { agent: Record }) => { executionAgent = input.agent; return { status: 'completed', summary: 'done', outputs: {}, artifacts: [] }; } } } as never; - await writerHandler.execute(context, { payload: {}, subject: { model: 'objective', id: 'core', title: 'Core' }, artifactKind: 'agent_note', nextMessageTypes: [], - workPackage: { kind: 'writer', title: 'Write', summary: 'Write', instructions: 'Write', context: {}, expectedOutputs: [], - constraints: { mode: 'acting', requiredCapabilities: [], allowedPaths: ['src/content/**'], forbiddenPaths: [], metadata: {} }, metadata: {} } } as never); - expect(executionAgent).toMatchObject({ execution: { sandboxMode: 'workspace_write', allowedPaths: ['src/content/**'] } }); - }); - it('uses assignment content architecture instead of inferring from provider-local paths', () => { - const context = { - repoRoot: '/provider/data/assignment-contexts/project-a', - capacity: { - assignment: { - metadata: { contentRoot: 'template/src/content' }, - workspaceContext: { project: { architecture: { contentPath: 'template/src/content' } } }, - }, - }, - } as never; - expect(executionContentRoot(context, {})).toBe('template/src/content'); - expect(executionContentRoot(context, { contentRoot: 'configured/content' })).toBe('configured/content'); - }); - it('resolves content extensions in order without batching a missing alternative', async () => { - const reads: string[][] = []; - const workspaceSearch = vi.fn(async () => ({ results: [] })); - const context = { - repoRoot: '/provider/data/assignment-contexts/project-a', - agent: { slug: 'architect', context: { queries: [] } }, - capacity: { - assignmentId: 'assignment-a', - treedxProxyHandle: { repositoryId: 'repo-a', workspaceId: 'workspace-a' }, - assignment: { metadata: { contentRoot: 'template/src/content' } }, - }, - treeDx: { - searchWorkspace: workspaceSearch, - async readRepositoryFiles(input: { paths: string[] }) { - reads.push(input.paths); - const path = input.paths[0]!; - return { payload: { files: [{ path, text: `content for ${path}` }] } }; - }, - }, - } as never; - const result = await resolveExecutionTreeDxContext(context, { model: 'objective', id: 'objective-a', title: 'Objective A' }, { objectiveId: 'objective-a' }); - expect(reads).toEqual([ - ['template/src/content/objectives/objective-a.mdx'], - ['template/src/content/agents/architect.mdx'], - ]); - expect(result.assignedObjective).toMatchObject({ path: 'template/src/content/objectives/objective-a.mdx' }); - expect(result.diagnostics.warnings).toEqual([]); - expect(workspaceSearch).not.toHaveBeenCalled(); - }); - it('reads and validates exact assignment instruction templates through TreeDX',async()=>{ - const reads:string[]=[]; - const context={repoRoot:'/provider/project',agent:{slug:'architect',context:{queries:[]}},capacity:{assignmentId:'assignment-a',treedxProxyHandle:{repositoryId:'repo-a'},assignment:{metadata:{contentRoot:'src/content'}}},treeDx:{ - async listRepositoryPaths(){return {entries:[{path:'src/content/objectives/core.mdx'},{path:'src/content/agents/architect.mdx'}]};}, - async readRepositoryFiles(input:{paths:string[]}) { const path=input.paths[0]!; reads.push(path); const template=path.includes('/agent-instruction-templates/'); return {payload:{files:[{path,text:template?'---\nid: plan-standard\ntitle: Plan\ndescription: Plan instructions\nrevision: 2\nkind: plan\ninstructions: Keep the checklist current.\nvariables: []\nappliesToProfiles: [planning]\n---\n':'content'}]}}; }, - }} as never; - const result=await resolveExecutionTreeDxContext(context,{model:'objective',id:'core',title:'Core'},{objectiveId:'core',instructionTemplateRefs:[{id:'plan-standard',revision:2}]}); - expect(result.instructionTemplates).toEqual([expect.objectContaining({id:'plan-standard',revision:2,kind:'plan',instructions:'Keep the checklist current.'})]); - expect(reads).toContain('src/content/agent-instruction-templates/plan-standard.mdx'); - }); - it('reads assignment-authoritative objective and agent paths before compatibility candidates', async () => { - const reads: string[] = []; - const context = { - repoRoot: '/provider/data/assignment-contexts/market', - agent: { slug: 'guide-steward', activityType: 'planning', context: { queries: [] } }, - capacity: { assignmentId: 'assignment-guide', treedxProxyHandle: { repositoryId: 'repo-market' }, - assignment: { metadata: { contentRoot: 'src/content', agentContentPath: 'src/content/agents/editorial/guide-steward.mdx' } } }, - treeDx: { async readRepositoryFiles(input: { paths: string[] }) { - const path = input.paths[0]!; reads.push(path); - return { payload: { files: [{ path, text: `content for ${path}`, sha: 'a'.repeat(40) }] } }; - } }, - } as never; - await resolveExecutionTreeDxContext(context, - { model: 'objective', id: 'core', title: 'Core', path: 'src/content/objectives/core.md' }, - { objectiveId: 'core', subjectPath: 'src/content/objectives/core.md' }); - expect(reads).toContain('src/content/objectives/core.md'); - expect(reads).toContain('src/content/agents/editorial/guide-steward.mdx'); - expect(reads).not.toContain('src/content/objectives/core.mdx'); - expect(reads).not.toContain('src/content/agents/guide-steward.mdx'); - }); - it('discovers repository paths before reading and never probes an absent extension', async () => { - const reads: string[] = []; - const paths = [ - 'src/content/objectives/core.md', - 'src/content/notes/editorial/core.mdx', - 'src/content/notes/editorial/books/treeseed-guide/core.mdx', - 'src/content/agents/editorial/guide-steward.mdx', - ]; - const result = await resolveExecutionTreeDxContext({ - repoRoot: '/provider/data/assignment-contexts/market', - agent: { slug: 'guide-steward', activityType: 'planning', context: { queries: [] } }, - capacity: { assignmentId: 'assignment-guide', treedxProxyHandle: { repositoryId: 'repo-market' }, - assignment: { metadata: { contentRoot: 'src/content' } } }, - treeDx: { - async listRepositoryPaths() { return { entries: paths.map((path) => ({ path })) }; }, - async buildContext() { return { results: [] }; }, - async readRepositoryFiles(input: { paths: string[] }) { - const path = input.paths[0]!; reads.push(path); - return { payload: { files: [{ path, text: `content for ${path}`, sha: 'a'.repeat(40) }] } }; - }, - }, - } as never, { model: 'objective', id: 'core', title: 'Core' }, { objectiveId: 'core' }); - expect(reads).toEqual(paths); - expect(reads).not.toContain('src/content/objectives/core.mdx'); - expect(result.diagnostics.warnings).toEqual([]); - }); - it('compiles revision-pinned editorial layers through TreeDX for Guide agents', async () => { - const context = { - repoRoot: '/provider/data/assignment-contexts/market', - agent: { slug: 'guide-writer', activityType: 'acting', context: { queries: [] } }, - capacity: { assignmentId: 'assignment-guide', treedxProxyHandle: { repositoryId: 'repo-market' }, - assignment: { metadata: { contentRoot: 'src/content' } } }, - treeDx: { - async readRepositoryFiles(input: { paths: string[] }) { - const path = input.paths[0]!; - return { payload: { files: [{ path, text: `editorial content for ${path}`, sha: path.includes('/core.') ? 'a'.repeat(40) : 'b'.repeat(40) }] } }; - }, - }, - } as never; - const result = await resolveExecutionTreeDxContext(context, - { model: 'knowledge', id: 'guide.deployment.knowledge', title: 'Knowledge', - path: 'src/content/knowledge/treeseed-guide/deployment/knowledge.md' }, - { bookId: 'treeseed-guide' }); - expect(result.editorialContext?.layers.map((layer) => layer.kind)).toEqual([ - 'core-objective', 'project-core', 'book-core', 'chapter-brief', 'target-page', 'assignment', - ]); - expect(result.editorialContext?.digest).toMatch(/^[a-f0-9]{64}$/u); - expect(result.diagnostics).toMatchObject({ editorialContextSchemaVersion: 'treeseed.editorial-context/v1', - declarativeContextPackCount: 1 }); - }); - it('allows Guide planning to orient from the editorial cores before a chapter is selected', async () => { - const context = { - repoRoot: '/provider/data/assignment-contexts/market', - agent: { slug: 'guide-steward', activityType: 'planning', context: { queries: [] } }, - capacity: { assignmentId: 'assignment-guide-plan', treedxProxyHandle: { repositoryId: 'repo-market' }, - assignment: { metadata: { contentRoot: 'src/content' } } }, - treeDx: { - async readRepositoryFiles(input: { paths: string[] }) { - const path = input.paths[0]!; - return { payload: { files: [{ path, text: `editorial content for ${path}`, sha: 'a'.repeat(40) }] } }; - }, - }, - } as never; - const result = await resolveExecutionTreeDxContext(context, - { model: 'objective', id: 'core', title: 'Core TreeSeed Objective' }, - { objectiveId: 'core' }); - expect(result.editorialContext?.layers.map((layer) => layer.kind)).toEqual([ - 'core-objective', 'project-core', 'book-core', 'assignment', - ]); - }); - it('allows a cross-chapter proposal review without inventing a Guide chapter', async () => { - const reads: string[] = []; - const context = { - repoRoot:'/provider/data/assignment-contexts/market',agent:{ slug:'audience-reviewer',activityType:'reviewing',context:{ queries:[] } }, - capacity:{ assignmentId:'assignment-review',treedxProxyHandle:{ repositoryId:'repo-market' },assignment:{ metadata:{ contentRoot:'src/content' } } }, - treeDx:{ async readRepositoryFiles(input:{ paths:string[] }) { const path = input.paths[0]!; reads.push(path); return { payload:{ files:[{ path,text:`content for ${path}`,sha:'a'.repeat(40) }] } }; } }, - } as never; - await expect(resolveExecutionTreeDxContext(context, - { model:'proposal',id:'guide-cohort',title:'Guide Cohort',path:'src/content/proposals/editorial/books/treeseed-guide/guide-cohort.mdx' }, - { subjectPath:'src/content/proposals/editorial/books/treeseed-guide/guide-cohort.mdx' }, - )).resolves.toBeDefined(); - expect(reads).toContain('src/content/proposals/editorial/books/treeseed-guide/guide-cohort.mdx'); - }); - it('gives the provider the assignment-owned content root and exact subject relation', () => { - const instructions = buildExecutionContentInstructions({ - agent: { systemPrompt: 'Architect prompt.' }, - capacity: { mode: 'acting', assignmentId: 'assignment-a' }, - } as never, { - payload: { decisionId: 'decision-a' }, - subject: { model: 'decision', id: 'decision-a', title: 'Decision A' }, - artifactKind: 'architecture_plan', - contextPackSummaries: [], - assignedObjective: null, - contentRoot: 'template/src/content', - }); - expect(instructions).toContain('Content root: template/src/content'); - expect(instructions).toContain('exact changedPaths entry back as placement.path'); - expect(instructions).toContain('Never batch-read inferred repository paths'); - expect(instructions).toContain('Dynamic context-query result payloads are transient execution input'); - expect(instructions).toContain('never stored or committed as query results'); - expect(instructions).toContain('call treeseed.content.describe with that model'); - expect(instructions).toContain('exact canonical id returned in its create receipt'); - expect(instructions).toContain('resolves every stored relation against current TreeDX workspace data'); - expect(instructions).toContain('Never defer the first plan update until the closeout threshold'); - expect(instructions).toContain('commit exactly once as the final mutating action'); - expect(instructions).toContain('not as remaining editorial scope'); - expect(instructions).toContain('"field":"relatedDecisions"'); - expect(instructions).not.toContain('Content root: src/content'); - }); - it('describes canonical model enums before an agent writes content', async () => { - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.describe', handleId: 'handle-a', allowedPaths: ['src/content/**'], - allowedReadPaths: ['src/content/**'], allowedWritePaths: [], routes: {}, - metadata: { contentAction: 'describe', contentRoot: 'src/content' }, - } as never, - input: { model: 'question' }, - }); - expect(result).toMatchObject({ ok: true, payload: { model: 'question', fields: { - status: { type: 'string', values: ['live', 'in progress', 'exploratory', 'planned', 'speculative'] }, - question_type: { type: 'string', values: ['research', 'implementation', 'strategy', 'evaluation', 'knowledge-gap'] }, - } } }); - }); - it('distinguishes red-proof work from post-implementation verification', () => { - const instructionsFor = (artifactKind: string) => buildExecutionContentInstructions({ - agent: { systemPrompt: 'Tester prompt.' }, - capacity: { mode: 'acting', assignmentId: 'assignment-a' }, - } as never, { - payload: {}, - subject: { model: 'decision', id: 'decision-a', title: 'Decision A' }, - artifactKind, - contextPackSummaries: [], - assignedObjective: null, - contentRoot: 'template/src/content', - }); - expect(instructionsFor('failing_test_proof')).toContain('nonzero expected exit code'); - expect(instructionsFor('failing_test_proof')).toContain('Checkpoint the authored test'); - expect(instructionsFor('passing_verification')).toContain('expected exit code 0'); - expect(instructionsFor('passing_verification')).toContain('Do not create a new red test'); - expect(instructionsFor('passing_verification')).toContain('or create a source checkpoint'); - expect(instructionsFor('review_decision')).toContain('authenticated governedPredecessorEvidence'); - expect(instructionsFor('review_decision')).toContain('Do not require documentation, release readiness'); - const requiredRevision = buildExecutionContentInstructions({ - agent: { systemPrompt: 'Reviewer prompt.' }, - capacity: { mode: 'acting', assignmentId: 'assignment-a' }, - } as never, { - payload: { governedReviewPolicy: { requireRevisionCycle: true, completedRevisionCycles: 0, requiredDisposition: 'rejected' } }, - subject: { model: 'decision', id: 'decision-a', title: 'Decision A' }, - artifactKind: 'review_decision', - contextPackSummaries: [], - assignedObjective: null, - contentRoot: 'template/src/content', - }); - expect(requiredRevision).toContain('requires this initial review to reject once'); - expect(requiredRevision).toContain('Do not invent a downstream prerequisite'); - }); - it('gives every governed research stage its authenticated output contract', () => { - const instructionsFor = (researchStage: string) => buildExecutionContentInstructions({ - agent: { systemPrompt: 'Research prompt.' }, - capacity: { mode: 'planning', assignmentId: 'assignment-research' }, - } as never, { - payload: { - researchStage, minimumIndependentSources: 2, maxRevisionCycles: 3, - latestReviewAttempt: { reason: 'The prior claim exceeded the cited evidence.' }, - }, - subject: { model: 'question', id: 'question-a', title: 'Question A' }, - artifactKind: researchStage === 'question-decomposition' ? 'planning_question' - : researchStage === 'cited-knowledge-publication' ? 'knowledge_page' : 'planning_note', - contextPackSummaries: [], - assignedObjective: null, - contentRoot: 'template/src/content', - }); - expect(instructionsFor('question-decomposition')).toContain('Create a new question model artifact'); - expect(instructionsFor('question-decomposition')).toContain('A note cannot satisfy the required planning_question artifact'); - expect(instructionsFor('independent-source-fetch')).toContain('research.fetch_source'); - expect(instructionsFor('independent-source-fetch')).toContain('claimIds ["claim-1"]'); - expect(instructionsFor('claim-synthesis')).toContain('treeseed.research_claims'); - expect(instructionsFor('claim-synthesis')).toContain('status is "unsupported"'); - expect(instructionsFor('citation-review-rejection')).toContain('disposition "rejected"'); - expect(instructionsFor('revision')).toContain('at most 3 revision cycles'); - expect(instructionsFor('revision')).toContain('The prior claim exceeded the cited evidence.'); - expect(instructionsFor('revision')).toContain('exact text is no broader than facts established'); - expect(instructionsFor('citation-review-approval')).toContain('disposition "approved"'); - }); - it('rejects a content link that contains no validated relation', async () => { - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', - providerAccessToken: 'redacted', - assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.link', handleId: 'handle-a', routes: {}, - metadata: { contentAction: 'link', contentRoot: 'template/src/content' }, - } as never, - input: { model: 'note', slug: 'architecture-plan', relations: [] }, - }); - expect(result).toMatchObject({ ok: false, code: 'content_relation_required' }); - }); - it('keeps model-aware writes under the transported assignment root when given a path-shaped slug', async () => { - const urls: string[] = []; - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', - providerAccessToken: 'redacted', - assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.create', - handleId: 'handle-a', - projectId: 'project-a', - assignmentId: 'assignment-a', - workspaceId: 'workspace-a', - allowedOperations: ['files:write', 'files:read', 'files:search'], - allowedPaths: ['template/src/content/**'], - routes: { - applyWorkspaceChangeset: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/changesets', - readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path', searchWorkspace: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/search', - }, - metadata: { contentAction: 'create', contentRoot: 'template/src/content', baseCommitSha: 'abc123', baseRef: 'refs/heads/main' }, - } as never, - input: { - model: 'note', - slug: 'template/src/content/notes/release-channel-evidence.mdx', - title: 'Release channel evidence', - body: 'Evidence body.', - relations: [{ field: 'relatedDecisions', targetSlug: 'normalize-release-channel-inputs' }], - }, - fetchImpl: (async (url) => { - urls.push(String(url)); if (String(url).includes('/search')) return Response.json({ ok: true, payload: { results: [{ path: 'template/src/content/decisions/normalize-release-channel-inputs.mdx' }] } }); - if (String(url).includes('decisions%2Fnormalize-release-channel-inputs')) return Response.json({ ok: true, payload: { content: '---\nid: decision:normalize-release-channel-inputs\ntitle: Normalize release channel inputs\nstatus: accepted\n---\nDecision.' } }); - return new Response(JSON.stringify({ ok: true, payload: {} }), { - status: 200, - headers: { 'content-type': 'application/json' }, - }); - }) as typeof fetch, - }); - expect(result).toMatchObject({ - ok: true, - changedPaths: ['template/src/content/notes/release-channel-evidence.mdx'], - }); - expect(urls).toHaveLength(5); - expect(urls.some((url) => url.includes('path=template%2Fsrc%2Fcontent%2Fnotes%2Frelease-channel-evidence.mdx'))).toBe(true); - expect(urls.some((url) => url.includes('/changesets'))).toBe(true); - }); - it('treats an interrupted create retry as one logical mutation when authoritative content matches', async () => { - const existing = [ - '---', - 'title: Release channel evidence', - 'id: note:release-channel-evidence', - 'updated_at: 2026-08-14T11:00:00.000Z', - '---', - 'Evidence body.', - ].join('\n'); - const fetchImpl = vi.fn(async () => Response.json({ ok:true,payload:{ content:existing } })); - const result = await callContentTool({ - apiBaseUrl:'http://127.0.0.1:3000',providerAccessToken:'redacted',assignmentId:'assignment-a', - descriptor:{ id:'treeseed.content.create',handleId:'handle-a',projectId:'project-a',assignmentId:'assignment-a',workspaceId:'workspace-a',allowedOperations:['files:write','files:read'],allowedPaths:['src/content/**'],routes:{ applyWorkspaceChangeset:'POST /changesets',readWorkspaceFile:'GET /files?path=:path' },metadata:{ contentAction:'create',contentRoot:'src/content',baseCommitSha:'abc123',baseRef:'refs/heads/main' } } as never, - input:{ model:'note',slug:'release-channel-evidence',title:'Release channel evidence',body:'Evidence body.' },fetchImpl:fetchImpl as typeof fetch, - }); - expect(result).toMatchObject({ ok:true,idempotentReplay:true,changedPaths:['src/content/notes/release-channel-evidence.mdx'] }); - expect(fetchImpl).toHaveBeenCalledTimes(1); - }); - it('accepts a canonical placement path without duplicating the content root and collection', async () => { - const urls: string[] = []; - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.create', handleId: 'handle-a', projectId: 'project-a', - assignmentId: 'assignment-a', workspaceId: 'workspace-a', - allowedOperations: ['files:write', 'files:read', 'files:search'], allowedPaths: ['src/content/**'], - routes: { - applyWorkspaceChangeset: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/changesets', - readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path', searchWorkspace: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/search', - }, - metadata: { contentAction: 'create', contentRoot: 'src/content', baseCommitSha: 'abc123', baseRef: 'refs/heads/main' }, - } as never, - input: { - model: 'note', slug: 'audience-review-plan', title: 'Audience review plan', body: 'Review body.', - placement: { path: 'src/content/notes/editorial/books/treeseed-guide/reviews/audience-review-plan.mdx' }, - relations: [{ field: 'about', targetSlug: 'objective:core' }], - }, - fetchImpl: (async (url) => { - urls.push(String(url)); if (String(url).includes('/search')) return Response.json({ ok: true, payload: { results: [{ path: 'src/content/objectives/core.mdx' }] } }); - if (String(url).includes('objectives%2Fcore')) return Response.json({ ok: true, payload: { content: '---\nid: objective:core\ntitle: Core\nstatus: live\n---\nObjective.' } }); - return new Response(JSON.stringify({ ok: true, payload: {} }), { - status: 200, headers: { 'content-type': 'application/json' }, - }); - }) as typeof fetch, - }); - expect(result).toMatchObject({ - ok: true, - changedPaths: ['src/content/notes/editorial/books/treeseed-guide/reviews/audience-review-plan.mdx'], - }); - expect(urls.some((url) => url.includes('path=src%2Fcontent%2Fnotes%2Feditorial%2Fbooks%2Ftreeseed-guide%2Freviews%2Faudience-review-plan.mdx'))).toBe(true); - expect(urls.some((url) => url.includes('/changesets'))).toBe(true); - }); - it('preserves a canonical Guide knowledge path through model-aware creation', async () => { - const urls: string[] = []; - const path = 'src/content/knowledge/treeseed-guide/foundation/agent-lab-guide-writing.md'; - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { id: 'treeseed.content.create', handleId: 'handle-a', projectId: 'project-a', assignmentId: 'assignment-a', workspaceId: 'workspace-a', allowedOperations: ['files:write', 'files:read'], allowedPaths: ['src/content/**'], routes: { applyWorkspaceChangeset: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/changesets', readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path' }, metadata: { contentAction: 'create', contentRoot: 'src/content', baseCommitSha: 'abc123', baseRef: 'refs/heads/main' } } as never, - input: { - model: 'knowledge', slug: 'agent-lab-guide-writing', title: 'Agent Lab Guide Writing', - fields: { - schemaVersion: 'treeseed.knowledge-page/v1', id: 'knowledge:agent-lab-guide-writing', book_id: 'treeseed-guide', - summary: 'Guide writing through the Agent Lab.', status: 'draft', visibility: 'public', - }, - placement: { path }, body: 'Guide body.', - }, - fetchImpl: (async (url) => { urls.push(String(url)); return new Response(JSON.stringify({ ok: true, payload: {} }), { status: 200, headers: { 'content-type': 'application/json' } }); }) as typeof fetch, - }); - expect(result).toMatchObject({ ok: true, changedPaths: [path] }); - expect(urls.some((url) => url.includes(`path=${encodeURIComponent(path)}`))).toBe(true); - expect(urls.some((url) => url.includes('/changesets'))).toBe(true); - }); - it('reads the assigned agent from its authoritative nested content path', async () => { - const urls: string[] = []; - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.read', handleId: 'handle-a', projectId: 'project-a', assignmentId: 'assignment-a', workspaceId: 'workspace-a', - allowedOperations: ['files:read', 'files:search'], allowedPaths: ['src/content/**'], - routes: { readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path' }, - metadata: { contentAction: 'read', contentRoot: 'src/content', agentSlug: 'guide-steward', agentContentPath: 'src/content/agents/editorial/guide-steward.mdx' }, - } as never, - input: { model: 'agent', id: 'guide-steward' }, - fetchImpl: (async (url) => { - urls.push(String(url)); - return new Response(JSON.stringify({ ok: true, payload: { content: 'Guide Steward' } }), { status: 200, headers: { 'content-type': 'application/json' } }); - }) as typeof fetch, - }); - expect(result).toMatchObject({ ok: true, refs: [{ path: 'src/content/agents/editorial/guide-steward.mdx' }] }); - expect(urls).toHaveLength(1); - expect(urls[0]).toContain('path=src%2Fcontent%2Fagents%2Feditorial%2Fguide-steward.mdx'); - }); - it('reads a hierarchically placed content record from its explicit receipt path', async () => { - const urls: string[] = []; - const path = 'src/content/notes/editorial/books/treeseed-guide/planning/stewardship/run-note.mdx'; - const result = await callContentTool({ apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { id: 'treeseed.content.read', handleId: 'handle-a', projectId: 'project-a', assignmentId: 'assignment-a', workspaceId: 'workspace-a', allowedOperations: ['files:read'], allowedPaths: ['src/content/**'], routes: { readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path' }, metadata: { contentAction: 'read', contentRoot: 'src/content' } } as never, - input: { model: 'note', path }, fetchImpl: (async (url) => { urls.push(String(url)); return new Response(JSON.stringify({ ok: true, payload: { content: 'Run note' } }), { status: 200, headers: { 'content-type': 'application/json' } }); }) as typeof fetch }); - expect(result).toMatchObject({ ok: true, refs: [{ path }] }); - expect(urls[0]).toContain(encodeURIComponent(path)); - }); - it('resolves a model identity to its current nested repository path when the canonical flat path is absent', async () => { - const urls: string[] = []; - const nestedPath = 'src/content/questions/editorial/books/treeseed-guide/what-needs-proof.mdx'; - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.read', handleId: 'handle-a', projectId: 'project-a', assignmentId: 'assignment-a', workspaceId: 'workspace-a', - allowedOperations: ['files:read','files:search'], allowedPaths: ['src/content/**'], - routes: { readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path', searchWorkspace: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/search' }, - metadata: { contentAction: 'read', contentRoot: 'src/content' }, - } as never, - input: { model: 'question', id: 'what-needs-proof' }, - fetchImpl: (async (url) => { - urls.push(String(url)); - const value = String(url); - if (value.includes('/search')) return new Response(JSON.stringify({ ok: true, payload: { results: [{ path: nestedPath }] } }), { status: 200, headers: { 'content-type': 'application/json' } }); - if (value.includes(encodeURIComponent(nestedPath))) return new Response(JSON.stringify({ ok: true, payload: { content: 'Question body' } }), { status: 200, headers: { 'content-type': 'application/json' } }); - return new Response(JSON.stringify({ code: 'not_found' }), { status: 404, headers: { 'content-type': 'application/json' } }); - }) as typeof fetch, - }); - expect(result).toMatchObject({ ok: true, refs: [{ path: nestedPath }] }); - expect(urls.some((url) => url.includes('/search'))).toBe(true); - expect(urls.some((url) => url.includes(encodeURIComponent(nestedPath)))).toBe(true); - }); -}); diff --git a/tests/contract/agents/execution-content-validation-contract.test.ts b/tests/contract/agents/execution-content-validation-contract.test.ts deleted file mode 100644 index ea5c4d2f..00000000 --- a/tests/contract/agents/execution-content-validation-contract.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { renderContentRecord } from '@treeseed/sdk/content-operations'; -import { describe,expect,it,vi } from 'vitest'; -import { callContentTool } from '../../../src/agents/tools/content-tool-runtime.ts'; - -describe('execution content validation contract',()=>{ - it('validates the existing record at an exact hierarchical placement', async () => { - const urls: string[] = []; - const objective = renderContentRecord({ model: 'objective', slug: 'core', title: 'Core', body: 'Objective.' }); - const existing = renderContentRecord({ - model: 'note', title: 'Technical review plan', body: 'Review body.', - relations: [{ field: 'relatedObjectives', targetSlug: 'objective:core' }], - }); - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.validate', handleId: 'handle-a', projectId: 'project-a', - assignmentId: 'assignment-a', workspaceId: 'workspace-a', - allowedOperations: ['files:read', 'files:search'], allowedPaths: ['src/content/**'], - routes: { readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path', searchWorkspace: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/search' }, - metadata: { contentAction: 'validate', contentRoot: 'src/content' }, - } as never, - input: { - model: 'note', - placement: { path: 'src/content/notes/editorial/books/treeseed-guide/reviews/technical-review-plan.mdx' }, - }, - fetchImpl: (async (url) => { - urls.push(String(url)); - const value = String(url); - if (value.includes('/search')) return Response.json({ ok: true, payload: { results: [ - { path: 'src/content/objectives/core.mdx' }, - { path: 'src/content/agent-tests/guide-editorial-cycle.mdx' }, - ] } }); - return new Response(JSON.stringify({ ok: true, payload: { content: value.includes('objectives%2Fcore') ? objective.content : existing.content } }), { - status: 200, headers: { 'content-type': 'application/json' }, - }); - }) as typeof fetch, - }); - expect(result).toMatchObject({ - ok: true, - refs: [{ path: 'src/content/notes/editorial/books/treeseed-guide/reviews/technical-review-plan.mdx' }], - }); - expect(urls).toHaveLength(3); - expect(urls.some((url) => url.includes('agent-tests'))).toBe(false); - }); - it('validates persisted relations from an exact existing record when the request only repeats identity and relations', async () => { - const existing = renderContentRecord({ - model: 'note', title: 'Workday report', body: 'Durable report body.', - relations: [{ field: 'relatedObjectives', targetSlug: 'objective:core' }], - }); - const objective = renderContentRecord({ model: 'objective', slug: 'core', title: 'Core', body: 'Objective.' }); - const fetchImpl = vi.fn(async (url: string | URL | Request) => { - const value = String(url); - if (value.includes('/search')) return Response.json({ ok: true, payload: { results: [{ path: 'src/content/objectives/core.mdx' }] } }); - return Response.json({ ok: true, payload: { content: value.includes('objectives%2Fcore') ? objective.content : existing.content } }); - }); - const path = 'src/content/notes/editorial/books/treeseed-guide/reports/workday-report.mdx'; - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.validate', handleId: 'handle-a', projectId: 'project-a', assignmentId: 'assignment-a', workspaceId: 'workspace-a', - allowedOperations: ['files:read', 'files:search'], allowedPaths: ['src/content/**'], - routes: { readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path', searchWorkspace: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/search' }, - metadata: { contentAction: 'validate', contentRoot: 'src/content' }, - } as never, - input: { - model: 'note', id: 'note:workday-report', slug: 'workday-report', placement: { path }, - relations: [{ field: 'relatedObjectives', targetModel: 'objective', targetSlug: 'core' }], - }, - fetchImpl: fetchImpl as typeof fetch, - }); - expect(result).toMatchObject({ ok: true, refs: [{ path }] }); - expect(fetchImpl).toHaveBeenCalledTimes(3); - expect(JSON.parse(String(fetchImpl.mock.calls[1]?.[1]?.body))).toMatchObject({ query: 'objective:core' }); - }); - it('preflights a proposed record at an exact placement against its current relation target', async () => { - const path = 'src/content/notes/editorial/books/treeseed-guide/reviews/proposed-review.mdx'; - const objective = renderContentRecord({ model: 'objective', slug: 'core', title: 'Core', body: 'Objective.' }); - const fetchImpl = vi.fn(async (url: string | URL | Request) => String(url).includes('/search') - ? Response.json({ ok: true, payload: { results: [{ path: 'src/content/objectives/core.mdx' }] } }) - : Response.json({ ok: true, payload: { content: objective.content } })); - const result = await callContentTool({ - apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { - id: 'treeseed.content.validate', handleId: 'handle-a', projectId: 'project-a', assignmentId: 'assignment-a', workspaceId: 'workspace-a', - allowedOperations: ['files:read', 'files:search'], allowedPaths: ['src/content/**'], - routes: { readWorkspaceFile: 'GET /v1/dx/projects/project-a/workspaces/workspace-a/files?path=:path', searchWorkspace: 'POST /v1/dx/projects/project-a/workspaces/workspace-a/search' }, - metadata: { contentAction: 'validate', contentRoot: 'src/content' }, - } as never, - input: { - model: 'note', title: 'Proposed technical review', body: 'Review body.', - relations: [{ field: 'relatedObjectives', targetModel: 'objective', targetSlug: 'objective:core' }], - placement: { path }, - }, - fetchImpl: fetchImpl as typeof fetch, - }); - expect(result).toMatchObject({ ok: true, refs: [expect.objectContaining({ path })] }); - expect(fetchImpl).toHaveBeenCalledTimes(2); - expect(JSON.parse(String(fetchImpl.mock.calls[0]?.[1]?.body))).toMatchObject({ query: 'objective:core' }); - }); - it('rejects a guessed hierarchical relation id when the current target has a different canonical identity', async () => { - const path = 'src/content/notes/editorial/books/treeseed-guide/evidence/core-evidence.mdx'; - const question = renderContentRecord({ model: 'question', slug: 'core-evidence-question', title: 'Core evidence question', body: 'Question.' }); - const fetchImpl = vi.fn(async (url: string | URL | Request) => String(url).includes('/search') - ? Response.json({ ok: true, payload: { results: [{ path: 'src/content/questions/core-evidence-question.mdx' }] } }) - : Response.json({ ok: true, payload: { content: question.content } })); - const result = await callContentTool({ apiBaseUrl: 'http://127.0.0.1:3000', providerAccessToken: 'redacted', assignmentId: 'assignment-a', - descriptor: { id: 'treeseed.content.validate', handleId: 'handle-a', projectId: 'project-a', assignmentId: 'assignment-a', workspaceId: 'workspace-a', allowedOperations: ['files:read','files:search'], allowedPaths: ['src/content/**'], routes: { readWorkspaceFile: 'GET /files?path=:path', searchWorkspace: 'POST /search' }, metadata: { contentAction: 'validate', contentRoot: 'src/content' } } as never, - input: { model: 'note', title: 'Core evidence', body: 'Evidence.', fields: { related_questions: ['question:editorial:treeseed-guide:core-evidence-question'] }, placement: { path } }, fetchImpl: fetchImpl as typeof fetch }); - expect(result).toMatchObject({ ok: false, code: 'content_relation_invalid', metadata: { diagnostics: [expect.objectContaining({ code: 'content_relation_target_mismatch', targetId: 'question:editorial:treeseed-guide:core-evidence-question' })] } }); - }); -}); diff --git a/tests/contract/agents/knowledge-contract.test.ts b/tests/contract/agents/knowledge-contract.test.ts deleted file mode 100644 index 64e5f429..00000000 --- a/tests/contract/agents/knowledge-contract.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - sumKnowledgeOptimizationScore, - validateKnowledgeDraft, - validateOptimizationReport, - type KnowledgeDraft, - type OptimizationReport, -} from '../../../src/agents/contracts/knowledge.ts'; -import { - validateResearchNote, - type ResearchNote, -} from '../../../src/agents/contracts/research.ts'; - -const now = '2026-05-13T12:00:00.000Z'; - -describe('research and knowledge artifact contracts', () => { - it('validates source-backed research notes', () => { - const note: ResearchNote = { - id: 'research:runtime-v1', - kind: 'research_note', - questionId: 'question:runtime', - state: 'draft', - contextQueries: [{ - id: 'runtime', - purpose: 'research', - source: 'task_payload', - includedNodeIds: ['knowledge:runtime'], - warnings: [], - }], - contextPackSummary: 'runtime: Agent runtime', - sourceRefs: [{ ref: 'src/content/knowledge/runtime.mdx', kind: 'path' }], - sourceMap: [{ - claim: 'Runtime knowledge is grounded in the runtime source document.', - sourceFiles: ['src/content/knowledge/runtime.mdx'], - sourceSymbolsOrSections: ['Agent Runtime'], - evidenceStrength: 'supporting', - uncertainty: 'Human review is still required.', - lastObservedRef: 'test', - }], - observedFacts: ['The runtime has an AgentKernel.'], - inferences: [{ - statement: 'Knowledge should be grounded in runtime source paths.', - sourceRefs: ['src/content/knowledge/runtime.mdx'], - confidence: 'medium', - }], - uncertainties: [{ - statement: 'Handler ownership still needs review.', - impact: 'medium', - }], - recommendedKnowledgeArtifacts: ['knowledge:runtime'], - recommendedImplementationProposal: null, - createdAt: now, - }; - - expect(validateResearchNote(note)).toEqual({ ok: true, errors: [] }); - }); - - it('validates generated knowledge drafts and optimization reports', () => { - const draft: KnowledgeDraft = { - id: 'knowledge:runtime', - kind: 'knowledge_draft', - title: 'Agent Runtime', - book: 'architecture', - section: 'runtime', - targetPath: 'src/content/knowledge/architecture/runtime/agent-runtime.mdx', - state: 'feature_branch', - sourceQuestionId: 'question:runtime', - sourceResearchIds: ['research:runtime-v1'], - frontmatter: { - title: 'Agent Runtime', - summary: 'How the runtime works.', - type: 'architecture', - status: 'pending_review', - generated_by: 'treeseed-agent', - agent_role: 'knowledge_generator', - source_question: 'question:runtime', - source_research: ['research:runtime-v1'], - review_state: 'pending_review', - book_target: 'architecture', - section_target: 'runtime', - confidence: 'medium', - source_map: [{ - claim: 'Runtime knowledge is grounded in the runtime source document.', - sourceFiles: ['src/content/knowledge/runtime.mdx'], - sourceSymbolsOrSections: ['Agent Runtime'], - evidenceStrength: 'supporting', - uncertainty: 'Human review is still required.', - lastObservedRef: 'test', - }], - updated: '2026-05-13', - related: { - objectives: [], - questions: ['question:runtime'], - proposals: [], - decisions: [], - }, - }, - body: [ - '# Agent Runtime', - '', - '## What this explains', - 'Runtime knowledge.', - '', - '## Current implementation', - '- The runtime has an AgentKernel.', - '', - '## Main flow', - '- The runtime executes work.', - '', - '## Important files', - '- src/content/knowledge/runtime.mdx', - '', - '## Source map', - '- src/content/knowledge/runtime.mdx', - '', - '## Governance and safety boundaries', - '- Human review required.', - '', - '## Open questions', - '- Handler ownership still needs review.', - '', - '## Verification notes', - '- Run optimizer.', - '', - ].join('\n'), - reviewState: 'pending_review', - createdAt: now, - updatedAt: now, - }; - const report: OptimizationReport = { - id: 'optimization:runtime', - kind: 'knowledge_optimization_report', - draftId: draft.id, - score: { - factual_grounding: 4, - book_fit: 4, - structure: 5, - future_agent_usefulness: 4, - human_reviewability: 4, - link_quality: 4, - uncertainty_visibility: 4, - }, - totalScore: 29, - recommendation: 'promote', - remainingIssues: ['Human review is still required.'], - criticalIssues: [], - createdAt: now, - }; - - expect(validateKnowledgeDraft(draft)).toEqual({ ok: true, errors: [] }); - expect(validateKnowledgeDraft({ - ...draft, - frontmatter: { ...draft.frontmatter, status: 'canonical' }, - }, { allowedStatuses: ['pending_review', 'canonical'] })).toEqual({ ok: true, errors: [] }); - expect(sumKnowledgeOptimizationScore(report.score)).toBe(29); - expect(validateOptimizationReport(report)).toEqual({ ok: true, errors: [] }); - }); -}); diff --git a/tests/contract/agents/starter-contracts.test.ts b/tests/contract/agents/starter-contracts.test.ts deleted file mode 100644 index dd4edc60..00000000 --- a/tests/contract/agents/starter-contracts.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { runAgentContractChecks } from '../../../src/agents/testing/agent-contracts.ts'; - -const repoRoot = resolve(__dirname, '../../../../..'); - -describe('starter agent contracts', () => { - for (const starter of [ - 'starters/engineering/template', - 'starters/research/template', - ]) { - it(`validates ${starter}`, async () => { - const result = await runAgentContractChecks({ - repoRoot: resolve(repoRoot, starter), - }); - - expect(result.issues).toEqual([]); - expect(result.ok).toBe(true); - }); - } -}); diff --git a/tests/contract/agents/verified-context-query-input.test.ts b/tests/contract/agents/verified-context-query-input.test.ts deleted file mode 100644 index 37edebb6..00000000 --- a/tests/contract/agents/verified-context-query-input.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe,expect,it,vi } from 'vitest'; -import { resolveExecutionTreeDxContext } from '../../../src/agents/handlers/execution-content-context.ts'; - -describe('verified context query assignment input',()=>{ - it('executes an admitted exact query in real time instead of injecting its test result',async()=>{ - const definitionRef='a'.repeat(40); const runtimeRef='b'.repeat(40); - const buildContext=vi.fn(async()=>({resolvedRef:runtimeRef,nodes:[{node:{id:'guide-node',path:'src/content/knowledge/guide.md',text:'Current guide context'}}],edges:[],totalTokenEstimate:12})); - const readRepositoryFiles=vi.fn(async(input:{paths:string[]})=>({payload:{files:[{path:input.paths[0],text:input.paths[0]?.includes('agent-context-queries')?query:'content'}]}})); - const query=`---\nid: guide\ntitle: Guide\ndescription: Current Guide context.\nrevision: 2\nmaturity: validated\npurpose: research\nquery: Current Guide\ntarget:\n kind: content\n models: [knowledge]\n paths: [/src/content/knowledge/guide.md]\nrelations: []\ndepth: 0\nresultLimit: 1\ncontextBudget: { maxItems: 1, maxCharacters: 2000 }\ntokenBudget: 500\nformat: full\nrequired: true\n---\n`; - const result=await resolveExecutionTreeDxContext({ - repoRoot:'/provider/data/project-a',agent:{slug:'architect',context:{queries:[]}}, - capacity:{assignmentId:'assignment-a',treedxProxyHandle:{repositoryId:'repo-a'},assignment:{metadata:{contentRoot:'src/content'}}}, - treeDx:{buildContext,readRepositoryFiles}, - } as never,{model:'objective',id:'core',title:'Core'},{objectiveId:'core',contextDefinitionRef:definitionRef,contextRuntimeRef:runtimeRef,contextQueryRefs:[{kind:'query',id:'guide',revision:2}],contextQueryChecks:[{ - id:'check-a',testRef:'test:guide',definition:{kind:'query',id:'guide',revision:2,commit:definitionRef}, - checkedAt:'2026-08-13T20:00:00.000Z',expiresAt:'2026-08-14T20:00:00.000Z',resultDigest:'test-result-digest',stats:{itemCount:1},assertions:[], - }]}); - expect(result.evidence).toEqual(expect.arrayContaining([expect.objectContaining({ - id:'treedx-realtime-context-query:guide',source:'treedx_realtime_context_query',pack:expect.objectContaining({nodes:expect.any(Array)}), - })])); - expect(buildContext).toHaveBeenCalledOnce(); - expect(buildContext).toHaveBeenCalledWith(expect.objectContaining({ref:runtimeRef})); - expect(readRepositoryFiles).toHaveBeenCalledWith(expect.objectContaining({ref:definitionRef})); - }); -}); diff --git a/tests/contract/package/package-shape.test.ts b/tests/contract/package/package-shape.test.ts deleted file mode 100644 index 72eaa8cd..00000000 --- a/tests/contract/package/package-shape.test.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { existsSync, readFileSync, readdirSync } from 'node:fs'; -import { execFileSync } from 'node:child_process'; -import { dirname, extname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; - -const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); -const PACKAGE_PACK_TEST_TIMEOUT_MS = 30_000; - -function readJson(path: string): T { - return JSON.parse(readFileSync(path, 'utf8')) as T; -} - -function walkFiles(root: string): string[] { - const files: string[] = []; - for (const entry of readdirSync(root, { withFileTypes: true })) { - const fullPath = join(root, entry.name); - if (entry.isDirectory()) { - files.push(...walkFiles(fullPath)); - continue; - } - files.push(fullPath); - } - return files; -} - -function sourcePathForDistSpecifier(specifier: string) { - const withoutPrefix = specifier.replace(/^\.\/dist\//u, '').replace(/\.js$/u, '.ts'); - return resolve(packageRoot, 'src', withoutPrefix); -} - -describe('agent package shape', () => { - it('keeps public exports and bins mapped to owned source files', () => { - const packageJson = readJson<{ - bin?: Record; - exports: Record; - scripts: Record; - }>(resolve(packageRoot, 'package.json')); - - expect(packageJson.bin).toBeUndefined(); - - for (const [exportName, exportValue] of Object.entries(packageJson.exports)) { - expect(exportValue.default, `${exportName} default export`).toMatch(/^\.\/dist\/.+\.js$/u); - expect(exportValue.types, `${exportName} type export`).toMatch(/^\.\/dist\/.+\.d\.ts$/u); - expect(existsSync(sourcePathForDistSpecifier(exportValue.default)), `${exportName} source file`).toBe(true); - } - - expect(packageJson.scripts.verify).toBe('TMPDIR=/tmp node --import tsx ./scripts/support/verify-driver.ts'); - expect(packageJson.scripts['verify:local']).toBe( - 'TREESEED_VERIFY_DRIVER=direct TMPDIR=/tmp node --import tsx ./scripts/support/verify-driver.ts', - ); - expect(packageJson.scripts['verify:action']).toBe( - 'TREESEED_VERIFY_DRIVER=act TMPDIR=/tmp node --import tsx ./scripts/support/verify-driver.ts', - ); - expect(Object.keys(packageJson.scripts).some((name) => name.includes('processing'))).toBe(false); - expect(Object.values(packageJson.scripts).some((command) => command.includes('treeseed-processing'))).toBe(false); - expect(packageJson.scripts).not.toHaveProperty('test:manager-worker'); - expect(packageJson.scripts).toHaveProperty('test:provider-runtime'); - }); - - it('enforces the assignment-only kernel module boundary', () => { - const kernelRoot = resolve(packageRoot, 'src/agents/kernel'); - const requiredModules = [ - 'agents/agent-kernel.ts', - 'capacity/assignments/assignment-preflight.ts', - 'telemetry/activity-profile-resolver.ts', - 'execution/context-loader.ts', - 'execution/execution-dispatcher.ts', - 'validation/output-validator.ts', - 'artifacts/artifact-manifest.ts', - 'telemetry/telemetry.ts', - 'validation/failure-classifier.ts', - 'runtime/kernel-runtime.ts', - ]; - for (const module of requiredModules) { - const source = readFileSync(resolve(kernelRoot, module), 'utf8'); - expect(source.split(/\r?\n/u).length, `${module} line count`).toBeLessThanOrEqual(500); - expect(source).not.toMatch(/@ts-(?:nocheck|ignore|expect-error)|eslint-disable|biome-ignore/gu); - } - for (const retiredModule of ['trigger-resolver.ts', 'mode-scheduler.ts', 'queue-observer.ts', 'priority-resolver.ts']) { - expect(existsSync(resolve(kernelRoot, retiredModule)), retiredModule).toBe(false); - } - const source = requiredModules.map((module) => readFileSync(resolve(kernelRoot, module), 'utf8')).join('\n'); - expect(source).not.toMatch(/\b(?:runAgent|runCycle|drainMessages|ModeScheduler|QueueObserver|PriorityResolver)\b/gu); - expect(source.match(/\brunAssignment\s*\(/gu)).toHaveLength(1); - }); - - it('enforces the focused provider runner module boundary', () => { - const providerRoot = resolve(packageRoot, 'src/provider'); - const requiredModules = [ - 'operations/runner.ts', - 'operations/runner-contracts.ts', - 'operations/runner-lifecycle.ts', - 'execution/kernel-bridge.ts', - 'capacity/assignments/kernel-assignment.ts', - 'treedx/graph/treedx-context-adapter.ts', - 'execution/execution-lifecycle.ts', - 'execution/execution-support.ts', - 'capacity/providers/execution-provider-selection.ts', - 'capacity/assignments/assignment-result-reporter.ts', - 'capacity/assignments/assignment-tool-policy.ts', - 'commerce/catalog/assignment-tool-catalog.ts', - 'coordination/lease-client.ts', - 'reporting/mode-run-reporter.ts', - 'capacity/accounting/usage-reporter.ts', - ]; - for (const module of requiredModules) { - const source = readFileSync(resolve(providerRoot, module), 'utf8'); - expect(source.split(/\r?\n/u).length, `${module} line count`).toBeLessThanOrEqual(500); - expect(source).not.toMatch(/@ts-(?:nocheck|ignore|expect-error)|eslint-disable|biome-ignore/gu); - expect(source).not.toMatch(/\bany\b/gu); - } - const runner = readFileSync(resolve(providerRoot, 'operations/runner.ts'), 'utf8'); - expect(runner.match(/\brunProviderAssignment\s*\(/gu)).toHaveLength(1); - expect(runner).not.toContain('createAssignmentTreeDxAdapter(input:'); - expect(runner).not.toContain('class LifecycleManagedExecutionProviderAdapter'); - }); - - it('enforces focused TreeDX content, tool receipt, and artifact owners', () => { - const requiredModules = [ - 'agents/handlers/execution-content.ts', - 'agents/handlers/execution-content-artifacts.ts', - 'agents/handlers/execution-content-context.ts', - 'agents/handlers/execution-content-prompt.ts', - 'agents/tools/agent-tool-runtime.ts', - 'agents/tools/agent-tool-telemetry.ts', - 'agents/kernel/artifacts/artifact-manifest.ts', - 'agents/kernel/artifacts/artifact-receipts.ts', - ]; - for (const module of requiredModules) { - const source = readFileSync(resolve(packageRoot, 'src', module), 'utf8'); - expect(source.split(/\r?\n/u).length, `${module} line count`).toBeLessThanOrEqual(500); - expect(source).not.toMatch(/@ts-(?:nocheck|ignore|expect-error)|eslint-disable|biome-ignore/gu); - } - expect(existsSync(resolve(packageRoot, 'src/agents/knowledge/pipeline.ts'))).toBe(false); - const publicIndex = readFileSync(resolve(packageRoot, 'src/index.ts'), 'utf8'); - expect(publicIndex).not.toMatch(/knowledge\/pipeline|buildKnowledgeDraft|buildResearchNote/gu); - const manifest = readFileSync(resolve(packageRoot, 'src/agents/kernel/artifacts/artifact-manifest.ts'), 'utf8'); - expect(manifest).not.toMatch(/contentArtifactRefs|\bcontentRefs\b|\bcodeChanges\b|\btoolSummary\b/gu); - const mutations = readFileSync(resolve(packageRoot, 'src/agents/adapters/tools/mutations.ts'), 'utf8'); - expect(mutations).toContain('Knowledge Hub content mutations require an assignment-scoped TreeDX tool receipt'); - const releaser = readFileSync(resolve(packageRoot, 'src/agents/handlers/releaser.ts'), 'utf8'); - expect(releaser).toContain('createExecutionContentHandler'); - expect(releaser).not.toMatch(/status:\s*['"]waiting['"]|releaseAttempted:\s*false/gu); - }); - - it('enforces focused specification and execution-provider modules', () => { - const requiredModules = [ - 'agents/support/spec-normalizer.ts', - 'agents/support/spec-normalizer-activities.ts', - 'agents/support/spec-normalizer-execution.ts', - 'agents/support/spec-normalizer-policy.ts', - 'agents/support/spec-normalizer-primitives.ts', - 'agents/adapters/codex/execution-codex.ts', - 'agents/adapters/codex/execution-codex-core.ts', - 'agents/adapters/codex/execution-codex-result.ts', - 'agents/adapters/reconciliation/execution-codex-adapter.ts', - 'agents/adapters/integrations/execution-jira.ts', - 'agents/adapters/reconciliation/execution-jira-adapter.ts', - 'agents/adapters/repositories/execution-github-issues.ts', - 'agents/adapters/reconciliation/execution-github-issues-adapter.ts', - 'agents/adapters/integrations/execution-discord.ts', - 'agents/adapters/reconciliation/execution-discord-adapter.ts', - 'api/projects/projects-core/project-routes.ts', - 'api/projects/projects-core/project-route-helpers.ts', - 'api/projects/projects-core/project-summary.ts', - 'api/auth/d1-store.ts', - 'api/auth/d1-store-core.ts', - 'api/auth/d1-user-store.ts', - ]; - for (const module of requiredModules) { - const source = readFileSync(resolve(packageRoot, 'src', module), 'utf8'); - expect(source.split(/\r?\n/u).length, `${module} line count`).toBeLessThanOrEqual(500); - expect(source).not.toMatch(/@ts-(?:nocheck|ignore|expect-error)|eslint-disable|biome-ignore/gu); - } - }); - - it('ships provider runtime entrypoint and support modules without source-mode temp artifacts', () => { - const requiredRuntimeFiles = [ - 'dist/provider/lifecycle/entrypoint.js', - 'dist/provider/configuration/config.js', - 'dist/provider/coordination/coordinator.js', - 'dist/provider/projects/projects-core/project-materialization.js', - 'dist/provider/operations/runner.js', - 'dist/provider/operations/runner-lifecycle.js', - 'dist/provider/treedx/graph/treedx-context-adapter.js', - 'dist/provider/execution/execution-lifecycle.js', - 'dist/provider/capacity/providers/execution-provider-selection.js', - 'dist/provider/execution/kernel-bridge.js', - 'dist/provider/capacity/assignments/assignment-result-reporter.js', - 'dist/provider/capacity/accounting/usage-reporter.js', - 'dist/provider/lifecycle/lifecycle.js', - 'dist/scripts/capacity/providers/build-capacity-provider-container.js', - 'dist/scripts/capacity/providers/test-capacity-provider-container.js', - 'dist/api/support/server.js', - 'dist/services/runtime-paths.js', - 'dist/agents/adapters/accounts/codex-auth.js', - 'dist/agents/adapters/codex/codex-readiness.js', - 'dist/agents/adapters/codex/execution-codex.js', - 'dist/agents/support/registry.js', - 'dist/agents/kernel/agents/agent-kernel.js', - 'dist/agents/kernel/capacity/assignments/assignment-preflight.js', - 'dist/agents/kernel/telemetry/activity-profile-resolver.js', - 'dist/agents/kernel/execution/context-loader.js', - 'dist/agents/kernel/execution/execution-dispatcher.js', - 'dist/agents/kernel/validation/output-validator.js', - 'dist/agents/kernel/artifacts/artifact-manifest.js', - 'dist/agents/kernel/artifacts/artifact-receipts.js', - 'dist/agents/kernel/telemetry/telemetry.js', - 'dist/agents/kernel/validation/failure-classifier.js', - 'dist/agents/handlers/writer.js', - 'dist/agents/handlers/actor.js', - 'dist/agents/handlers/estimate.js', - 'dist/agents/handlers/releaser.js', - 'dist/agents/handlers/reporter.js', - 'dist/agents/handlers/execution-content-context.js', - 'dist/agents/tools/agent-tool-telemetry.js', - 'dist/templates/github/deploy-capacity-provider.workflow.yml', - 'dist/templates/railway/capacity-provider.yml', - ]; - - for (const filePath of requiredRuntimeFiles) { - expect(existsSync(resolve(packageRoot, filePath)), `${filePath} exists`).toBe(true); - } - - const distFiles = walkFiles(resolve(packageRoot, 'dist')) - .map((filePath) => filePath.slice(packageRoot.length + 1).replace(/\\/gu, '/')); - expect(distFiles.filter((filePath) => /(^|\/)\.ts-run-/u.test(filePath))).toEqual([]); - }); - - it('packs runtime closure without source-mode temp artifacts', () => { - const output = execFileSync('npm', ['pack', '--plan', '--json', '--ignore-scripts'], { - cwd: packageRoot, - encoding: 'utf8', - env: { - ...process.env, - TREESEED_SKIP_PACKAGE_PREPARE: '1', - }, - }); - const [pack] = JSON.parse(output) as Array<{ files: Array<{ path: string }> }>; - const paths = pack.files.map((entry) => entry.path); - - expect(paths).toContain('dist/provider/lifecycle/entrypoint.js'); - expect(paths).toContain('dist/provider/configuration/config.js'); - expect(paths).toContain('dist/provider/coordination/coordinator.js'); - expect(paths).toContain('dist/provider/projects/projects-core/project-materialization.js'); - expect(paths).toContain('dist/provider/operations/runner.js'); - expect(paths).toContain('dist/provider/lifecycle/lifecycle.js'); - expect(paths).toContain('dist/scripts/capacity/providers/build-capacity-provider-container.js'); - expect(paths).toContain('dist/scripts/capacity/providers/test-capacity-provider-container.js'); - expect(paths).toContain('dist/services/runtime-paths.js'); - expect(paths).toContain('Dockerfile'); - expect(paths).toContain('docker-entrypoint.sh'); - expect(paths).toContain('compose.capacity-provider.yml'); - expect(paths).toContain('treeseed.package.yaml'); - expect(paths).toContain('docs/capacity-provider-runtime.md'); - expect(paths).toContain('templates/github/deploy-capacity-provider.workflow.yml'); - expect(paths).toContain('templates/railway/capacity-provider.yml'); - expect(paths.filter((filePath) => /(^|\/)\.ts-run-/u.test(filePath))).toEqual([]); - }, PACKAGE_PACK_TEST_TIMEOUT_MS); - - it('ships secure package-owned container assets', () => { - const dockerfile = readFileSync(resolve(packageRoot, 'Dockerfile'), 'utf8'); - const entrypoint = readFileSync(resolve(packageRoot, 'docker-entrypoint.sh'), 'utf8'); - const releaseVerify = readFileSync(resolve(packageRoot, 'scripts/packages/release-verify.ts'), 'utf8'); - const compose = readFileSync(resolve(packageRoot, 'compose.capacity-provider.yml'), 'utf8'); - const containerSmoke = readFileSync(resolve(packageRoot, 'scripts/capacity/providers/test-capacity-provider-container.ts'), 'utf8'); - const docs = readFileSync(resolve(packageRoot, 'docs/capacity-provider-runtime.md'), 'utf8'); - - expect(dockerfile).not.toContain('FROM node-runtime AS agent-api'); - expect(dockerfile).toContain('FROM node:22-alpine AS agent-provider-base'); - expect(dockerfile).toContain('FROM agent-provider-base AS agent-runtime'); - expect(dockerfile).toContain('FROM agent-runtime AS agent-manager'); - expect(dockerfile).toContain('FROM agent-runtime AS agent-runner'); - expect(dockerfile).toContain('FROM agent-runner AS railway-runtime'); - expect(dockerfile).toContain('ENTRYPOINT ["tini", "--", "/app/docker-entrypoint.sh"]'); - expect(entrypoint).toContain('setpriv'); - expect(releaseVerify).toContain("[providerEntrypoint, 'plan', '--json']"); - expect(releaseVerify).not.toContain("[providerEntrypoint, 'register'"); - expect(releaseVerify).toContain('privateKeyRef: secret://capacity/packed-smoke-provider-identity'); - expect(releaseVerify).not.toContain('privateKeyRef: env:'); - expect(dockerfile).toContain('FROM node:22'); - expect(dockerfile).toContain('.treeseed/docker/runtime/shared/node_modules'); - expect(dockerfile).not.toContain('RUN npm ci'); - expect(dockerfile).not.toContain('COPY . .'); - expect(dockerfile).not.toContain('treeseed-processing'); - expect(dockerfile).not.toContain('packages/core'); - expect(compose).toContain('target: agent-manager'); - expect(compose).toContain('target: agent-runner'); - expect(compose.match(/soft: 0/gu)).toHaveLength(2); - expect(compose.match(/hard: 0/gu)).toHaveLength(2); - expect(compose.match(/^\s+TREESEED_PROVIDER_ASSIGNMENT_DISK_HEADROOM_BYTES:/gmu)).toHaveLength(2); - expect(compose.match(/^\s+TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST:/gmu)).toHaveLength(2); - expect(compose).not.toContain('treeseed/agent-api'); - expect(compose).toContain('treeseed/agent-manager'); - expect(compose).toContain('treeseed/agent-runner'); - expect(compose).toContain('TREESEED_PROVIDER_STARTUP_MODE'); - expect(compose).toContain('TREESEED_CAPACITY_PROVIDER_MANIFEST: /config/treeseed.capacity-provider.yaml'); - expect(compose).toContain(':/config/treeseed.capacity-provider.yaml:ro'); - expect(compose).toContain(':/run/treeseed-secrets/codex-auth.json:ro'); - expect(compose).not.toContain(':/data/codex/auth.json:ro'); - expect(compose.match(/TREESEED_PROVIDER_UID: \$\{TREESEED_PROVIDER_CONTAINER_UID:-65532\}/gu)).toHaveLength(2); - expect(readFileSync(resolve(packageRoot, 'scripts/capacity/providers/build-capacity-provider-container.ts'), 'utf8')).not.toMatch(/rmSync\([^\n]+@github\/copilot/u); - expect(compose.match(/TREESEED_PROVIDER_GID: \$\{TREESEED_PROVIDER_CONTAINER_GID:-65532\}/gu)).toHaveLength(2); - expect(compose).not.toContain('TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN:'); - expect(compose).not.toContain('env_file'); - expect(compose).not.toMatch(/tscp_[A-Za-z0-9_]+|tsp_[A-Za-z0-9_]+|sk-[A-Za-z0-9_]+/u); - expect(containerSmoke).toContain("'config', '--quiet'"); - expect(containerSmoke).not.toContain("projectName, 'config'],"); - expect(docs).toContain('trsd config'); - expect(docs).toContain('Do not create plaintext `.env` files'); - const railwayTemplate = readFileSync(resolve(packageRoot, 'templates/railway/capacity-provider.yml'), 'utf8'); - const deployWorkflow = readFileSync(resolve(packageRoot, 'templates/github/deploy-capacity-provider.workflow.yml'), 'utf8'); - expect(railwayTemplate).not.toContain('api:'); - expect(railwayTemplate).toContain('manager:'); - expect(railwayTemplate).toContain('runner:'); - expect(railwayTemplate).not.toContain('node ./dist/provider/entrypoint.js api'); - expect(railwayTemplate).not.toMatch(/tscp_[A-Za-z0-9_]+|tsp_[A-Za-z0-9_]+|sk-[A-Za-z0-9_]+/u); - expect(deployWorkflow).toContain('Build package-owned provider role images'); - expect(deployWorkflow).toContain('npm run capacity-provider:build -- --prepare-only'); - expect(deployWorkflow).not.toContain('placeholder'); - }); - - it('does not import web/core runtime surfaces', () => { - const importPattern = /(?:from\s+['"]|import\s*\(\s*['"]|export\s+\*\s+from\s+['"]|export\s+\{[^}]*\}\s+from\s+['"])([^'"]+)['"]/gu; - const forbiddenSpecifiers = [ - /^@treeseed\/core(?:\/|$)/u, - /^astro(?:\/|$)/u, - /^@astrojs\/starlight(?:\/|$)/u, - ]; - - for (const filePath of walkFiles(resolve(packageRoot, 'src'))) { - if (!['.ts', '.js'].includes(extname(filePath))) continue; - const source = readFileSync(filePath, 'utf8'); - for (const match of source.matchAll(importPattern)) { - const specifier = match[1]; - expect( - forbiddenSpecifiers.some((pattern) => pattern.test(specifier)), - `${filePath} imports ${specifier}`, - ).toBe(false); - } - } - }); - - it('documents current workflows and registry-owned environment names', () => { - const readme = readFileSync(resolve(packageRoot, 'README.md'), 'utf8'); - const envRegistry = readFileSync(resolve(packageRoot, 'src/env.yaml'), 'utf8'); - const staleBudgetName = ['TREESEED_WORKDAY', 'CAPACITY_BUDGET'].join('_'); - const staleWorkflowName = ['.github/workflows', 'ci.yml'].join('/'); - const staleSmokePhrase = ['legacy', 'smoke path'].join(' '); - const packageSource = [ - readme, - envRegistry, - ...walkFiles(resolve(packageRoot, 'src')) - .filter((filePath) => ['.ts', '.yaml', '.yml'].includes(extname(filePath))) - .map((filePath) => readFileSync(filePath, 'utf8')), - ].join('\n'); - - expect(readme).toContain('.github/workflows/verify.yml'); - expect(readme).toContain('.github/workflows/publish.yml'); - expect(readme).toContain('active team allocation sets'); - expect(readme).not.toContain(staleWorkflowName); - expect(readme).not.toContain(staleSmokePhrase); - expect(packageSource).not.toContain(staleBudgetName); - expect(packageSource).not.toContain('codex_subscription'); - expect(envRegistry).not.toContain('TREESEED_WORKDAY_TASK_CREDIT_BUDGET'); - }); - - it('exposes one canonical selector per execution-provider adapter', () => { - const factory = readFileSync(resolve(packageRoot, 'src/agents/adapters/execution/execution.ts'), 'utf8'); - const registry = readFileSync(resolve(packageRoot, 'src/agent-runtime.ts'), 'utf8'); - expect(existsSync(resolve(packageRoot, 'src/agents/adapters/execution-deterministic.ts'))).toBe(false); - expect(existsSync(resolve(packageRoot, 'src/agents/adapters/execution-mock.ts'))).toBe(false); - for (const id of ['codex', 'copilot', 'jira', 'github_issues', 'discord', 'workflow']) { - expect(factory).toContain(`'${id}'`); - } - expect(registry).toContain('BUILT_IN_AGENT_EXECUTION_PROVIDER_IDS.map'); - expect(registry).toContain('createExecutionProviderAdapter(id, { repoRoot })'); - for (const removed of ['mock', 'deterministic', 'ci_mock', 'deterministic_mock', 'jira_issue_queue', 'human_issue_queue', 'github_issue_queue', 'issue_queue', 'discord_thread', 'workflow_operation', 'deterministic_workflow', 'github_actions_workflow']) { - expect(factory).not.toContain(`configuredMode === '${removed}'`); - expect(registry).not.toContain(`['${removed}'`); - } - }); - - it('forbids provider-shaped test doubles across the agent package', () => { - const roots = ['src', 'test', '.fixtures', 'guarantees'] - .map((directory) => resolve(packageRoot, directory)) - .filter(existsSync); - const thisFile = fileURLToPath(import.meta.url); - const providerDoubleFile = new RegExp( - String.raw`(?:mock|fake|stub|synthetic).*(?:provider|execution)|(?:provider|execution).*(?:mock|fake|stub|synthetic)`, - 'iu', - ); - const providerDoubleDeclaration = new RegExp( - String.raw`\b(?:class|function|const|let|var)\s+(?:Mock|Fake|Stub|Synthetic)\w*(?:Provider|Execution)\w*`, - 'u', - ); - - for (const filePath of roots.flatMap(walkFiles)) { - if (filePath === thisFile) continue; - expect(providerDoubleFile.test(filePath), `provider double filename: ${filePath}`).toBe(false); - if (!['.ts', '.tsx', '.js', '.mjs', '.cjs'].includes(extname(filePath))) continue; - const source = readFileSync(filePath, 'utf8'); - expect(providerDoubleDeclaration.test(source), `provider double declaration: ${filePath}`).toBe(false); - } - }); -}); diff --git a/tests/contract/package/release-version.test.ts b/tests/contract/package/release-version.test.ts deleted file mode 100644 index 08a0c3f6..00000000 --- a/tests/contract/package/release-version.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { parseAgentReleaseVersion } from '../../../scripts/packages/release-version.ts'; - -describe('Agent release version policy', () => { - it('routes exact stable and RC versions to isolated npm dist-tags', () => { - expect(parseAgentReleaseVersion('0.13.0', '0.13.0')).toEqual({ channel: 'stable', distTag: 'latest' }); - expect(parseAgentReleaseVersion('0.13.0-rc.1', '0.13.0-rc.1')).toEqual({ channel: 'prerelease', distTag: 'rc' }); - }); - - it('rejects mismatches and unsupported prerelease channels', () => { - expect(() => parseAgentReleaseVersion('0.13.0-rc.1', '0.13.0-rc.2')).toThrow(/does not match/u); - expect(() => parseAgentReleaseVersion('0.13.0-beta.1', '0.13.0-beta.1')).toThrow(/stable or rc\.N/u); - expect(() => parseAgentReleaseVersion('v0.13.0', 'v0.13.0')).toThrow(/stable or rc\.N/u); - }); -}); diff --git a/tests/contract/provider/provider-architecture.test.ts b/tests/contract/provider/provider-architecture.test.ts deleted file mode 100644 index 930cc765..00000000 --- a/tests/contract/provider/provider-architecture.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; - -const files = [ - 'projects/projects-core/availability-projection.ts', - 'coordination/client.ts', - 'configuration/config.ts', - 'coordination/connection-scheduler.ts', - 'coordination/connection-state.ts', - 'coordination/coordinator.ts', - 'lifecycle/entrypoint.ts', - 'accounts/identity.ts', - 'coordination/lease-recovery.ts', - 'lifecycle/lifecycle.ts', - 'capacity/capacity-core/local-capacity-store.ts', - 'configuration/manifest.ts', - 'teams/multi-team-runtime.ts', - 'capacity/capacity-core/native-capacity-limits.ts', -]; - -describe('provider coordinator architecture', () => { - it('keeps Phase 3 runtime modules focused, typed, and free of retired schedulers', () => { - const failures = files.flatMap((file) => { - const source = readFileSync(resolve(process.cwd(), 'src/provider', file), 'utf8'); - const issues: string[] = []; - if (source.split(/\r?\n/u).length > 500) issues.push('over-500-lines'); - if (/@ts-(?:nocheck|ignore|expect-error)|eslint-disable|biome-ignore/gu.test(source)) issues.push('compiler-suppression'); - if (/\bany\b/gu.test(source)) issues.push('explicit-any'); - if (/runRunnerSkeleton|backgroundRunnerState|availabilitySessions/gu.test(source)) issues.push('retired-process-local-runtime'); - return issues.map((issue) => ({ file, issue })); - }); - expect(failures).toEqual([]); - }); - - it('keeps team polling in the manager and durable-dispatch consumption in runners', () => { - const source = readFileSync(resolve(process.cwd(), 'src/provider/teams/multi-team-runtime.ts'), 'utf8'); - const manager = source.slice(source.indexOf('export async function runMultiTeamProviderManager'), source.indexOf('export async function runMultiTeamProviderRunners')); - const runner = source.slice(source.indexOf('export async function runMultiTeamProviderRunners')); - expect(manager).toContain('.nextAssignment('); - expect(runner).toContain('.claimDispatch('); - expect(runner).not.toContain('.nextAssignment('); - }); -}); diff --git a/tests/contract/provider/workday-contracts.test.ts b/tests/contract/provider/workday-contracts.test.ts deleted file mode 100644 index 2f30b175..00000000 --- a/tests/contract/provider/workday-contracts.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - evaluateWorkdayContinuation, - selectFairPlanningAgentCycles, -} from '@treeseed/sdk/agent-capacity'; - -describe('capacity workday contracts', () => { - it('continues useful work until duration or budget bound', () => { - const active = { - status: 'active', - now: '2026-07-16T12:00:00.000Z', - deadlineAt: '2026-07-16T13:00:00.000Z', - totalSeconds: 100, - committedSeconds: 40, - usefulEligibleWork: true, - }; - - expect(evaluateWorkdayContinuation(active)).toEqual({ - continue: true, - reason: 'within_duration_and_budget', - }); - expect(evaluateWorkdayContinuation({ ...active, now: active.deadlineAt })).toEqual({ - continue: false, - reason: 'duration_bound_reached', - }); - expect(evaluateWorkdayContinuation({ ...active, committedSeconds: 100 })).toEqual({ - continue: false, - reason: 'budget_bound_reached', - }); - expect(evaluateWorkdayContinuation({ ...active, usefulEligibleWork: false })).toEqual({ - continue: false, - reason: 'no_useful_eligible_work', - }); - }); - - it('exercises every eligible planning agent before repeats', () => { - const agents = [ - { slug: 'alpha', projectAgentClassSlug: 'planning' }, - { slug: 'beta', projectAgentClassSlug: 'planning' }, - { slug: 'gamma', projectAgentClassSlug: 'planning' }, - ]; - const initial = selectFairPlanningAgentCycles('project-1', agents, [], 4); - expect(initial.slice(0, 3).map(({ agent }) => agent.slug)).toEqual(['alpha', 'beta', 'gamma']); - expect(initial[3]).toMatchObject({ agent: { slug: 'alpha' }, cycle: 2 }); - - const resumed = selectFairPlanningAgentCycles('project-1', agents, [{ - projectId: 'project-1', - agentId: 'alpha', - metadata: { cycle: 1 }, - }], 3); - expect(resumed.map(({ agent }) => agent.slug)).toEqual(['beta', 'gamma', 'alpha']); - expect(resumed[2]?.cycle).toBe(2); - }); -}); diff --git a/tests/fixtures/0007_site_web_sessions.sql b/tests/fixtures/0007_site_web_sessions.sql deleted file mode 100644 index c1fa88f2..00000000 --- a/tests/fixtures/0007_site_web_sessions.sql +++ /dev/null @@ -1,196 +0,0 @@ -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - email TEXT, - display_name TEXT, - status TEXT NOT NULL DEFAULT 'active', - metadata_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_identities ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - provider TEXT NOT NULL, - provider_subject TEXT NOT NULL, - email TEXT, - email_verified INTEGER NOT NULL DEFAULT 0, - profile_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_identities_provider_subject - ON user_identities(provider, provider_subject); - -CREATE TABLE IF NOT EXISTS roles ( - id TEXT PRIMARY KEY, - key TEXT NOT NULL UNIQUE, - description TEXT, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS permissions ( - id TEXT PRIMARY KEY, - key TEXT NOT NULL UNIQUE, - resource TEXT NOT NULL, - action TEXT NOT NULL, - scope TEXT NOT NULL, - description TEXT, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS role_permissions ( - role_id TEXT NOT NULL, - permission_id TEXT NOT NULL, - created_at TEXT NOT NULL, - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS user_role_bindings ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - role_id TEXT NOT NULL, - created_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_role_bindings_user_role - ON user_role_bindings(user_id, role_id); - -CREATE TABLE IF NOT EXISTS api_tokens ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - kind TEXT NOT NULL, - name TEXT NOT NULL, - token_prefix TEXT NOT NULL, - token_hash TEXT NOT NULL, - scopes_json TEXT NOT NULL, - expires_at TEXT, - last_used_at TEXT, - revoked_at TEXT, - metadata_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id - ON api_tokens(user_id); - -CREATE INDEX IF NOT EXISTS idx_api_tokens_prefix - ON api_tokens(token_prefix); - -CREATE TABLE IF NOT EXISTS service_credentials ( - id TEXT PRIMARY KEY, - service_id TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - secret_hash TEXT NOT NULL, - roles_json TEXT NOT NULL, - permissions_json TEXT NOT NULL, - revoked_at TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - last_used_at TEXT -); - -CREATE TABLE IF NOT EXISTS auth_sessions ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - session_type TEXT NOT NULL, - refresh_token_hash TEXT NOT NULL, - scopes_json TEXT NOT NULL, - expires_at TEXT NOT NULL, - revoked_at TEXT, - data_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id - ON auth_sessions(user_id); - -CREATE TABLE IF NOT EXISTS audit_events ( - id TEXT PRIMARY KEY, - actor_type TEXT NOT NULL, - actor_id TEXT, - event_type TEXT NOT NULL, - target_type TEXT, - target_id TEXT, - data_json TEXT, - created_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_audit_events_target - ON audit_events(target_type, target_id); - -CREATE TABLE IF NOT EXISTS device_codes ( - id TEXT PRIMARY KEY, - device_code TEXT NOT NULL UNIQUE, - user_code TEXT NOT NULL UNIQUE, - requested_scopes_json TEXT NOT NULL, - expires_at TEXT NOT NULL, - interval_seconds INTEGER NOT NULL, - status TEXT NOT NULL, - user_id TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS teams ( - id TEXT PRIMARY KEY, - slug TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - metadata_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS team_memberships ( - id TEXT PRIMARY KEY, - team_id TEXT NOT NULL, - user_id TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'active', - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_team_memberships_team_user - ON team_memberships(team_id, user_id); - -CREATE TABLE IF NOT EXISTS team_role_bindings ( - id TEXT PRIMARY KEY, - team_membership_id TEXT NOT NULL, - role_id TEXT NOT NULL, - created_at TEXT NOT NULL, - FOREIGN KEY (team_membership_id) REFERENCES team_memberships(id) ON DELETE CASCADE, - FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS web_sessions ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - identity_id TEXT, - provider TEXT NOT NULL, - provider_subject TEXT NOT NULL, - email TEXT, - display_name TEXT, - principal_json TEXT NOT NULL, - csrf_token TEXT NOT NULL, - authenticated_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (identity_id) REFERENCES user_identities(id) ON DELETE SET NULL -); - -CREATE INDEX IF NOT EXISTS idx_web_sessions_user_id - ON web_sessions(user_id); diff --git a/tests/fixtures/agent-tests/codebase-cartographer/basic/expected-result.json b/tests/fixtures/agent-tests/codebase-cartographer/basic/expected-result.json deleted file mode 100644 index 3873d0f1..00000000 --- a/tests/fixtures/agent-tests/codebase-cartographer/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "waiting" -} diff --git a/tests/fixtures/agent-tests/codebase-cartographer/basic/trigger-message.json b/tests/fixtures/agent-tests/codebase-cartographer/basic/trigger-message.json deleted file mode 100644 index 20832004..00000000 --- a/tests/fixtures/agent-tests/codebase-cartographer/basic/trigger-message.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "taskId": "task-cartographer", - "messageType": "research_task_requested", - "question": { - "id": "question:runtime", - "title": "How does the runtime work?", - "book": "architecture", - "section": "runtime", - "targetPath": "src/content/knowledge/architecture/runtime/runtime.mdx" - } -} diff --git a/tests/fixtures/agent-tests/docs-engineer/basic/expected-result.json b/tests/fixtures/agent-tests/docs-engineer/basic/expected-result.json deleted file mode 100644 index 396d718b..00000000 --- a/tests/fixtures/agent-tests/docs-engineer/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "completed" -} diff --git a/tests/fixtures/agent-tests/docs-engineer/basic/trigger-message.json b/tests/fixtures/agent-tests/docs-engineer/basic/trigger-message.json deleted file mode 100644 index a192ded2..00000000 --- a/tests/fixtures/agent-tests/docs-engineer/basic/trigger-message.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "taskId": "task-engineer", - "messageType": "approval_request_approved", - "provider": "codex", - "allowedPaths": [] -} diff --git a/tests/fixtures/agent-tests/docs-planner/basic/expected-result.json b/tests/fixtures/agent-tests/docs-planner/basic/expected-result.json deleted file mode 100644 index 396d718b..00000000 --- a/tests/fixtures/agent-tests/docs-planner/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "completed" -} diff --git a/tests/fixtures/agent-tests/docs-planner/basic/trigger-message.json b/tests/fixtures/agent-tests/docs-planner/basic/trigger-message.json deleted file mode 100644 index 42cdc7c9..00000000 --- a/tests/fixtures/agent-tests/docs-planner/basic/trigger-message.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "taskId": "task-docs-planner", - "messageType": "knowledge_gap_detected" -} diff --git a/tests/fixtures/agent-tests/docs-reviewer/basic/expected-result.json b/tests/fixtures/agent-tests/docs-reviewer/basic/expected-result.json deleted file mode 100644 index 396d718b..00000000 --- a/tests/fixtures/agent-tests/docs-reviewer/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "completed" -} diff --git a/tests/fixtures/agent-tests/docs-reviewer/basic/trigger-message.json b/tests/fixtures/agent-tests/docs-reviewer/basic/trigger-message.json deleted file mode 100644 index 14aaabaf..00000000 --- a/tests/fixtures/agent-tests/docs-reviewer/basic/trigger-message.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "taskId": "task-reviewer", - "messageType": "docs_mutation_completed" -} diff --git a/tests/fixtures/agent-tests/governance-steward/basic/expected-result.json b/tests/fixtures/agent-tests/governance-steward/basic/expected-result.json deleted file mode 100644 index 396d718b..00000000 --- a/tests/fixtures/agent-tests/governance-steward/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "completed" -} diff --git a/tests/fixtures/agent-tests/governance-steward/basic/trigger-message.json b/tests/fixtures/agent-tests/governance-steward/basic/trigger-message.json deleted file mode 100644 index 4cc5f8c2..00000000 --- a/tests/fixtures/agent-tests/governance-steward/basic/trigger-message.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "taskId": "task-governance", - "messageType": "promotion_request_created" -} diff --git a/tests/fixtures/agent-tests/governed-mutation/basic/chain.json b/tests/fixtures/agent-tests/governed-mutation/basic/chain.json deleted file mode 100644 index 019ef38c..00000000 --- a/tests/fixtures/agent-tests/governed-mutation/basic/chain.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "governed-mutation-approval", - "requiresHumanApproval": true, - "allowedPaths": ["docs/**"] -} diff --git a/tests/fixtures/agent-tests/knowledge-generator/basic/expected-result.json b/tests/fixtures/agent-tests/knowledge-generator/basic/expected-result.json deleted file mode 100644 index 396d718b..00000000 --- a/tests/fixtures/agent-tests/knowledge-generator/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "completed" -} diff --git a/tests/fixtures/agent-tests/knowledge-generator/basic/trigger-message.json b/tests/fixtures/agent-tests/knowledge-generator/basic/trigger-message.json deleted file mode 100644 index 146d8e46..00000000 --- a/tests/fixtures/agent-tests/knowledge-generator/basic/trigger-message.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "taskId": "task-generator", - "messageType": "research_note_created", - "researchNote": { - "id": "research:runtime", - "kind": "research_note", - "questionId": "question:runtime", - "state": "draft", - "contextQueries": [], - "contextPackSummary": "Runtime source context.", - "sourceRefs": [{ "ref": "packages/agent/src/provider/operations/runner.ts", "kind": "path", "title": "Runner" }], - "sourceMap": [{ - "claim": "The worker executes claimed tasks.", - "sourceFiles": ["packages/agent/src/provider/operations/runner.ts"], - "sourceSymbolsOrSections": ["runWorkerCycle"], - "evidenceStrength": "direct", - "uncertainty": "", - "lastObservedRef": "graph-1" - }], - "observedFacts": ["The worker executes claimed tasks."], - "inferences": [], - "uncertainties": [], - "recommendedKnowledgeArtifacts": ["knowledge:runtime"], - "recommendedImplementationProposal": null, - "createdAt": "2026-05-19T00:00:00.000Z" - }, - "question": { - "id": "question:runtime", - "title": "Runtime", - "book": "architecture", - "section": "runtime", - "targetPath": "src/content/knowledge/architecture/runtime/runtime.mdx" - } -} diff --git a/tests/fixtures/agent-tests/knowledge-optimizer/basic/expected-result.json b/tests/fixtures/agent-tests/knowledge-optimizer/basic/expected-result.json deleted file mode 100644 index 396d718b..00000000 --- a/tests/fixtures/agent-tests/knowledge-optimizer/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "completed" -} diff --git a/tests/fixtures/agent-tests/knowledge-optimizer/basic/trigger-message.json b/tests/fixtures/agent-tests/knowledge-optimizer/basic/trigger-message.json deleted file mode 100644 index 79870dbe..00000000 --- a/tests/fixtures/agent-tests/knowledge-optimizer/basic/trigger-message.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "taskId": "task-optimizer", - "messageType": "knowledge_draft_created" -} diff --git a/tests/fixtures/agent-tests/releaser/basic/expected-result.json b/tests/fixtures/agent-tests/releaser/basic/expected-result.json deleted file mode 100644 index 396d718b..00000000 --- a/tests/fixtures/agent-tests/releaser/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "completed" -} diff --git a/tests/fixtures/agent-tests/releaser/basic/trigger-message.json b/tests/fixtures/agent-tests/releaser/basic/trigger-message.json deleted file mode 100644 index 9a6943ac..00000000 --- a/tests/fixtures/agent-tests/releaser/basic/trigger-message.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "taskId": "task-releaser", - "messageType": "workday_report_created" -} diff --git a/tests/fixtures/agent-tests/workday-reporter/basic/expected-result.json b/tests/fixtures/agent-tests/workday-reporter/basic/expected-result.json deleted file mode 100644 index 396d718b..00000000 --- a/tests/fixtures/agent-tests/workday-reporter/basic/expected-result.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "completed" -} diff --git a/tests/fixtures/agent-tests/workday-reporter/basic/trigger-message.json b/tests/fixtures/agent-tests/workday-reporter/basic/trigger-message.json deleted file mode 100644 index e01fc763..00000000 --- a/tests/fixtures/agent-tests/workday-reporter/basic/trigger-message.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "taskId": "task-reporter", - "messageType": "review_passed" -} diff --git a/tests/fixtures/runtime-site/src/manifest.yaml b/tests/fixtures/runtime-site/src/manifest.yaml deleted file mode 100644 index 7caecd33..00000000 --- a/tests/fixtures/runtime-site/src/manifest.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: treeseed-agent-runtime-test -siteConfigPath: ./src/config.yaml -content: - pages: ./src/content/pages - agents: ./src/content/agents -features: - agents: true diff --git a/tests/fixtures/runtime-site/treeseed.site.yaml b/tests/fixtures/runtime-site/treeseed.site.yaml deleted file mode 100644 index 5887c6e4..00000000 --- a/tests/fixtures/runtime-site/treeseed.site.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: TreeSeed Agent Runtime Test -slug: treeseed-agent-runtime-test -siteUrl: https://agent-runtime.test -contactEmail: test@agent-runtime.invalid -hosting: - kind: hosted_project - registration: optional - marketBaseUrl: https://api.treeseed.invalid - teamId: treeseed-agent-runtime-test - projectId: treeseed-agent-runtime-test -cloudflare: - accountId: fixture-account - workerName: treeseed-agent-runtime-test - pages: - projectName: treeseed-agent-runtime-test - previewProjectName: treeseed-agent-runtime-test-staging - productionBranch: main - stagingBranch: staging - buildOutputDir: dist -providers: - agents: - execution: codex - mutation: local_branch - repository: git - verification: local - notification: sdk_message - research: project_graph - deploy: cloudflare - site: default diff --git a/tests/integration/agents/agent-tool-copilot.test.ts b/tests/integration/agents/agent-tool-copilot.test.ts deleted file mode 100644 index 73b8b43a..00000000 --- a/tests/integration/agents/agent-tool-copilot.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { mkdtempSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { createCopilotAgentTools } from '../../../src/agents/tools/agent-tool-copilot.ts'; -import type { ExecutionProviderToolDescriptor } from '../../../src/agents/runtime/runtime-types.ts'; - -function statusDescriptor(): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id: 'treeseed.status', - name: 'TreeSeed status', - description: 'Inspect TreeSeed status.', - inputSchema: { type: 'object', properties: {}, additionalProperties: false }, - executionTarget: 'sdk_dispatch', - mutability: 'read', - metadata: { - assignmentId: 'assignment-copilot', - projectId: 'project-copilot', - telemetryCategory: 'treeseed', - dispatchPreferredMode: 'prefer_local', - }, - }; -} - -describe('Copilot agent tools', () => { - it('maps TreeSeed descriptors to Copilot-native tools and executes through the runtime', async () => { - const repoRoot = mkdtempSync(join(tmpdir(), 'treeseed-copilot-tool-')); - try { - const tools = createCopilotAgentTools({ - apiBaseUrl: '', - providerAccessToken: '', - assignmentId: 'assignment-copilot', - descriptors: [statusDescriptor()], - repoRoot, - sdk: { - dispatch: async (request) => ({ - ok: true, - request, - }), - }, - }); - - expect(tools).toHaveLength(1); - expect(tools[0]).toMatchObject({ - name: 'treeseed_status', - skipPermission: true, - parameters: { type: 'object' }, - }); - - const result = await tools[0]?.handler({}, { - sessionId: 'session-1', - toolCallId: 'tool-call-1', - toolName: 'treeseed_status', - arguments: {}, - }); - - expect(result).toMatchObject({ - resultType: 'success', - toolTelemetry: { - toolId: 'treeseed.status', - callName: 'treeseed_status', - }, - }); - expect(String(result && typeof result === 'object' && 'textResultForLlm' in result ? result.textResultForLlm : '')).toContain('"operation":"status"'); - } finally { - rmSync(repoRoot, { recursive: true, force: true }); - } - }); -}); diff --git a/tests/integration/agents/agent-tool-mcp-server.test.ts b/tests/integration/agents/agent-tool-mcp-server.test.ts deleted file mode 100644 index 83d13432..00000000 --- a/tests/integration/agents/agent-tool-mcp-server.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; -import { callTreeDxProxyTool } from '../../../src/agents/tools/treedx-proxy-client.ts'; -import { - agentToolMcpName, - createAgentToolMcpServer, - createAgentToolMcpServerCommand, -} from '../../../src/agents/tools/agent-tool-mcp-server.ts'; -import type { TreeDxProxyExecutionToolDescriptor } from '../../../src/agents/runtime/runtime-types.ts'; - -const descriptor: TreeDxProxyExecutionToolDescriptor = { - kind: 'agent_tool', - id: 'treedx.apply_workspace_changeset', - name: 'Apply TreeDX workspace changeset', - description: 'Assignment-scoped atomic TreeDX workspace changeset.', - inputSchema: { - type: 'object', - properties: { - contract: { type: 'string' }, baseCommitSha: { type: 'string' }, baseRef: { type: 'string' }, - patch: { type: 'string' }, patchSha256: { type: 'string' }, idempotencyKey: { type: 'string' }, - }, - required: ['contract', 'baseCommitSha', 'baseRef', 'patch', 'patchSha256', 'idempotencyKey'], - additionalProperties: false, - }, - executionTarget: 'treedx_proxy', - mutability: 'content_write', - projectId: 'project-1', - assignmentId: 'assignment-1', - handleId: 'handle-1', - repositoryId: 'repo-1', - workspaceId: 'workspace-1', - allowedOperations: ['files:read', 'files:write', 'git:commit'], - allowedPaths: ['src/content/**'], - metadata: { - contentRoot: 'template/src/content', - requiredArtifactKind: 'documentation_update', - requireContentArtifact: true, - }, - routes: { - buildContext: 'POST /v1/dx/projects/project-1/repos/:repoId/context/build', - readRepositoryFiles: 'POST /v1/dx/projects/project-1/repos/:repoId/files/read', - searchWorkspace: 'POST /v1/dx/projects/project-1/workspaces/:workspaceId/search', - readWorkspaceFile: 'GET /v1/dx/projects/project-1/workspaces/:workspaceId/files?path=:path', - applyWorkspaceChangeset: 'POST /v1/dx/projects/project-1/workspaces/:workspaceId/changesets', - commitWorkspace: 'POST /v1/dx/projects/project-1/workspaces/:workspaceId/commit', - }, -}; - -const assignmentPlanDescriptor: TreeDxProxyExecutionToolDescriptor = { - ...descriptor, - id: 'treeseed.assignment_plan', - name: 'Assignment plan', - description: 'Read the durable assignment operational plan.', - executionTarget: 'treeseed_content', - mutability: 'content_write', - metadata: { - ...descriptor.metadata, - contentRoot: 'src/content', - contentAction: 'read', - contentModel: 'assignment_plan', - }, -}; - -describe('agent tool MCP tooling', () => { - it('creates an assignment-scoped command without embedding credentials in args', () => { - const command = createAgentToolMcpServerCommand({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-secret', - assignmentId: 'assignment-1', - repoRoot: '/repo', - telemetryPath: '/tmp/tools.jsonl', - descriptors: [descriptor], - researchSourcePolicy: { - schemaVersion: 1, - allowedDomains: ['sources.example.test'], - requestTimeoutMs: 10_000, - maxResponseBytes: 100_000, - maxRedirects: 2, - allowedContentTypes: ['text/*'], - }, - }); - expect(command.command).toBe(process.execPath); - expect(command.args.join(' ')).not.toContain('provider-secret'); - expect(command.env.TREESEED_CAPACITY_PROVIDER_ACCESS_TOKEN).toBe('provider-secret'); - expect(command.env.TREESEED_AGENT_TOOL_REPO_ROOT).toBe('/repo'); - expect(command.env.TREESEED_AGENT_TOOL_TELEMETRY_PATH).toBe('/tmp/tools.jsonl'); - expect(command.env.TREESEED_AGENT_TOOL_DESCRIPTORS).not.toContain('provider-secret'); - const transported = JSON.parse(command.env.TREESEED_AGENT_TOOL_DESCRIPTORS); - expect(transported[0].metadata).toMatchObject({ - contentRoot: 'template/src/content', - requiredArtifactKind: 'documentation_update', - requireContentArtifact: true, - }); - expect(JSON.parse(command.env.TREESEED_AGENT_TOOL_RESEARCH_SOURCE_POLICY)).toMatchObject({ - allowedDomains: ['sources.example.test'], - maxRedirects: 2, - }); - }); - - it('calls an atomic workspace changeset through the API proxy with assignment headers', async () => { - const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ ok: true, payload: { path: 'src/content/a.mdx' } }), { status: 200 })); - await callTreeDxProxyTool({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-secret', - assignmentId: 'assignment-1', - handleId: 'handle-1', - descriptor, - toolName: 'treedx.apply_workspace_changeset', - input: { contract: 'treedx.changeset/v1', baseCommitSha: 'abc123', baseRef: 'refs/heads/main', patch: 'patch', patchSha256: 'digest', idempotencyKey: 'changeset-1' }, - fetchImpl: fetchImpl as unknown as typeof fetch, - }); - expect(fetchImpl).toHaveBeenCalledWith( - 'https://api.example.test/v1/dx/projects/project-1/workspaces/workspace-1/changesets', - expect.objectContaining({ - method: 'POST', - headers: expect.objectContaining({ - authorization: 'Bearer provider-secret', - 'x-treeseed-assignment-id': 'assignment-1', - 'x-treeseed-treedx-proxy-handle-id': 'handle-1', - }), - }), - ); - }); - - it('bounds context packs below the control-plane response ceiling by default', async () => { - const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ ok: true, payload: { nodes: [] } }), { status: 200 })); - await callTreeDxProxyTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-secret', assignmentId: 'assignment-1', - handleId: 'handle-1', descriptor, toolName: 'treedx.build_context', input: { query: 'guide architecture' }, - fetchImpl: fetchImpl as unknown as typeof fetch, - }); - const request = fetchImpl.mock.calls[0]?.[1] as RequestInit; - expect(JSON.parse(String(request.body))).toEqual({ query: 'guide architecture', budget: { maxNodes: 16, maxTokens: 4000 } }); - }); - - it('pages repository reads below the control-plane response ceiling', async () => { - const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ file: { content: 'bounded' } }), { status: 200 })); - await callTreeDxProxyTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-secret', assignmentId: 'assignment-1', - handleId: 'handle-1', descriptor, toolName: 'treedx.read_repository_files', - input: { paths: ['src/content/a.mdx'], offsetBytes: 42 }, fetchImpl: fetchImpl as unknown as typeof fetch, - }); - const request = fetchImpl.mock.calls[0]?.[1] as RequestInit; - expect(JSON.parse(String(request.body))).toEqual({ - paths: ['src/content/a.mdx'], maxBytes: 131_072, offsetBytes: 42, - }); - }); - - it('retries transient assignment-scoped TreeDX reads without retrying mutations', async () => { - const readFetch = vi.fn() - .mockRejectedValueOnce(new TypeError('fetch failed')) - .mockResolvedValue(new Response(JSON.stringify({ ok: true, payload: { content: 'recovered' } }), { status: 200 })); - await expect(callTreeDxProxyTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-secret', assignmentId: 'assignment-1', - handleId: 'handle-1', descriptor, toolName: 'treedx.read_workspace_file', input: { path: 'src/content/a.mdx' }, - fetchImpl: readFetch as unknown as typeof fetch, - })).resolves.toMatchObject({ ok: true }); - expect(readFetch).toHaveBeenCalledTimes(2); - - const writeFetch = vi.fn().mockRejectedValue(new TypeError('fetch failed')); - await expect(callTreeDxProxyTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-secret', assignmentId: 'assignment-1', - handleId: 'handle-1', descriptor, toolName: 'treedx.apply_workspace_changeset', input: { path: 'src/content/a.mdx', content: 'body' }, - fetchImpl: writeFetch as unknown as typeof fetch, - })).rejects.toThrow('fetch failed'); - expect(writeFetch).toHaveBeenCalledTimes(1); - }); - - it('fans multi-file repository reads into independently bounded requests', async () => { - const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => { - const body = JSON.parse(String(init?.body)); - return new Response(JSON.stringify({ ok: true, payload: { files: [{ path: body.paths[0] }] } }), { status: 200 }); - }); - const response = await callTreeDxProxyTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-secret', assignmentId: 'assignment-1', - handleId: 'handle-1', descriptor, toolName: 'treedx.read_repository_files', - input: { paths: ['src/content/a.mdx', 'src/content/b.mdx'] }, fetchImpl: fetchImpl as unknown as typeof fetch, - }); - expect(fetchImpl).toHaveBeenCalledTimes(2); - expect((response as { payload: { files: unknown[] } }).payload.files).toEqual([ - { path: 'src/content/a.mdx' }, { path: 'src/content/b.mdx' }, - ]); - }); - - it('serves allowed tools through a standards-compliant MCP connection', async () => { - const fetchImpl = vi.fn(async (url: string | URL | Request) => { - if (String(url).includes('/v1/provider/assignments/')) return new Response(JSON.stringify({ ok: true, payload: { - id: 'assignment-1', projectId: 'project-1', stateVersion: 1, status: 'leased', leaseState: 'leased', - assignedAt: '2026-08-14T00:00:00.000Z', capacityEnvelope: { reservedSeconds: 900, - budget: { time: { hardDeadlineAt: '2099-01-01T00:00:00.000Z' } } }, - } }), { status: 200 }); - if (String(url).includes('/files?path=')) return new Response(JSON.stringify({ content: '---\nschemaVersion: treeseed.assignment-plan/v1\nassignmentId: assignment-1\n---\n' }), { status: 200 }); - return new Response(JSON.stringify({ ok: true, payload: { path: 'src/content/a.mdx' } }), { status: 200 }); - }); - const telemetry: unknown[] = []; - const server = createAgentToolMcpServer({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-secret', - assignmentId: 'assignment-1', - descriptors: [descriptor, assignmentPlanDescriptor], - fetchImpl: fetchImpl as unknown as typeof fetch, - onTelemetry: (entry) => telemetry.push(entry), - }); - const client = new Client({ name: 'treeseed-test-client', version: '1.0.0' }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - - await Promise.all([ - server.connect(serverTransport), - client.connect(clientTransport), - ]); - try { - const tools = await client.listTools(); - expect(tools.tools.map((tool) => tool.name)).toEqual([ - 'treedx_apply_workspace_changeset', 'treeseed_assignment_plan', - ]); - expect(agentToolMcpName('treedx.apply_workspace_changeset')).toBe('treedx_apply_workspace_changeset'); - - const result = await client.callTool({ - name: 'treedx_apply_workspace_changeset', - arguments: { contract: 'treedx.changeset/v1', baseCommitSha: 'abc123', baseRef: 'refs/heads/main', patch: 'patch', patchSha256: 'digest', idempotencyKey: 'changeset-1' }, - }); - expect(result.isError).not.toBe(true); - expect(result.structuredContent).toEqual({ ok: true, payload: { path: 'src/content/a.mdx' } }); - expect(telemetry).toEqual(expect.arrayContaining([ - expect.objectContaining({ toolId: 'treedx.apply_workspace_changeset', status: 'started' }), - expect.objectContaining({ toolId: 'treedx.apply_workspace_changeset', status: 'completed' }), - ])); - } finally { - await client.close(); - } - }); - - it('returns structured validation errors for invalid tool input', async () => { - const server = createAgentToolMcpServer({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-secret', - assignmentId: 'assignment-1', - descriptors: [descriptor], - }); - const client = new Client({ name: 'treeseed-test-client', version: '1.0.0' }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await Promise.all([ - server.connect(serverTransport), - client.connect(clientTransport), - ]); - try { - const result = await client.callTool({ - name: 'treedx.apply_workspace_changeset', - arguments: { path: 'src/content/a.mdx' }, - }); - expect(result.isError).toBe(true); - expect(result.structuredContent).toMatchObject({ ok: false, code: 'invalid_tool_input' }); - } finally { - await client.close(); - } - }); - - it('rejects tools not included in the assignment catalog', async () => { - const server = createAgentToolMcpServer({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-secret', - assignmentId: 'assignment-1', - descriptors: [], - }); - const client = new Client({ name: 'treeseed-test-client', version: '1.0.0' }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await Promise.all([ - server.connect(serverTransport), - client.connect(clientTransport), - ]); - try { - const result = await client.callTool({ name: 'treedx.apply_workspace_changeset', arguments: {} }); - expect(result.isError).toBe(true); - expect(result.structuredContent).toMatchObject({ ok: false, code: 'tool_not_allowed' }); - } finally { - await client.close(); - } - }); - - it('uses an empty catalog when descriptor env JSON is malformed', async () => { - const previous = process.env.TREESEED_AGENT_TOOL_DESCRIPTORS; - process.env.TREESEED_AGENT_TOOL_DESCRIPTORS = '{malformed'; - try { - const server = createAgentToolMcpServer(); - const client = new Client({ name: 'treeseed-test-client', version: '1.0.0' }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await Promise.all([ - server.connect(serverTransport), - client.connect(clientTransport), - ]); - try { - const tools = await client.listTools(); - expect(tools.tools).toEqual([]); - } finally { - await client.close(); - } - } finally { - if (previous === undefined) delete process.env.TREESEED_AGENT_TOOL_DESCRIPTORS; - else process.env.TREESEED_AGENT_TOOL_DESCRIPTORS = previous; - } - }); -}); diff --git a/tests/integration/agents/capacity/assignments/agent-tool-runtime.reads-and-searches-only-bounded-assignment-repository-paths.test.ts b/tests/integration/agents/capacity/assignments/agent-tool-runtime.reads-and-searches-only-bounded-assignment-repository-paths.test.ts deleted file mode 100644 index a6c1d2e6..00000000 --- a/tests/integration/agents/capacity/assignments/agent-tool-runtime.reads-and-searches-only-bounded-assignment-repository-paths.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; - -import { rm } from 'node:fs/promises'; - -import { tmpdir } from 'node:os'; - -import { join } from 'node:path'; - -import { execFileSync } from 'node:child_process'; - -import { afterEach, describe, expect, it, vi } from 'vitest'; - -import { - callAgentTool, -} from '../../../../../src/agents/tools/agent-tool-runtime.ts'; - -import { callAgentToolWithTelemetry } from '../../../../../src/agents/tools/agent-tool-telemetry.ts'; - -import type { ExecutionProviderToolDescriptor } from '../../../../../src/agents/runtime/runtime-types.ts'; - -import { createAssignmentToolCatalog } from '../../../../../src/provider/commerce/catalog/assignment-tool-catalog.ts'; - -import { AgentSdk } from '@treeseed/sdk/sdk'; - -const tempRoots: string[] = []; - -function statusDescriptor(overrides: Partial = {}): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id: 'treeseed.status', - name: 'TreeSeed status', - description: 'Status', - inputSchema: { type: 'object', properties: {}, additionalProperties: false }, - outputSchema: { type: 'object', additionalProperties: true }, - executionTarget: 'sdk_dispatch', - mutability: 'read', - metadata: { - assignmentId: 'assignment-1', - projectId: 'project-1', - dispatchPreferredMode: 'auto', - telemetryCategory: 'treeseed', - }, - ...overrides, - }; -} - -function changedPathsDescriptor(worktreeRoot: string): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id: 'treeseed.changed_paths', - name: 'Changed paths', - description: 'Changed paths', - inputSchema: { - type: 'object', - properties: { includeDiffSummary: { type: 'boolean' } }, - additionalProperties: false, - }, - outputSchema: { type: 'object', additionalProperties: true }, - executionTarget: 'provider_runner', - mutability: 'read', - metadata: { - assignmentId: 'assignment-1', - projectId: 'project-1', - worktreeRoot, - allowedPaths: ['src/content/**'], - forbiddenPaths: ['src/content/private/**'], - telemetryCategory: 'repository', - }, - }; -} -describe('agent tool runtime', () => { -afterEach(async () => { - vi.restoreAllMocks(); - await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); - }); - -it('reads and searches only bounded assignment repository paths', async () => { - const root = mkdtempSync(join(tmpdir(), 'treeseed-agent-repository-tools-')); - tempRoots.push(root); - execFileSync('git', ['init', '-b', 'main'], { cwd: root }); - execFileSync('git', ['config', 'user.email', 'agent-tool@example.test'], { cwd: root }); - execFileSync('git', ['config', 'user.name', 'Agent Tool'], { cwd: root }); - mkdirSync(join(root, 'src/private'), { recursive: true }); - writeFileSync(join(root, 'src/scheduler.ts'), 'export const scheduler = \"weighted-deficit\";\\n', 'utf8'); - writeFileSync(join(root, 'src/private/secret.ts'), 'export const secret = true;\\n', 'utf8'); - execFileSync('git', ['add', '.'], { cwd: root }); - execFileSync('git', ['commit', '-m', 'fixture'], { cwd: root }); - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.repository.read_file', 'treeseed.repository.search'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - allowedPaths: ['src/**'], - forbiddenPaths: ['src/private/**'], - }); - const options = { - apiBaseUrl: 'https://api.example.test', - providerAccessToken: '', - assignmentId: 'assignment-1', - descriptors: catalog.descriptors, - repoRoot: root, - fetchImpl: vi.fn(async () => new Response(JSON.stringify({ ok: true, payload: { - id: 'assignment-1', teamId: 'team-1', projectId: 'project-1', stateVersion: 1, - status: 'leased', leaseState: 'leased', capacityEnvelope: {}, decisionInput: {}, metadata: {}, - } }), { status: 200 })) as typeof fetch, - }; - await expect(callAgentTool(options, 'treeseed.repository.read_file', { - path: 'src/scheduler.ts', - })).resolves.toMatchObject({ - ok: true, - payload: { path: 'src/scheduler.ts', content: expect.stringContaining('weighted-deficit'), truncated: false }, - }); - await expect(callAgentTool(options, 'treeseed.repository.search', { - query: 'weighted-deficit', - paths: ['src'], - })).resolves.toMatchObject({ - ok: true, - payload: { matches: [{ path: 'src/scheduler.ts', match: expect.stringContaining('weighted-deficit') }] }, - }); - await expect(callAgentTool(options, 'treeseed.repository.read_file', { - path: 'src/private/secret.ts', - })).resolves.toMatchObject({ ok: false, code: 'path_forbidden' }); - await expect(callAgentTool(options, 'treeseed.repository.read_file', { - path: '../outside', - })).resolves.toMatchObject({ ok: false, code: 'repository_path_invalid' }); - }); - -it('emits redacted telemetry for tool calls', async () => { - const telemetry: unknown[] = []; - const dispatch = vi.fn(async () => ({ ok: true, mode: 'inline', payload: { token: 'not-input' } })); - await callAgentToolWithTelemetry({ - apiBaseUrl: '', - providerAccessToken: '', - assignmentId: 'assignment-1', - descriptors: [statusDescriptor()], - sdk: { dispatch }, - onTelemetry: (entry) => telemetry.push(entry), - }, 'treeseed.status'); - expect(telemetry).toHaveLength(2); - expect(telemetry.at(-1)).toMatchObject({ - toolId: 'treeseed.status', - status: 'completed', - }); - }); -}); diff --git a/tests/integration/agents/capacity/providers/codex-provider.cancels-a-timed-out-turn-before-returning.test.ts b/tests/integration/agents/capacity/providers/codex-provider.cancels-a-timed-out-turn-before-returning.test.ts deleted file mode 100644 index c6a4c209..00000000 --- a/tests/integration/agents/capacity/providers/codex-provider.cancels-a-timed-out-turn-before-returning.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { describe,expect,it,vi } from 'vitest'; -import { runCodexTask,type CodexExecutionRequest } from '../../../../../src/agents/adapters/codex/execution-codex.ts'; - -const request: CodexExecutionRequest = { - taskId: 'assignment-timeout', - agentSlug: 'planner', - repoRoot: '/repo', - prompt: 'Plan until cancelled.', - allowedPaths: [], - forbiddenPaths: [], - sandboxMode: 'read_only', - approvalPolicy: 'never', - timeoutMs: 5, -}; - -describe('Codex provider timeout cancellation', () => { - it('aborts and joins the active turn before reporting the timeout', async () => { - const observed: string[] = []; - const runStreamed = vi.fn(async (_prompt: string, options?: { signal?: AbortSignal }) => ({ - events: (async function* () { - await new Promise((resolve) => options?.signal?.addEventListener('abort', () => { - observed.push('aborted'); - resolve(); - }, { once: true })); - throw Object.assign(new Error('turn aborted'), { name: 'AbortError' }); - })(), - })); - const result = await runCodexTask(request, { - client: Promise.resolve({ - startThread: () => ({ id: 'thread-timeout', run: vi.fn(), runStreamed }), - resumeThread: vi.fn(), - }), - onEvent: vi.fn(), - }); - expect(result).toMatchObject({ threadId: 'thread-timeout', status: 'failed', error: { code: 'codex_execution_timeout' } }); - expect(observed).toEqual(['aborted']); - expect(runStreamed.mock.calls[0]?.[1]?.signal?.aborted).toBe(true); - }); - - it('retains the thread id announced by the stream when the SDK populates it after start', async () => { - const runStreamed = vi.fn(async (_prompt: string, options?: { signal?: AbortSignal }) => ({ - events: (async function* () { - yield { type: 'thread.started', thread_id: 'thread-streamed-timeout' }; - await new Promise((resolve) => options?.signal?.addEventListener('abort', resolve, { once: true })); - throw Object.assign(new Error('turn aborted'), { name: 'AbortError' }); - })(), - })); - const result = await runCodexTask(request, { - client: Promise.resolve({ - startThread: () => ({ id: null, run: vi.fn(), runStreamed }), - resumeThread: vi.fn(), - }), - }); - expect(result).toMatchObject({ - threadId: 'thread-streamed-timeout', - status: 'failed', - error: { code: 'codex_execution_timeout' }, - }); - }); - - it('aborts the active turn when assignment authority is revoked', async () => { - const authority = new AbortController(); - const runStreamed = vi.fn(async (_prompt: string, options?: { signal?: AbortSignal }) => ({ - events: (async function* () { - await new Promise((resolve) => options?.signal?.addEventListener('abort', resolve, { once: true })); - throw Object.assign(new Error('turn aborted'), { name: 'AbortError' }); - })(), - })); - const pending = runCodexTask({ ...request, timeoutMs: 5_000 }, { - signal: authority.signal, - client: Promise.resolve({ - startThread: () => ({ id: 'thread-revoked', run: vi.fn(), runStreamed }), - resumeThread: vi.fn(), - }), - onEvent: vi.fn(), - }); - await vi.waitFor(() => expect(runStreamed).toHaveBeenCalledOnce()); - authority.abort(new Error('Assignment reached terminal state.')); - await expect(pending).resolves.toMatchObject({ - status: 'failed', - error: { code: 'codex_execution_authority_revoked', message: 'Assignment reached terminal state.', retryable: false }, - }); - }); -}); diff --git a/tests/integration/agents/capacity/providers/codex-provider.makes-verify-and-checkpoint-receipts-explicit-completion-gates.test.ts b/tests/integration/agents/capacity/providers/codex-provider.makes-verify-and-checkpoint-receipts-explicit-completion-gates.test.ts deleted file mode 100644 index 66c299db..00000000 --- a/tests/integration/agents/capacity/providers/codex-provider.makes-verify-and-checkpoint-receipts-explicit-completion-gates.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { appendFile, mkdtemp, readFile, rm } from 'node:fs/promises'; - -import { tmpdir } from 'node:os'; - -import { dirname, resolve } from 'node:path'; - -import { fileURLToPath } from 'node:url'; - -import { describe, expect, it, vi } from 'vitest'; - -import { resolveAgentRuntimeProviders } from '../../../../../src/agent-runtime.ts'; - -import { - checkCodexProviderReadiness, - resolveCodexProviderConfig, -} from '../../../../../src/agents/adapters/codex/codex-readiness.ts'; - -import { - codexClientEnvironment, - materializeCodexAuthFromEnv, - resolveCodexAuthFile, -} from '../../../../../src/agents/adapters/accounts/codex-auth.ts'; - -import { - CodexExecutionProviderAdapter, - buildCodexPrompt, - codexExecutionTimeoutMs, - missingCodexCompletionReceipts, - runCodexTask, - type CodexExecutionRequest, -} from '../../../../../src/agents/adapters/codex/execution-codex.ts'; - -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; - -import type { - AgentCapacityEnvelope, - DecisionExecutionInput, - ProviderAssignment, -} from '@treeseed/sdk/agent-capacity'; - -import type { ExecutionProviderInvocation } from '../../../../../src/agents/runtime/runtime-types.ts'; - -const baseRequest: CodexExecutionRequest = { - taskId: 'task:codex-provider-skeleton', - agentSlug: 'engineer', - repoRoot: '/repo', - prompt: 'Inspect the provider boundary.', - allowedPaths: [], - forbiddenPaths: [], - sandboxMode: 'read_only', - approvalPolicy: 'never', - metadata: { - subscriptionPlan: 'pro', - }, -}; - -const testDir = dirname(fileURLToPath(import.meta.url)); - -const agent: AgentRuntimeSpec = { - slug: 'engineer', - handler: 'actor', - enabled: true, - systemPrompt: '', - persona: '', - cli: {}, - triggers: [], - permissions: [], - execution: { - provider: 'codex', - model: 'gpt-5.5', - approvalPolicy: 'never', - sandboxMode: 'workspace_write', - reasoningEffort: 'medium', - allowedPaths: ['docs/**'], - forbiddenPaths: ['.git/**'], - worktree: { enabled: true }, - maxConcurrency: 1, - timeoutSeconds: 60, - cooldownSeconds: 0, - leaseSeconds: 60, - retryLimit: 1, - branchPrefix: 'agent/', - }, - outputs: { - messageTypes: [], - modelMutations: [], - }, - tools: { allowed: [] }, -}; - -function executionInvocation(input: { - agent: AgentRuntimeSpec; - runId: string; - instructions: string; - tools?: ExecutionProviderInvocation['tools']; - metadata?: Record; - workPackageMetadata?: Record; -}): ExecutionProviderInvocation { - return { - assignment: { - id: input.runId, - teamId: 'team-test', - projectId: 'project-test', - capacityProviderId: 'capacity-provider-test', - projectAgentClassId: 'agent-class-test', - mode: 'acting', - status: 'leased', - leaseState: 'leased', - agentId: input.agent.slug, - handlerId: input.agent.handler, - capacityEnvelope: {} as AgentCapacityEnvelope, - decisionInput: {} as DecisionExecutionInput, - } as ProviderAssignment, - capacityEnvelope: {} as AgentCapacityEnvelope, - decisionInput: {} as DecisionExecutionInput, - agent: input.agent, - workPackage: { - kind: 'implementation', - title: 'Codex provider test', - summary: 'Provider contract test.', - instructions: input.instructions, - context: {}, - expectedOutputs: [{ type: 'final_response', required: true }], - constraints: { - mode: 'acting', - requiredCapabilities: ['repo_read'], - allowedPaths: input.agent.execution.allowedPaths, - forbiddenPaths: input.agent.execution.forbiddenPaths, - }, - metadata: input.workPackageMetadata ?? {}, - }, - leaseToken: null, - runnerId: 'test-runner', - tools: input.tools, - metadata: { runId: input.runId, ...(input.metadata ?? {}) }, - }; -} -describe('codex execution provider', () => { -it('makes verify and checkpoint receipts explicit completion gates', () => { - const prompt = buildCodexPrompt({ - ...baseRequest, - sandboxMode: 'workspace_write', - worktreeRoot: '/repo/.agent-worktrees/tester', - tools: [ - { - kind: 'agent_tool', - id: 'treeseed.verify', - name: 'Verify', - description: 'Verify', - inputSchema: {}, - outputSchema: {}, - executionTarget: 'provider_runner', - mutability: 'read', - }, - { - kind: 'agent_tool', - id: 'treeseed.checkpoint', - name: 'Checkpoint', - description: 'Checkpoint', - inputSchema: {}, - outputSchema: {}, - executionTarget: 'provider_runner', - mutability: 'worktree_write', - }, - { - kind: 'agent_tool', - id: 'treeseed.review_decision', - name: 'Review decision', - description: 'Review decision', - inputSchema: {}, - outputSchema: {}, - executionTarget: 'provider_runner', - mutability: 'shared_state_write', - }, - ], - }); - expect(prompt).toContain('successful verification_completed receipt'); - expect(prompt).toContain('successful source_checkpoint_committed receipt containing commitSha'); - expect(prompt).toContain('successful review_decision_recorded receipt'); - expect(prompt).toContain('final response is not completion'); - }); - -it('makes live time checks and resumable closeout mandatory for every Codex assignment', () => { - const prompt = buildCodexPrompt({ - ...baseRequest, - metadata: { executionTiming: { startedAt: '2026-08-12T14:45:00.000Z', deadlineAt: '2026-08-12T15:00:00.000Z', closeoutWarningSeconds: 120 } }, - tools: [{ - kind:'agent_tool',id:'treeseed.status',name:'Assignment status',description:'Read time remaining.', - inputSchema:{ type:'object',properties:{},additionalProperties:false },outputSchema:{ type:'object' }, - executionTarget:'sdk_dispatch',mutability:'read', - }], - }); - expect(prompt).toContain('Assignment custody deadline: 2026-08-12T15:00:00.000Z'); - expect(prompt).toContain('Protected closeout allocation: 120 seconds'); - expect(prompt).toContain('Call treeseed_status immediately'); - expect(prompt).toContain('When shouldCloseOut=true'); - expect(prompt).toContain('Zod-valid project proposal may request more capacity'); - expect(prompt).toContain('It never changes the current deadline or approves itself'); -}); - -it('identifies required completion receipts from granted tools and the assigned deliverable', () => { - const tools = [ - { id: 'treeseed.verify' }, - { id: 'treeseed.checkpoint' }, - { id: 'treeseed.status' }, - ] as ExecutionProviderInvocation['tools']; - expect(missingCodexCompletionReceipts(tools ?? [], [{ - status: 'completed', - derivedEvents: [{ type: 'verification_completed' }], - }], 'failing_test_proof')).toEqual(['source_checkpoint_committed']); - expect(missingCodexCompletionReceipts(tools ?? [], [{ - status: 'completed', - derivedEvents: [{ type: 'verification_completed' }], - }], 'passing_verification')).toEqual([]); - expect(missingCodexCompletionReceipts([], [], 'discussion_response')).toEqual(['discussion_final_response']); - expect(missingCodexCompletionReceipts([], [{ - toolId: 'treeseed.discussion.respond', status: 'completed', inputSummary: { - discussionId: 'discussion-a', sourceMessageRefs: ['src/content/discussion-messages/discussion-a/source.mdx'], - }, derivedEvents: [], - }], 'discussion_response')).toEqual([]); - const researchTools = [{ id: 'research.fetch_source' }] as ExecutionProviderInvocation['tools']; - expect(missingCodexCompletionReceipts([], [], 'planning_note', 'independent-source-fetch', 2)).toEqual(['research_fetch_tool_available']); - expect(missingCodexCompletionReceipts(researchTools ?? [], [{ - status: 'completed', - derivedEvents: [{ type: 'research_citation_fetched', citation: { sourceUrl: 'https://one.test/a' } }], - }], 'planning_note', 'independent-source-fetch', 2)).toEqual(['research_independent_publishers:2']); - expect(missingCodexCompletionReceipts(researchTools ?? [], [{ - status: 'completed', - derivedEvents: [{ type: 'research_citation_fetched', citation: { sourceUrl: 'https://one.test/a' } }], - }, { - status: 'completed', - derivedEvents: [{ type: 'research_citation_fetched', citation: { sourceUrl: 'https://one.test/b' } }], - }], 'planning_note', 'independent-source-fetch', 2)).toEqual(['research_independent_publishers:2']); - expect(missingCodexCompletionReceipts(researchTools ?? [], [{ - status: 'completed', - derivedEvents: [{ type: 'research_citation_fetched', citation: { sourceUrl: 'https://one.test/a' } }], - }, { - status: 'completed', - derivedEvents: [{ type: 'research_citation_fetched', citation: { sourceUrl: 'https://two.test/b' } }], - }], 'planning_note', 'independent-source-fetch', 2)).toEqual([]); - expect(missingCodexCompletionReceipts([], [{ - status: 'completed', - derivedEvents: [{ type: 'content_created', contentRef: { model: 'note', path: 'notes/dummy.mdx' } }], - }], 'revision_verification')).toEqual(['content_subject_linked:notes/dummy.mdx']); - expect(missingCodexCompletionReceipts([], [{ - status: 'completed', - derivedEvents: [{ type: 'content_created', contentRef: { model: 'note', path: 'notes/dummy.mdx' } }], - }, { - status: 'completed', - derivedEvents: [{ type: 'content_updated', contentRef: { model: 'note', path: 'notes/dummy.mdx', subjectId: 'decision-a', subjectField: 'relatedDecisions' } }], - }], 'revision_verification')).toEqual([]); - expect(missingCodexCompletionReceipts([], [{ - status: 'completed', - derivedEvents: [{ type: 'content_created', contentRef: { model: 'question', path: 'questions/context.mdx', subjectId: 'decision-a', subjectField: 'relatedDecisions' } }], - }], 'implementation_change', null, 2, true)).toEqual(['content_artifact_kind:implementation_change']); - expect(missingCodexCompletionReceipts([], [{ - status: 'completed', - derivedEvents: [{ type: 'content_created', contentRef: { model: 'note', path: 'notes/implementation.mdx', subjectId: 'decision-a', subjectField: 'relatedDecisions' } }], - }], 'implementation_change', null, 2, true)).toEqual([]); - expect(missingCodexCompletionReceipts([], [{ - status: 'completed', - derivedEvents: [{ type: 'content_created', contentRef: { model: 'knowledge', path: 'src/content/knowledge/guide/page.mdx' } }], - }], 'knowledge_update', null, 2, true)).toEqual([]); - const operationalTools = [ - { id: 'treeseed.assignment_plan' }, - { id: 'treeseed.assignment_status_update' }, - { id: 'treeseed.assignment_summary' }, - { id: 'treeseed.content.commit' }, - ] as ExecutionProviderInvocation['tools']; - expect(missingCodexCompletionReceipts(operationalTools ?? [], [], 'planning_note', null, 2, true)).toEqual([ - 'content_committed', 'assignment_plan', 'assignment_terminal_status', 'assignment_summary', 'content_artifact_kind:planning_note', - ]); - expect(missingCodexCompletionReceipts(operationalTools ?? [], [{ - toolId: 'treeseed.assignment_plan', status: 'completed', inputSummary: { action: 'write' }, derivedEvents: [], - }, { - toolId: 'treeseed.assignment_status_update', status: 'completed', inputSummary: { status: 'completed' }, derivedEvents: [], - }, { - toolId: 'treeseed.assignment_summary', status: 'completed', inputSummary: { action: 'write', status: 'completed' }, derivedEvents: [], - }, { - toolId: 'treeseed.content.create', status: 'completed', inputSummary: {}, - derivedEvents: [{ type: 'content_created', contentRef: { model: 'note', path: 'notes/evidence.mdx', subjectId: 'objective-a', subjectField: 'about' } }], - }, { - toolId: 'treeseed.content.commit', status: 'completed', inputSummary: {}, derivedEvents: [{ type: 'content_committed', commitSha: 'a'.repeat(40) }], - }], 'planning_note', null, 2, true)).toEqual([]); - expect(missingCodexCompletionReceipts([], [], 'planning_note', null, 2, false, ['evidence-ready'])).toEqual([ - 'signal_publication:evidence-ready', - ]); - expect(missingCodexCompletionReceipts([], [{ - toolId: 'treeseed.publish_signal', status: 'completed', inputSummary: {}, - derivedEvents: [{ type: 'signal_requested', signal: { contractId: 'evidence-ready' } }], - }], 'planning_note', null, 2, false, ['evidence-ready'])).toEqual([]); - }); - -it('instructs research assignments with the callable MCP name rather than only the policy id', () => { - const prompt = buildCodexPrompt({ - ...baseRequest, - metadata: { - workPackage: { - metadata: { researchStage: 'independent-source-fetch' }, - }, - }, - tools: [{ - kind: 'agent_tool', - id: 'research.fetch_source', - name: 'Fetch governed research source', - description: 'Fetch a governed source.', - inputSchema: {}, - outputSchema: {}, - executionTarget: 'provider_runner', - mutability: 'read', - }], - }); - expect(prompt).toContain('callName research_fetch_source'); - expect(prompt).toContain('Search for and invoke the callName, not the dotted policy id.'); - }); -}); diff --git a/tests/integration/agents/capacity/providers/codex-provider.prepares-an-exact-ref-worktree-even-when-the-agent-sandbox-is-read-only.test.ts b/tests/integration/agents/capacity/providers/codex-provider.prepares-an-exact-ref-worktree-even-when-the-agent-sandbox-is-read-only.test.ts deleted file mode 100644 index 67694977..00000000 --- a/tests/integration/agents/capacity/providers/codex-provider.prepares-an-exact-ref-worktree-even-when-the-agent-sandbox-is-read-only.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { appendFile, mkdtemp, readFile, rm } from 'node:fs/promises'; - -import { tmpdir } from 'node:os'; - -import { dirname, resolve } from 'node:path'; - -import { fileURLToPath } from 'node:url'; - -import { describe, expect, it, vi } from 'vitest'; - -import { resolveAgentRuntimeProviders } from '../../../../../src/agent-runtime.ts'; - -import { - checkCodexProviderReadiness, - resolveCodexProviderConfig, -} from '../../../../../src/agents/adapters/codex/codex-readiness.ts'; - -import { - codexClientEnvironment, - materializeCodexAuthFromEnv, - resolveCodexAuthFile, -} from '../../../../../src/agents/adapters/accounts/codex-auth.ts'; - -import { - CodexExecutionProviderAdapter, - buildCodexPrompt, - codexExecutionTimeoutMs, - missingCodexCompletionReceipts, - runCodexTask, - type CodexExecutionRequest, -} from '../../../../../src/agents/adapters/codex/execution-codex.ts'; - -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; - -import type { - AgentCapacityEnvelope, - DecisionExecutionInput, - ProviderAssignment, -} from '@treeseed/sdk/agent-capacity'; - -import type { ExecutionProviderInvocation } from '../../../../../src/agents/runtime/runtime-types.ts'; - -const baseRequest: CodexExecutionRequest = { - taskId: 'task:codex-provider-skeleton', - agentSlug: 'engineer', - repoRoot: '/repo', - prompt: 'Inspect the provider boundary.', - allowedPaths: [], - forbiddenPaths: [], - sandboxMode: 'read_only', - approvalPolicy: 'never', - metadata: { - subscriptionPlan: 'pro', - }, -}; - -const testDir = dirname(fileURLToPath(import.meta.url)); - -const agent: AgentRuntimeSpec = { - slug: 'engineer', - handler: 'actor', - enabled: true, - systemPrompt: '', - persona: '', - cli: {}, - triggers: [], - permissions: [], - execution: { - provider: 'codex', - model: 'gpt-5.5', - approvalPolicy: 'never', - sandboxMode: 'workspace_write', - reasoningEffort: 'medium', - allowedPaths: ['docs/**'], - forbiddenPaths: ['.git/**'], - worktree: { enabled: true }, - maxConcurrency: 1, - timeoutSeconds: 60, - cooldownSeconds: 0, - leaseSeconds: 60, - retryLimit: 1, - branchPrefix: 'agent/', - }, - outputs: { - messageTypes: [], - modelMutations: [], - }, - tools: { allowed: [] }, -}; - -function executionInvocation(input: { - agent: AgentRuntimeSpec; - runId: string; - instructions: string; - tools?: ExecutionProviderInvocation['tools']; - metadata?: Record; - workPackageMetadata?: Record; -}): ExecutionProviderInvocation { - return { - assignment: { - id: input.runId, - teamId: 'team-test', - projectId: 'project-test', - capacityProviderId: 'capacity-provider-test', - projectAgentClassId: 'agent-class-test', - mode: 'acting', - status: 'leased', - leaseState: 'leased', - agentId: input.agent.slug, - handlerId: input.agent.handler, - capacityEnvelope: {} as AgentCapacityEnvelope, - decisionInput: {} as DecisionExecutionInput, - } as ProviderAssignment, - capacityEnvelope: {} as AgentCapacityEnvelope, - decisionInput: {} as DecisionExecutionInput, - agent: input.agent, - workPackage: { - kind: 'implementation', - title: 'Codex provider test', - summary: 'Provider contract test.', - instructions: input.instructions, - context: {}, - expectedOutputs: [{ type: 'final_response', required: true }], - constraints: { - mode: 'acting', - requiredCapabilities: ['repo_read'], - allowedPaths: input.agent.execution.allowedPaths, - forbiddenPaths: input.agent.execution.forbiddenPaths, - }, - metadata: input.workPackageMetadata ?? {}, - }, - leaseToken: null, - runnerId: 'test-runner', - tools: input.tools, - metadata: { runId: input.runId, ...(input.metadata ?? {}) }, - }; -} -describe('codex execution provider', () => { -it('requires TreeDX path discovery before Knowledge Hub batch reads', () => { - const prompt = buildCodexPrompt({ ...baseRequest, tools: [{ - kind: 'agent_tool', id: 'treedx.read_repository_files', name: 'Read repository files', - description: 'Read exact TreeDX paths.', inputSchema: {}, outputSchema: {}, - executionTarget: 'provider_runner', mutability: 'read', - }] }); - expect(prompt).toContain('Do not infer or guess Knowledge Hub file paths.'); - expect(prompt).toContain('Batch file reads fail when any requested path is absent'); - expect(prompt).toContain('Never inspect src/content or docs/src/content with shell commands'); -}); - -it('prepares an exact-ref worktree even when the agent sandbox is read-only', async () => { - const readOnlyAgent = { - ...agent, - execution: { ...agent.execution, sandboxMode: 'read_only' as const, allowedPaths: [] }, - }; - let prepared = false; - const adapter = new CodexExecutionProviderAdapter({ - repoRoot: '/repo', - prepareWorktree: async () => { - prepared = true; - return { - branchName: 'agent/researcher/read-only-ref', - worktreeRoot: '/repo/.agent-worktrees/researcher/read-only-ref', - exactBaseRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - created: true, - }; - }, - env: { TREESEED_CODEX_SUBSCRIPTION_PLAN: 'pro', TREESEED_CODEX_API_KEY: 'codex-test-key-1234567890' }, - }); - await expect(adapter.start(executionInvocation({ - agent: readOnlyAgent, - runId: 'read-only-ref', - instructions: 'Read only from the governed ref.', - metadata: { exactBaseRef: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' }, - }))).rejects.toMatchObject({ code: 'worktree_base_ref_mismatch' }); - expect(prepared).toBe(true); - }); - -it('keeps TreeDX source authority separate from the local execution worktree ref', async () => { - const prepareWorktree = vi.fn(async (input: { exactBaseRef?: string }) => ({ branchName: 'agent/guide/acting', worktreeRoot: '/repo/.agent-worktrees/guide/acting', exactBaseRef: 'local-commit', created: true })); - const run = vi.fn(async () => ({ finalResponse: 'Completed through TreeDX.', items: [], usage: { input_tokens: 2, output_tokens: 1 } })); - const adapter = new CodexExecutionProviderAdapter({ repoRoot: '/repo', prepareWorktree: prepareWorktree as never, - createCodexClient: async () => ({ startThread: () => ({ id: 'thread', run }), resumeThread: () => ({ id: 'thread', run }) }), - env: { TREESEED_CODEX_SUBSCRIPTION_PLAN: 'pro', TREESEED_CODEX_API_KEY: 'codex-test-key-1234567890' } }); - const result = await adapter.start(executionInvocation({ agent, runId: 'treedx-ref', instructions: 'Use TreeDX.', metadata: { exactBaseRef: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', contentAuthority: 'treedx' } })); - expect(prepareWorktree).toHaveBeenCalledWith(expect.objectContaining({ exactBaseRef: undefined })); - expect(result.metadata).toMatchObject({ baseRef: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' }); -}); - -it('keeps provider code free of direct command invocation APIs', async () => { - const files = [ - resolve(testDir, '../../../../../src/agents/adapters/codex/codex-readiness.ts'), - resolve(testDir, '../../../../../src/agents/adapters/accounts/codex-auth.ts'), - resolve(testDir, '../../../../../src/agents/adapters/codex/execution-codex.ts'), - ]; - const disallowed = [ - /node:child_process/, - /child_process/, - /\bspawn\s*\(/, - /\bexec\s*\(/, - /\bexeca\s*\(/, - /codex\s+exec/, - /npx\s+codex/, - ]; - - for (const file of files) { - const source = await readFile(file, 'utf8'); - for (const pattern of disallowed) { - expect(source, `${file} should not match ${pattern}`).not.toMatch(pattern); - } - } - }); -}); diff --git a/tests/integration/agents/capacity/providers/codex-provider.recovers-required-response-suspension-from-durable-tool-evidence.test.ts b/tests/integration/agents/capacity/providers/codex-provider.recovers-required-response-suspension-from-durable-tool-evidence.test.ts deleted file mode 100644 index f87c05ed..00000000 --- a/tests/integration/agents/capacity/providers/codex-provider.recovers-required-response-suspension-from-durable-tool-evidence.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {describe,expect,it} from 'vitest'; -import {hasRequiredResponseSuspensionReceipt} from '../../../../../src/agents/adapters/reconciliation/execution-codex-adapter.ts'; - -describe('Codex required-response suspension evidence',()=>{ - it('accepts only the successful exact required-response tool receipt',()=>{ - expect(hasRequiredResponseSuspensionReceipt([{ - status:'completed',toolId:'treeseed.discussion.respond', - inputSummary:{requiredResponse:true},outputSummary:{ok:true,suspended:true}, - }])).toBe(true); - expect(hasRequiredResponseSuspensionReceipt([{ - status:'failed',toolId:'treeseed.discussion.respond', - inputSummary:{requiredResponse:true},outputSummary:{suspended:true}, - }])).toBe(false); - expect(hasRequiredResponseSuspensionReceipt([{ - status:'completed',toolId:'treeseed.discussion.respond', - inputSummary:{requiredResponse:false},outputSummary:{ok:true,suspended:false}, - }])).toBe(false); - }); -}); diff --git a/tests/integration/agents/capacity/providers/codex-provider.resumes-the-same-thread-once-to-obtain-missing-completion-receipts.test.ts b/tests/integration/agents/capacity/providers/codex-provider.resumes-the-same-thread-once-to-obtain-missing-completion-receipts.test.ts deleted file mode 100644 index 87ad673e..00000000 --- a/tests/integration/agents/capacity/providers/codex-provider.resumes-the-same-thread-once-to-obtain-missing-completion-receipts.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { appendFile, mkdtemp, readFile, rm } from 'node:fs/promises'; - -import { tmpdir } from 'node:os'; - -import { dirname, resolve } from 'node:path'; - -import { fileURLToPath } from 'node:url'; - -import { describe, expect, it, vi } from 'vitest'; - -import { resolveAgentRuntimeProviders } from '../../../../../src/agent-runtime.ts'; - -import { - checkCodexProviderReadiness, - resolveCodexProviderConfig, -} from '../../../../../src/agents/adapters/codex/codex-readiness.ts'; - -import { - codexClientEnvironment, - materializeCodexAuthFromEnv, - resolveCodexAuthFile, -} from '../../../../../src/agents/adapters/accounts/codex-auth.ts'; - -import { - CodexExecutionProviderAdapter, - buildCodexPrompt, - codexExecutionTimeoutMs, - missingCodexCompletionReceipts, - runCodexTask, - type CodexExecutionRequest, -} from '../../../../../src/agents/adapters/codex/execution-codex.ts'; - -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; - -import type { - AgentCapacityEnvelope, - DecisionExecutionInput, - ProviderAssignment, -} from '@treeseed/sdk/agent-capacity'; - -import type { ExecutionProviderInvocation } from '../../../../../src/agents/runtime/runtime-types.ts'; - -const baseRequest: CodexExecutionRequest = { - taskId: 'task:codex-provider-skeleton', - agentSlug: 'engineer', - repoRoot: '/repo', - prompt: 'Inspect the provider boundary.', - allowedPaths: [], - forbiddenPaths: [], - sandboxMode: 'read_only', - approvalPolicy: 'never', - metadata: { - subscriptionPlan: 'pro', - }, -}; - -const testDir = dirname(fileURLToPath(import.meta.url)); - -const agent: AgentRuntimeSpec = { - slug: 'engineer', - handler: 'actor', - enabled: true, - systemPrompt: '', - persona: '', - cli: {}, - triggers: [], - permissions: [], - execution: { - provider: 'codex', - model: 'gpt-5.5', - approvalPolicy: 'never', - sandboxMode: 'workspace_write', - reasoningEffort: 'medium', - allowedPaths: ['docs/**'], - forbiddenPaths: ['.git/**'], - worktree: { enabled: true }, - maxConcurrency: 1, - timeoutSeconds: 60, - cooldownSeconds: 0, - leaseSeconds: 60, - retryLimit: 1, - branchPrefix: 'agent/', - }, - outputs: { - messageTypes: [], - modelMutations: [], - }, - tools: { allowed: [] }, -}; - -function executionInvocation(input: { - agent: AgentRuntimeSpec; - runId: string; - instructions: string; - tools?: ExecutionProviderInvocation['tools']; - metadata?: Record; - workPackageMetadata?: Record; -}): ExecutionProviderInvocation { - return { - assignment: { - id: input.runId, - teamId: 'team-test', - projectId: 'project-test', - capacityProviderId: 'capacity-provider-test', - projectAgentClassId: 'agent-class-test', - mode: 'acting', - status: 'leased', - leaseState: 'leased', - agentId: input.agent.slug, - handlerId: input.agent.handler, - capacityEnvelope: {} as AgentCapacityEnvelope, - decisionInput: {} as DecisionExecutionInput, - } as ProviderAssignment, - capacityEnvelope: {} as AgentCapacityEnvelope, - decisionInput: {} as DecisionExecutionInput, - agent: input.agent, - workPackage: { - kind: 'implementation', - title: 'Codex provider test', - summary: 'Provider contract test.', - instructions: input.instructions, - context: {}, - expectedOutputs: [{ type: 'final_response', required: true }], - constraints: { - mode: 'acting', - requiredCapabilities: ['repo_read'], - allowedPaths: input.agent.execution.allowedPaths, - forbiddenPaths: input.agent.execution.forbiddenPaths, - }, - metadata: input.workPackageMetadata ?? {}, - }, - leaseToken: null, - runnerId: 'test-runner', - tools: input.tools, - metadata: { runId: input.runId, ...(input.metadata ?? {}) }, - }; -} -describe('codex execution provider', () => { -it('resumes the same thread once to obtain missing completion receipts', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-codex-receipt-')); - const telemetryPath = resolve(root, 'events.jsonl'); - const run = vi.fn() - .mockResolvedValueOnce({ finalResponse: 'Finished too early.', items: [], usage: { input_tokens: 4, output_tokens: 2 } }) - .mockImplementationOnce(async () => { - await appendFile(telemetryPath, `${JSON.stringify({ - status: 'completed', - derivedEvents: [ - { type: 'verification_completed', status: 'passed' }, - { type: 'source_checkpoint_committed', commitSha: 'abc123' }, - { type: 'content_created', contentRef: { model: 'note', path: 'notes/implementation.mdx', subjectId: 'decision-a', subjectField: 'relatedDecisions' } }, - ], - })}\n`); - return { finalResponse: 'Receipts complete.', items: [], usage: { input_tokens: 3, output_tokens: 1 } }; - }); - const startThread = vi.fn(() => ({ id: 'thread-receipts', run })); - const resumeThread = vi.fn(() => ({ id: 'thread-receipts', run })); - const cleanup = vi.fn(async () => undefined); - const createCodexClient = vi.fn(async () => ({ startThread, resumeThread, cleanup })); - const adapter = new CodexExecutionProviderAdapter({ - repoRoot: '/repo', - prepareWorktree: async () => ({ - branchName: 'agent/tester/run-receipts', - worktreeRoot: '/repo/.agent-worktrees/tester/run-receipts', - exactBaseRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - created: true, - }), - createCodexClient, - env: { TREESEED_CODEX_SUBSCRIPTION_PLAN: 'pro', TREESEED_CODEX_API_KEY: 'codex-test-key-1234567890' }, - }); - try { - const result = await adapter.start(executionInvocation({ - agent, - runId: 'run-receipts', - instructions: 'Add the governed test.', - workPackageMetadata: { artifactKind: 'implementation_change', requireContentArtifact: true }, - tools: [ - { - kind: 'agent_tool', id: 'treeseed.verify', name: 'Verify', description: 'Verify', - inputSchema: {}, outputSchema: {}, executionTarget: 'provider_runner', mutability: 'read', - }, - { - kind: 'agent_tool', id: 'treeseed.checkpoint', name: 'Checkpoint', description: 'Checkpoint', - inputSchema: {}, outputSchema: {}, executionTarget: 'provider_runner', mutability: 'worktree_write', - }, - ], - metadata: { - exactBaseRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - toolTelemetryPath: telemetryPath, - }, - })); - expect(startThread).toHaveBeenCalledTimes(1); - expect(resumeThread).toHaveBeenCalledWith('thread-receipts', expect.any(Object)); - expect(createCodexClient).toHaveBeenCalledTimes(1); - expect(run).toHaveBeenCalledTimes(2); - expect(String(run.mock.calls[1]?.[0])).toContain('Missing required tool receipts'); - expect(String(run.mock.calls[1]?.[0])).toContain('content_artifact_kind:implementation_change'); - expect(String(run.mock.calls[1]?.[0])).not.toContain('Available TreeSeed tools:'); - expect(String(run.mock.calls[1]?.[0])).not.toContain('Work package:'); - expect(result).toMatchObject({ - status: 'completed', - summary: 'Receipts complete.', - metadata: { - codex: { - usage: { inputTokens: 7, outputTokens: 3 }, - }, - }, - }); - expect(cleanup).toHaveBeenCalledTimes(1); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - -it('reserves closeout time and resumes a timed-out main turn on the same thread', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-codex-closeout-')); - const telemetryPath = resolve(root, 'events.jsonl'); - const run = vi.fn() - .mockImplementationOnce((_prompt: string, options?: { signal?: AbortSignal }) => new Promise((_, reject) => { - options?.signal?.addEventListener('abort', () => reject(Object.assign(new Error('turn aborted'), { name: 'AbortError' })), { once: true }); - })) - .mockImplementationOnce(async () => { - await appendFile(telemetryPath, `${JSON.stringify({ status: 'completed', derivedEvents: [{ - type: 'content_created', contentRef: { model: 'note', path: 'notes/closeout.mdx', subjectId: 'objective-a', subjectField: 'about' }, - }] })}\n`); - return { finalResponse: 'Closed out.', items: [], usage: { input_tokens: 3, output_tokens: 1 } }; - }); - const startThread = vi.fn(() => ({ id: 'thread-closeout', run })); - const resumeThread = vi.fn(() => ({ id: 'thread-closeout', run })); - const adapter = new CodexExecutionProviderAdapter({ - repoRoot: '/repo', - prepareWorktree: async () => ({ - branchName: 'agent/tester/run-closeout', worktreeRoot: '/repo/.agent-worktrees/tester/run-closeout', - exactBaseRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', created: true, - }), - createCodexClient: async () => ({ startThread, resumeThread }), - env: { TREESEED_CODEX_SUBSCRIPTION_PLAN: 'pro', TREESEED_CODEX_API_KEY: 'codex-test-key-1234567890' }, - }); - try { - const closeoutAgent = { ...agent, execution: { ...agent.execution, timeoutSeconds: 0.2 } }; - const result = await adapter.start(executionInvocation({ - agent: closeoutAgent, runId: 'run-closeout', instructions: 'Create the required linked note.', - workPackageMetadata: { - artifactKind: 'planning_note', requireContentArtifact: true, - executionTiming: { deadlineAt: new Date(Date.now() + 31_000).toISOString(), closeoutWarningSeconds: 150 }, - }, - metadata: { exactBaseRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', toolTelemetryPath: telemetryPath }, - })); - expect(startThread).toHaveBeenCalledOnce(); - expect(resumeThread).toHaveBeenCalledWith('thread-closeout', expect.any(Object)); - expect(String(run.mock.calls[1]?.[0])).toContain('assignment is now in closeout'); - expect(result).toMatchObject({ status: 'completed', summary: 'Closed out.' }); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); - -it('recovers completion when correction times out after every durable receipt exists', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-codex-durable-closeout-')); - const telemetryPath = resolve(root, 'events.jsonl'); - const run = vi.fn() - .mockResolvedValueOnce({ finalResponse: 'Needs closeout.', items: [], usage: { input_tokens: 2, output_tokens: 1 } }) - .mockImplementationOnce(async (_prompt: string, options?: { signal?: AbortSignal }) => { - await appendFile(telemetryPath, `${JSON.stringify({ status: 'completed', derivedEvents: [{ - type: 'content_created', contentRef: { model: 'note', path: 'notes/durable.mdx', subjectId: 'objective-a', subjectField: 'about' }, - }] })}\n`); - return new Promise((_, reject) => options?.signal?.addEventListener('abort', - () => reject(Object.assign(new Error('turn aborted after receipts'), { name: 'AbortError' })), { once: true })); - }); - const adapter = new CodexExecutionProviderAdapter({ - repoRoot: '/repo', createCodexClient: async () => ({ - startThread: () => ({ id: 'thread-durable', run }), resumeThread: () => ({ id: 'thread-durable', run }), - }), env: { TREESEED_CODEX_SUBSCRIPTION_PLAN: 'pro', TREESEED_CODEX_API_KEY: 'codex-test-key-1234567890' }, - }); - try { - const result = await adapter.start(executionInvocation({ - agent: { ...agent, execution: { ...agent.execution, sandboxMode: 'read_only', timeoutSeconds: .05 } }, - runId: 'run-durable-closeout', instructions: 'Create the required linked note.', - workPackageMetadata: { artifactKind: 'planning_note', requireContentArtifact: true }, - metadata: { toolTelemetryPath: telemetryPath }, - })); - expect(result).toMatchObject({ status: 'completed', metadata: { codex: { metadata: { - completionRecovery: { source: 'durable_tool_receipts', providerResultCode: 'codex_execution_timeout' }, - } } } }); - } finally { await rm(root, { recursive: true, force: true }); } -}); - -it('caps activity runtime at the provider maximum while honoring shorter profiles', () => { - expect(codexExecutionTimeoutMs(900_000, 1_800)).toBe(900_000); - expect(codexExecutionTimeoutMs(900_000, 60)).toBe(60_000); - expect(codexExecutionTimeoutMs(900_000, undefined)).toBe(900_000); - }); - -it('keeps both capacity-provider services on the canonical Codex timeout default', async () => { - const compose = await readFile(resolve(testDir, '../../../../../compose.capacity-provider.yml'), 'utf8'); - expect(compose.match(/TREESEED_CODEX_TIMEOUT_MS: \$\{TREESEED_CODEX_TIMEOUT_MS:-900000\}/gu)).toHaveLength(2); - }); - -it('reports unavailable instead of advertising capacity when authentication is absent', async () => { - const adapter = new CodexExecutionProviderAdapter({ - env: { TREESEED_CODEX_AUTH_FILE: '/missing/codex-auth.json' }, - }); - - await expect(adapter.observe({} as never)).resolves.toMatchObject({ - available: false, - pressure: 'exhausted', - metadata: { authMode: 'missing' }, - }); - }); - -it('reports missing SDK as a warning for non-Codex selections and blocker for Codex defaults', () => { - const missing = () => { - throw new Error('missing'); - }; - - const optional = checkCodexProviderReadiness({ - env: { - TREESEED_AGENT_EXECUTION_PROVIDER: 'jira', - }, - nodeVersion: 'v24.0.0', - resolvePackage: missing, - }); - expect(optional).toMatchObject({ - ok: true, - providerSelected: false, - sdkInstalled: false, - blockingIssues: [], - }); - expect(optional.warnings).toEqual(expect.arrayContaining([ - expect.stringContaining('@openai/codex-sdk is not installed'), - ])); - - const selected = checkCodexProviderReadiness({ - env: {}, - nodeVersion: 'v24.0.0', - resolvePackage: missing, - }); - expect(selected).toMatchObject({ - ok: false, - providerSelected: true, - sdkInstalled: false, - }); - expect(selected.blockingIssues).toEqual(expect.arrayContaining([ - expect.stringContaining('@openai/codex-sdk is required'), - ])); - }); - -it('reports installed SDK, selected profile, and auth hint', () => { - const readiness = checkCodexProviderReadiness({ - env: { - TREESEED_EXECUTION_PROVIDER: 'codex', - TREESEED_CODEX_SUBSCRIPTION_PLAN: 'pro', - HOME: '/home/test', - }, - nodeVersion: 'v24.0.0', - resolvePackage: () => '/repo/node_modules/@openai/codex-sdk/dist/index.js', - fileExists: (path) => path === '/home/test/.codex/auth.json', - }); - - expect(readiness).toMatchObject({ - ok: true, - providerSelected: true, - sdkInstalled: true, - nodeVersionOk: true, - authDetected: true, - authMode: 'codex_auth_json', - authPath: '/home/test/.codex/auth.json', - subscriptionPlan: 'pro', - defaultModel: 'gpt-5.5', - warnings: [], - blockingIssues: [], - }); - }); -}); diff --git a/tests/integration/agents/capacity/providers/codex-provider.uses-codex-api-key-as-an-api-billed-fallback-when-auth-json-is-absent.test.ts b/tests/integration/agents/capacity/providers/codex-provider.uses-codex-api-key-as-an-api-billed-fallback-when-auth-json-is-absent.test.ts deleted file mode 100644 index 05954d9c..00000000 --- a/tests/integration/agents/capacity/providers/codex-provider.uses-codex-api-key-as-an-api-billed-fallback-when-auth-json-is-absent.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { appendFile, mkdtemp, readFile, rm } from 'node:fs/promises'; - -import { tmpdir } from 'node:os'; - -import { dirname, resolve } from 'node:path'; - -import { fileURLToPath } from 'node:url'; - -import { describe, expect, it, vi } from 'vitest'; - -import { resolveAgentRuntimeProviders } from '../../../../../src/agent-runtime.ts'; - -import { - checkCodexProviderReadiness, - resolveCodexProviderConfig, -} from '../../../../../src/agents/adapters/codex/codex-readiness.ts'; - -import { - codexClientEnvironment, - materializeCodexAuthFromEnv, - resolveCodexAuthFile, -} from '../../../../../src/agents/adapters/accounts/codex-auth.ts'; - -import { - CodexExecutionProviderAdapter, - buildCodexPrompt, - codexExecutionTimeoutMs, - missingCodexCompletionReceipts, - runCodexTask, - type CodexExecutionRequest, -} from '../../../../../src/agents/adapters/codex/execution-codex.ts'; - -import type { AgentRuntimeSpec } from '@treeseed/sdk/types/agents'; - -import type { - AgentCapacityEnvelope, - DecisionExecutionInput, - ProviderAssignment, -} from '@treeseed/sdk/agent-capacity'; - -import type { ExecutionProviderInvocation } from '../../../../../src/agents/runtime/runtime-types.ts'; - -const baseRequest: CodexExecutionRequest = { - taskId: 'task:codex-provider-skeleton', - agentSlug: 'engineer', - repoRoot: '/repo', - prompt: 'Inspect the provider boundary.', - allowedPaths: [], - forbiddenPaths: [], - sandboxMode: 'read_only', - approvalPolicy: 'never', - metadata: { - subscriptionPlan: 'pro', - }, -}; - -const testDir = dirname(fileURLToPath(import.meta.url)); - -const agent: AgentRuntimeSpec = { - slug: 'engineer', - handler: 'actor', - enabled: true, - systemPrompt: '', - persona: '', - cli: {}, - triggers: [], - permissions: [], - execution: { - provider: 'codex', - model: 'gpt-5.5', - approvalPolicy: 'never', - sandboxMode: 'workspace_write', - reasoningEffort: 'medium', - allowedPaths: ['docs/**'], - forbiddenPaths: ['.git/**'], - worktree: { enabled: true }, - maxConcurrency: 1, - timeoutSeconds: 60, - cooldownSeconds: 0, - leaseSeconds: 60, - retryLimit: 1, - branchPrefix: 'agent/', - }, - outputs: { - messageTypes: [], - modelMutations: [], - }, - tools: { allowed: [] }, -}; - -function executionInvocation(input: { - agent: AgentRuntimeSpec; - runId: string; - instructions: string; - tools?: ExecutionProviderInvocation['tools']; - metadata?: Record; - workPackageMetadata?: Record; -}): ExecutionProviderInvocation { - return { - assignment: { - id: input.runId, - teamId: 'team-test', - projectId: 'project-test', - capacityProviderId: 'capacity-provider-test', - projectAgentClassId: 'agent-class-test', - mode: 'acting', - status: 'leased', - leaseState: 'leased', - agentId: input.agent.slug, - handlerId: input.agent.handler, - capacityEnvelope: {} as AgentCapacityEnvelope, - decisionInput: {} as DecisionExecutionInput, - } as ProviderAssignment, - capacityEnvelope: {} as AgentCapacityEnvelope, - decisionInput: {} as DecisionExecutionInput, - agent: input.agent, - workPackage: { - kind: 'implementation', - title: 'Codex provider test', - summary: 'Provider contract test.', - instructions: input.instructions, - context: {}, - expectedOutputs: [{ type: 'final_response', required: true }], - constraints: { - mode: 'acting', - requiredCapabilities: ['repo_read'], - allowedPaths: input.agent.execution.allowedPaths, - forbiddenPaths: input.agent.execution.forbiddenPaths, - }, - metadata: input.workPackageMetadata ?? {}, - }, - leaseToken: null, - runnerId: 'test-runner', - tools: input.tools, - metadata: { runId: input.runId, ...(input.metadata ?? {}) }, - }; -} -describe('codex execution provider', () => { -it('uses TREESEED_CODEX_API_KEY as an API-billed fallback when auth.json is absent', () => { - const readiness = checkCodexProviderReadiness({ - env: { - TREESEED_EXECUTION_PROVIDER: 'codex', - TREESEED_CODEX_API_KEY: 'codex-test-key-1234567890', - HOME: '/home/test', - }, - nodeVersion: 'v24.0.0', - resolvePackage: () => '/repo/node_modules/@openai/codex-sdk/dist/index.js', - fileExists: () => false, - }); - - expect(readiness).toMatchObject({ - ok: true, - authDetected: true, - authMode: 'api_key', - authPath: '/home/test/.codex/auth.json', - }); - }); - -it('warns when local Codex config uses unsupported default service tier', () => { - const readiness = checkCodexProviderReadiness({ - env: { - TREESEED_EXECUTION_PROVIDER: 'codex', - HOME: '/home/test', - }, - nodeVersion: 'v24.0.0', - resolvePackage: () => '/repo/node_modules/@openai/codex-sdk/dist/index.js', - fileExists: (path) => path === '/home/test/.codex/auth.json' || path === '/home/test/.codex/config.toml', - readFile: () => 'service_tier = "default"\n', - }); - - expect(readiness.ok).toBe(true); - expect(readiness.warnings).toEqual(expect.arrayContaining([ - expect.stringContaining('service_tier=default'), - ])); - }); - -it('resolves parity auth under /data/codex and materializes auth JSON secrets once', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-codex-auth-')); - const authJson = JSON.stringify({ OPENAI_CODEX_LOGIN: 'test-login', refresh_token: 'test-refresh' }); - const env = { - TREESEED_PROCESSING_PARITY: '1', - TREESEED_DATA_DIR: root, - TREESEED_CODEX_AUTH_JSON_B64: Buffer.from(authJson).toString('base64'), - } as NodeJS.ProcessEnv; - try { - expect(resolveCodexAuthFile(env)).toBe(resolve(root, 'codex/auth.json')); - const first = await materializeCodexAuthFromEnv(env); - expect(first).toMatchObject({ materialized: true, reason: 'created' }); - expect(env.TREESEED_CODEX_AUTH_FILE).toBe(resolve(root, 'codex/auth.json')); - expect(env.CODEX_HOME).toBe(resolve(root, 'codex')); - expect(await readFile(resolve(root, 'codex/auth.json'), 'utf8')).toContain('test-refresh'); - - const second = await materializeCodexAuthFromEnv({ - ...env, - TREESEED_CODEX_AUTH_JSON_B64: Buffer.from(JSON.stringify({ refresh_token: 'stale-copy' })).toString('base64'), - } as NodeJS.ProcessEnv); - expect(second).toMatchObject({ materialized: false, reason: 'exists' }); - expect(await readFile(resolve(root, 'codex/auth.json'), 'utf8')).toContain('test-refresh'); - expect(codexClientEnvironment(env)).toMatchObject({ - CODEX_HOME: resolve(root, 'codex'), - TREESEED_CODEX_AUTH_FILE: resolve(root, 'codex/auth.json'), - }); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - -it('selects the canonical codex provider', () => { - const readiness = checkCodexProviderReadiness({ - env: { - TREESEED_EXECUTION_PROVIDER: 'codex', - HOME: '/home/test', - }, - nodeVersion: 'v24.0.0', - resolvePackage: () => '/repo/node_modules/@openai/codex-sdk/dist/index.js', - fileExists: (path) => path === '/home/test/.codex/auth.json', - }); - - expect(readiness).toMatchObject({ - ok: true, - providerSelected: true, - authMode: 'codex_auth_json', - }); - }); - -it('normalizes provider configuration from environment values', () => { - const config = resolveCodexProviderConfig({ - TREESEED_CODEX_SUBSCRIPTION_PLAN: 'business', - TREESEED_CODEX_APPROVAL_POLICY: 'on-request', - TREESEED_CODEX_SANDBOX_MODE: 'workspace-write', - TREESEED_CODEX_TIMEOUT_MS: '1200', - }); - - expect(config).toMatchObject({ - providerId: 'codex', - subscriptionPlan: 'business', - approvalPolicy: 'on_request', - sandboxMode: 'workspace_write', - timeoutMs: 1200, - }); - }); - -it('registers codex as the execution provider', () => { - const runtime = resolveAgentRuntimeProviders('/repo', { - execution: 'codex', - mutation: 'local_branch', - repository: 'git', - verification: 'local', - notification: 'sdk_message', - research: 'project_graph', - }); - - expect(runtime.execution).toBeInstanceOf(CodexExecutionProviderAdapter); - }); - -it('returns waiting for workspace-write requests missing worktree or allowed paths', async () => { - const createCodexClient = vi.fn(); - - const missingWorktree = await runCodexTask({ - ...baseRequest, - sandboxMode: 'workspace_write', - allowedPaths: ['src/content/knowledge/**'], - }, { createCodexClient }); - expect(missingWorktree).toMatchObject({ - status: 'waiting', - error: { - code: 'worktree_required', - }, - }); - - const missingAllowedPaths = await runCodexTask({ - ...baseRequest, - sandboxMode: 'workspace_write', - worktreeRoot: '/repo/.agent-worktrees/task', - allowedPaths: [], - }, { createCodexClient }); - expect(missingAllowedPaths).toMatchObject({ - status: 'waiting', - error: { - code: 'allowed_paths_required', - }, - }); - - expect(createCodexClient).not.toHaveBeenCalled(); - }); - -it('rejects a prepared worktree that cannot prove the governed exact base ref', async () => { - const adapter = new CodexExecutionProviderAdapter({ - repoRoot: '/repo', - prepareWorktree: async () => ({ - branchName: 'agent/engineer/run-ref-mismatch', - worktreeRoot: '/repo/.agent-worktrees/engineer/run-ref-mismatch', - exactBaseRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - created: true, - }), - env: { TREESEED_CODEX_SUBSCRIPTION_PLAN: 'pro', TREESEED_CODEX_API_KEY: 'codex-test-key-1234567890' }, - }); - await expect(adapter.start(executionInvocation({ - agent, runId: 'run-ref-mismatch', instructions: 'Implement only from the governed ref.', - metadata: { exactBaseRef: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' }, - }))).rejects.toMatchObject({ code: 'worktree_base_ref_mismatch', retryable: false }); - }); -}); diff --git a/tests/integration/agents/commerce/catalog/agent-tool-runtime.rejects-tools-not-listed-in-the-assignment-catalog.test.ts b/tests/integration/agents/commerce/catalog/agent-tool-runtime.rejects-tools-not-listed-in-the-assignment-catalog.test.ts deleted file mode 100644 index 7c15c737..00000000 --- a/tests/integration/agents/commerce/catalog/agent-tool-runtime.rejects-tools-not-listed-in-the-assignment-catalog.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; - -import { rm } from 'node:fs/promises'; - -import { tmpdir } from 'node:os'; - -import { join } from 'node:path'; - -import { execFileSync } from 'node:child_process'; - -import { afterEach, describe, expect, it, vi } from 'vitest'; - -import { - callAgentTool, -} from '../../../../../src/agents/tools/agent-tool-runtime.ts'; - -import { callAgentToolWithTelemetry } from '../../../../../src/agents/tools/agent-tool-telemetry.ts'; - -import type { ExecutionProviderToolDescriptor } from '../../../../../src/agents/runtime/runtime-types.ts'; - -import { createAssignmentToolCatalog } from '../../../../../src/provider/commerce/catalog/assignment-tool-catalog.ts'; - -import { AgentSdk } from '@treeseed/sdk/sdk'; - -const tempRoots: string[] = []; - -function assignmentStatusEnvelope() { - return { ok: true, payload: { - id: 'assignment-1', teamId: 'team-1', projectId: 'project-1', workDayId: 'workday-1', - stateVersion: 2, status: 'leased', leaseState: 'leased', assignedAt: '2026-08-14T00:00:00.000Z', - capacityEnvelope: { reservedSeconds: 900, budget: { time: { hardDeadlineAt: '2099-01-01T00:00:00.000Z' } } }, - decisionInput: { input: { activityType: 'planning' } }, metadata: { workdayRunId: 'run-1' }, - } }; -} - -function statusDescriptor(overrides: Partial = {}): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id: 'treeseed.status', - name: 'TreeSeed status', - description: 'Status', - inputSchema: { type: 'object', properties: {}, additionalProperties: false }, - outputSchema: { type: 'object', additionalProperties: true }, - executionTarget: 'sdk_dispatch', - mutability: 'read', - metadata: { - assignmentId: 'assignment-1', - projectId: 'project-1', - dispatchPreferredMode: 'auto', - telemetryCategory: 'treeseed', - }, - ...overrides, - }; -} - -function changedPathsDescriptor(worktreeRoot: string): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id: 'treeseed.changed_paths', - name: 'Changed paths', - description: 'Changed paths', - inputSchema: { - type: 'object', - properties: { includeDiffSummary: { type: 'boolean' } }, - additionalProperties: false, - }, - outputSchema: { type: 'object', additionalProperties: true }, - executionTarget: 'provider_runner', - mutability: 'read', - metadata: { - assignmentId: 'assignment-1', - projectId: 'project-1', - worktreeRoot, - allowedPaths: ['src/content/**'], - forbiddenPaths: ['src/content/private/**'], - telemetryCategory: 'repository', - }, - }; -} -describe('agent tool runtime', () => { -afterEach(async () => { - vi.restoreAllMocks(); - await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); - }); - -it('rejects tools not listed in the assignment catalog', async () => { - await expect(callAgentTool({ - apiBaseUrl: '', - providerAccessToken: '', - assignmentId: 'assignment-1', - descriptors: [], - }, 'treeseed.status')).resolves.toMatchObject({ ok: false, code: 'tool_not_allowed' }); - }); - -it('validates required fields and additional properties before execution', async () => { - const descriptor: ExecutionProviderToolDescriptor = { - ...statusDescriptor(), - id: 'treedx.apply_workspace_changeset', - executionTarget: 'treedx_proxy', - mutability: 'content_write', - inputSchema: { - type: 'object', - properties: { - path: { type: 'string' }, - content: { type: 'string' }, - }, - required: ['path', 'content'], - additionalProperties: false, - }, - }; - await expect(callAgentTool({ - apiBaseUrl: '', - providerAccessToken: '', - assignmentId: 'assignment-1', - descriptors: [descriptor], - }, 'treedx.apply_workspace_changeset', { path: 'src/content/a.mdx' })).resolves.toMatchObject({ - ok: false, - code: 'invalid_tool_input', - metadata: { field: 'content' }, - }); - await expect(callAgentTool({ - apiBaseUrl: '', - providerAccessToken: '', - assignmentId: 'assignment-1', - descriptors: [statusDescriptor()], - }, 'treeseed.status', { extra: true })).resolves.toMatchObject({ - ok: false, - code: 'invalid_tool_input', - metadata: { field: 'extra' }, - }); - }); - -it('uses descriptor dispatch preferred mode for sdk dispatch tools', async () => { - const dispatch = vi.fn(async () => ({ ok: true, mode: 'inline', payload: {} })); - await expect(callAgentTool({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', - descriptors: [statusDescriptor()], - sdk: { dispatch }, - }, 'treeseed.status')).resolves.toMatchObject({ ok: true }); - expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ - namespace: 'workflow', - operation: 'status', - preferredMode: 'auto', - })); - }); - -it('reads assignment status from the provider-authorized API without exposing lease credentials', async () => { - const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ ok: true, payload: { - id: 'assignment-1', workDayId: 'workday-1', projectId: 'project-1', agentId: 'guide-steward', - projectAgentClassId: 'class-1', handlerId: 'writer', mode: 'planning', status: 'leased', - leaseState: 'leased', leaseExpiresAt: '2026-08-04T00:00:00.000Z', leaseToken: 'must-not-leak', - decisionInput: { input: { activityType: 'planning' } }, capacityEnvelope: { reservedSeconds: 1 }, - metadata: { workdayRunId: 'run-1' }, treedxProxyHandle: { token: 'must-not-leak' }, - } }), { status: 200, headers: { 'content-type': 'application/json' } })); - const result = await callAgentTool({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', - descriptors: [statusDescriptor()], - repoRoot: '/repo/.agent-worktrees/assignment-1', - fetchImpl: fetchImpl as typeof fetch, - }, 'treeseed.status'); - expect(result).toMatchObject({ ok: true, payload: { - assignmentId: 'assignment-1', workdayId: 'workday-1', workdayRunId: 'run-1', - activityType: 'planning', status: 'leased', time: { allocatedSeconds: 1, phase: 'working', shouldCloseOut: false }, - } }); - expect(JSON.stringify(result)).not.toContain('must-not-leak'); - expect(fetchImpl).toHaveBeenCalledWith( - 'https://api.example.test/v1/provider/assignments/assignment-1', - expect.objectContaining({ headers: expect.objectContaining({ authorization: 'Bearer provider-key' }) }), - ); - }); - -it('creates model-aware content through TreeDX proxy calls', async () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.questions.create'], - projectId: 'project-1', - assignmentId: 'assignment-1', - workspaceMode: 'workspace_write', - treedxProxyHandle: { - id: 'handle-1', - teamId: 'team-1', - projectId: 'project-1', - assignmentId: 'assignment-1', - repositoryId: 'repo-1', - workspaceId: 'workspace-1', - baseCommitSha: 'abc123', baseRef: 'refs/heads/main', - allowedOperations: ['files:read', 'files:search', 'files:write'], - allowedPaths: ['src/content/**'], - }, - permissionProjection: { - read: { models: ['question'], actions: ['query', 'read'] }, - write: { models: ['question'], actions: ['create', 'validate'] }, - commit: { allowed: false }, - }, - allowedPaths: ['src/content/**'], - forbiddenPaths: [], - }); - const calls: Array<{ url: string; init: RequestInit }> = []; - const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { - calls.push({ url: String(url), init: init ?? {} }); - if (String(url).includes('/v1/provider/assignments/')) return new Response(JSON.stringify(assignmentStatusEnvelope()), { status: 200 }); - if (String(url).includes('/search')) return Response.json({ ok: true, payload: { results: [{ path: 'src/content/objectives/objective-1.mdx' }] } }); - if (init?.method === 'GET') { - if (String(url).includes('src%2Fcontent%2Fquestions%2F')) return new Response(JSON.stringify({ ok:false,code:'not_found' }), { status: 404 }); - if (String(url).includes('src%2Fcontent%2Fobjectives%2F')) return Response.json({ ok: true, payload: { content: '---\nid: objective-1\ntitle: Objective one\nstatus: live\n---\nObjective.' } }); - return new Response(JSON.stringify({ content: '---\nid: question:how-should-content-tools-work\ntitle: How should content tools work?\nquestion_type: implementation\nrelated_objectives: [objective-1]\n---\n\nCreate canonical content.\n' }), { status: 200 }); - } - return new Response(JSON.stringify({ ok: true }), { status: 200 }); - }) as unknown as typeof fetch; - const telemetry: Array> = []; - - await expect(callAgentToolWithTelemetry({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', - descriptors: catalog.descriptors, - fetchImpl, - onTelemetry: (entry) => telemetry.push(entry as unknown as Record), - }, 'treeseed.questions.create', { - title: 'How should content tools work?', - fields: { questionType: 'implementation', relatedObjectives: ['objective-1'] }, - body: 'Create canonical content.', - })).resolves.toMatchObject({ - ok: true, - action: 'create', - refs: [{ model: 'question', collection: 'questions', slug: 'how-should-content-tools-work' }], - changedPaths: ['src/content/questions/how-should-content-tools-work.mdx'], - }); - expect(fetchImpl).toHaveBeenCalled(); - const contentWrite = calls.find((call) => typeof call.init.body === 'string' && call.init.body.includes('question_type')); - expect(String(contentWrite?.init.body)).toContain('question_type'); - expect(String(contentWrite?.init.body)).not.toContain('provider-key'); - expect(telemetry.at(-1)).toMatchObject({ - derivedEvents: [{ - type: 'content_created', - contentRef: { - model: 'question', - path: 'src/content/questions/how-should-content-tools-work.mdx', - subjectId: 'objective-1', - subjectField: 'related_objectives', - }, - }], - }); - }); - -it('places model-aware content beneath the assignment package content root', async () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.questions.create'], - projectId: 'project-1', - assignmentId: 'assignment-1', - workspaceMode: 'workspace_write', - treedxProxyHandle: { - id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', - repositoryId: 'repo-1', workspaceId: 'workspace-1', - baseCommitSha: 'abc123', baseRef: 'refs/heads/main', - allowedOperations: ['files:read', 'files:write'], - allowedPaths: ['docs/src/content/**'], - allowedWritePaths: ['docs/src/content', 'docs/src/content/**'], - }, - permissionProjection: { - read: { models: ['question'], actions: ['read'] }, - write: { models: ['question'], actions: ['create'] }, - commit: { allowed: false }, - }, - allowedPaths: ['docs/src/content/**'], - forbiddenPaths: [], - }); - const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => - new Response(JSON.stringify(String(url).includes('/v1/provider/assignments/') ? assignmentStatusEnvelope() : init?.method === 'GET' ? { content: '' } : { ok: true }), { status: 200 })) as unknown as typeof fetch; - const result = await callAgentTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', descriptors: catalog.descriptors, fetchImpl, - }, 'treeseed.questions.create', { title: 'Package scoped question' }); - expect(result).toMatchObject({ - ok: true, - changedPaths: ['docs/src/content/questions/package-scoped-question.mdx'], - }); - }); - -it('reads legacy Markdown content while returning the authoritative TreeDX path', async () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.content.read'], - projectId: 'project-1', - assignmentId: 'assignment-1', - workspaceMode: 'workspace_write', - treedxProxyHandle: { - id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', - repositoryId: 'repo-1', workspaceId: 'workspace-1', - allowedOperations: ['files:read'], - allowedPaths: ['docs/src/content/**'], - allowedWritePaths: ['docs/src/content', 'docs/src/content/**'], - }, - permissionProjection: { - read: { models: ['objective'], actions: ['read'] }, - write: { models: [], actions: [] }, - commit: { allowed: false }, - }, - allowedPaths: ['docs/src/content/**'], - forbiddenPaths: [], - }); - const requestedPaths: string[] = []; - const fetchImpl = vi.fn(async (url: string | URL | Request) => { - if (String(url).includes('/v1/provider/assignments/')) return new Response(JSON.stringify(assignmentStatusEnvelope()), { status: 200 }); - const path = new URL(String(url)).searchParams.get('path') ?? ''; - requestedPaths.push(path); - if (path.endsWith('.mdx')) { - return new Response(JSON.stringify({ code: 'not_found' }), { status: 404 }); - } - return new Response(JSON.stringify({ content: '---\nid: objective:core\n---\n' }), { status: 200 }); - }) as unknown as typeof fetch; - const result = await callAgentTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', descriptors: catalog.descriptors, fetchImpl, - }, 'treeseed.content.read', { model: 'objective', id: 'core' }); - expect(result).toMatchObject({ - ok: true, - refs: [{ model: 'objective', path: 'docs/src/content/objectives/core.md' }], - }); - expect(requestedPaths).toEqual([ - 'docs/src/content/objectives/core.mdx', - 'docs/src/content/objectives/core.md', - ]); - }); -}); diff --git a/tests/integration/agents/communication/required-response-content-checkpoint.test.ts b/tests/integration/agents/communication/required-response-content-checkpoint.test.ts deleted file mode 100644 index e6fe9293..00000000 --- a/tests/integration/agents/communication/required-response-content-checkpoint.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { mkdtempSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -import { callAgentTool } from '../../../../src/agents/tools/agent-tool-runtime.ts'; -import { callAgentToolWithTelemetry,deriveToolEvents } from '../../../../src/agents/tools/agent-tool-telemetry.ts'; -import { treeDxContentReceipts } from '../../../../src/agents/adapters/codex/execution-codex-receipts.ts'; -import { createAssignmentToolCatalog } from '../../../../src/provider/commerce/catalog/assignment-tool-catalog.ts'; - -const plan = `--- -id: assignment-1 -title: assignment plan for assignment-1 -status: active -teamId: team-1 -projectId: project-1 -workdayId: workday-1 -assignmentId: assignment-1 -createdAt: 2026-08-15T00:00:00.000Z -updatedAt: 2026-08-15T00:00:00.000Z -revision: 1 -objective: Answer the exact discussion message. -completed: [] -remaining: [] -risks: [] ---- -`; - -const summary = `--- -id: assignment-1 -title: assignment summary for assignment-1 -status: suspended -teamId: team-1 -projectId: project-1 -workdayId: workday-1 -assignmentId: assignment-1 -createdAt: 2026-08-15T00:00:00.000Z -updatedAt: 2026-08-15T00:00:00.000Z -summary: Waiting for a required human response. -lessons: [] -blockers: - - Waiting for a required discussion response. -performance: - outcome: suspended - metrics: {} -resumeState: - checkpoint: pending-required-response-checkpoint - nextAction: Resume after the governed response. - contextRefs: [] -artifactRefs: [] -verificationRefs: [] ---- -`; - -function status(stateVersion: number, phase: 'execution' | 'closeout') { - return { ok: true, payload: { - id: 'assignment-1', teamId: 'team-1', projectId: 'project-1', workDayId: 'workday-1', - stateVersion, status: 'leased', leaseState: 'leased', assignedAt: '2026-08-15T00:00:00.000Z', - capacityEnvelope: { reservedSeconds: 900, budget: { time: { hardDeadlineAt: '2099-01-01T00:00:00.000Z' } } }, - decisionInput: { input: { activityType: 'chat' } }, - metadata: { workdayRunId: 'conversation-1', operationalState: phase === 'closeout' ? 'closeout' : 'executing' }, - } }; -} - -function telemetryPath(entries:Record[]){ - const path=join(mkdtempSync(join(tmpdir(),'chat-response-')),'telemetry.jsonl'); - writeFileSync(path,entries.map((entry)=>JSON.stringify(entry)).join('\n')+'\n','utf8'); - return path; -} - -function completedOperational(toolId:string,inputSummary:Record){ - return {status:'completed',toolId,inputSummary}; -} - -describe('required-response communication closeout', () => { - it('blocks a Chat workspace commit until terminal status and summary telemetry exist',async()=>{ - const catalog=createAssignmentToolCatalog({agentTools:['treeseed.content.commit'],projectId:'project-1',assignmentId:'assignment-1',workspaceMode:'workspace_write',contentRoot:'src/content',treedxProxyHandle:{id:'handle-1',teamId:'team-1',projectId:'project-1',assignmentId:'assignment-1',repositoryId:'repo-1',workspaceId:'workspace-1',status:'active',allowedOperations:['files:read','files:write','git:commit'],allowedPaths:['src/content/**'],allowedWritePaths:['src/content/**']},permissionProjection:{read:{models:['discussion_message'],actions:['read']},write:{models:['discussion_message'],actions:['create']},commit:{allowed:true}}}); - const fetchImpl=vi.fn(); - const result=await callAgentToolWithTelemetry({apiBaseUrl:'https://api.example.test',providerAccessToken:'provider-token',assignmentId:'assignment-1',descriptors:catalog.descriptors,fetchImpl,telemetryPath:join(mkdtempSync(join(tmpdir(),'chat-closeout-')),'telemetry.jsonl')},'treeseed.content.commit',{message:'Premature Chat checkpoint'}); - expect(result).toMatchObject({ok:false,code:'content_completion_required_before_commit',metadata:{missingReceipts:['assignment_terminal_status','assignment_summary']}}); - expect(fetchImpl).not.toHaveBeenCalled(); - }); - - it('commits TreeDX operational state and sends the fresh assignment version before suspension', async () => { - let stateVersion = 2; - let responseBody: Record = {}; - const fetchImpl = vi.fn(async (request: string | URL | Request, init?: RequestInit) => { - const url = String(request); - if (url.endsWith('/closeout-start')) { - stateVersion = 3; - return new Response(JSON.stringify({ ok: true, payload: status(3, 'closeout').payload })); - } - if (url.endsWith('/discussions/responses')) { - responseBody = JSON.parse(String(init?.body)); - return new Response(JSON.stringify({ ok: true, payload: { - message: { id: 'message-agent', path: 'src/content/discussion-messages/discussion-1/message-agent.mdx' }, - changeset: { resultCommitSha: 'response-commit' }, suspended: true, - } }), { status: 201 }); - } - if (/\/v1\/provider\/assignments\/assignment-1$/u.test(url)) { - return new Response(JSON.stringify(status(stateVersion, stateVersion === 3 ? 'closeout' : 'execution'))); - } - if (url.includes('/files?path=')) { - const content = decodeURIComponent(url).includes('/assignment-summaries/') ? summary : plan; - return new Response(JSON.stringify({ ok: true, payload: { content } })); - } - if (url.endsWith('/changesets')) return new Response(JSON.stringify({ ok: true, payload: { status: 'applied', refs: [] } })); - if (url.endsWith('/commit')) return new Response(JSON.stringify({ ok: true, payload: { - status: 'committed', commitSha: 'operational-commit', branchName: 'refs/heads/assignment-1', - } })); - return new Response(JSON.stringify({ ok: true, payload: {} })); - }) as unknown as typeof fetch; - - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.content.commit'], projectId: 'project-1', assignmentId: 'assignment-1', - workspaceMode: 'workspace_write', contentRoot: 'src/content', - treedxProxyHandle: { - id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', - repositoryId: 'repo-1', workspaceId: 'workspace-1', status: 'active', - baseCommitSha: 'base-commit', baseRef: 'refs/heads/staging', - allowedOperations: ['files:read', 'files:write', 'workspace:write', 'git:commit'], - allowedPaths: ['src/content/**'], allowedReadPaths: ['src/content/**'], allowedWritePaths: ['src/content/**'], - }, - permissionProjection: { - read: { models: ['discussion_message'], actions: ['read'] }, - write: { models: ['discussion_message'], actions: ['create'] }, - commit: { allowed: true }, - }, - }); - const result = await callAgentTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-token', - assignmentId: 'assignment-1', leaseToken: 'lease-1', descriptors: catalog.descriptors, fetchImpl, - telemetryPath:telemetryPath([ - completedOperational('treeseed.discussion.read',{}), - completedOperational('treeseed.discussion.follow',{}), - ]), - }, 'treeseed.discussion.respond', { - discussionId: 'discussion-1', replyTo: 'message-human', sourceMessageRefs: ['message-human'], - message: 'Which exact Guide page should I review?', recipients: ['human-1'], requiredResponse: true, - expectedStateVersion: 2, idempotencyKey: 'required-response-1', - }); - - expect(result, JSON.stringify(result)).toMatchObject({ ok: true, suspended: true }); - expect(responseBody).toMatchObject({ - expectedStateVersion: 3, - checkpoint: { kind: 'treedx-content', commitSha: 'operational-commit', branchRef: 'refs/heads/assignment-1' }, - }); - expect(fetchImpl.mock.calls.some(([url]) => String(url).endsWith('/commit'))).toBe(true); - const events=deriveToolEvents('treeseed.discussion.respond',catalog.descriptors.find((entry)=>entry.id==='treeseed.discussion.respond')!,{ - replyTo:'message-human',requiredResponse:true, - },result); - const receipts=treeDxContentReceipts([{ status:'completed',toolId:'treeseed.discussion.respond',derivedEvents:events }]); - expect(receipts).toEqual(expect.arrayContaining([ - expect.objectContaining({ metadata:expect.objectContaining({ contentRef:expect.objectContaining({ model:'assignment_summary',commitSha:'operational-commit' }) }) }), - expect.objectContaining({ metadata:expect.objectContaining({ contentRef:expect.objectContaining({ model:'discussion_message',commitSha:'response-commit' }) }) }), - ])); - }); - - it('checkpoints terminal operational evidence before retrying a normal final response',async()=>{ - let responseAttempts=0;let commitCalls=0; - const fetchImpl=vi.fn(async(request:string|URL|Request)=>{ - const url=String(request); - if(/\/v1\/provider\/assignments\/assignment-1$/u.test(url))return new Response(JSON.stringify(status(4,'closeout'))); - if(url.includes('/files?path='))return new Response(JSON.stringify({ok:true,payload:{content:url.includes('/assignment-summaries/')?summary:plan}})); - if(url.endsWith('/commit')){commitCalls+=1;return new Response(JSON.stringify({ok:true,payload:{status:'committed',commitSha:'terminal-checkpoint',branchName:'refs/heads/assignment-1'}}));} - if(url.endsWith('/discussions/responses')){ - responseAttempts+=1; - if(responseAttempts===1)return new Response(JSON.stringify({ok:false,code:'assignment_discussion_checkpoint_required',error:'Commit operational evidence first.'}),{status:409}); - return new Response(JSON.stringify({ok:true,payload:{message:{id:'message-final',path:'src/content/discussion-messages/discussion-1/message-final.mdx'},commitSha:'response-commit',changeset:{resultCommitSha:'response-commit'}}}),{status:201}); - } - return new Response(JSON.stringify({ok:true,payload:{}})); - }) as unknown as typeof fetch; - const catalog=createAssignmentToolCatalog({agentTools:['treeseed.content.commit'],projectId:'project-1',assignmentId:'assignment-1',workspaceMode:'workspace_write',contentRoot:'src/content',treedxProxyHandle:{id:'handle-1',teamId:'team-1',projectId:'project-1',assignmentId:'assignment-1',repositoryId:'repo-1',workspaceId:'workspace-1',status:'active',baseCommitSha:'base-commit',baseRef:'refs/heads/staging',allowedOperations:['files:read','files:write','workspace:write','git:commit'],allowedPaths:['src/content/**'],allowedReadPaths:['src/content/**'],allowedWritePaths:['src/content/**']},permissionProjection:{read:{models:['discussion_message'],actions:['read']},write:{models:['discussion_message'],actions:['create']},commit:{allowed:true}}}); - const closeoutTelemetry=telemetryPath([ - completedOperational('treeseed.discussion.read',{}), - completedOperational('treeseed.discussion.follow',{}), - completedOperational('treeseed.assignment_status_update',{status:'completed'}), - completedOperational('treeseed.assignment_summary',{action:'write',status:'completed'}), - ]); - const result=await callAgentTool({apiBaseUrl:'https://api.example.test',providerAccessToken:'provider-token',assignmentId:'assignment-1',leaseToken:'lease-1',descriptors:catalog.descriptors,fetchImpl,telemetryPath:closeoutTelemetry},'treeseed.discussion.respond',{discussionId:'discussion-1',replyTo:'message-human',sourceMessageRefs:['message-human'],message:'Final evidence-backed response.',recipients:['human-1'],requiredResponse:false,expectedStateVersion:4,idempotencyKey:'final-response-1'}); - expect(result).toMatchObject({ok:true,finalResponseCheckpoint:{commitSha:'terminal-checkpoint',branchRef:'refs/heads/assignment-1'}}); - expect(responseAttempts).toBe(2);expect(commitCalls).toBe(1); - const events=deriveToolEvents('treeseed.discussion.respond',catalog.descriptors.find((entry)=>entry.id==='treeseed.discussion.respond')!,{replyTo:'message-human'},result); - expect(events).toEqual(expect.arrayContaining([expect.objectContaining({type:'content_committed',commitSha:'terminal-checkpoint'}),expect.objectContaining({type:'content_created',contentRef:expect.objectContaining({model:'discussion_message',commitSha:'response-commit'})})])); - }); - - it('refuses a final response before terminal status telemetry exists',async()=>{ - const fetchImpl=vi.fn(async(request:string|URL|Request)=>{ - const url=String(request); - if(/\/v1\/provider\/assignments\/assignment-1$/u.test(url))return new Response(JSON.stringify(status(4,'closeout'))); - if(url.includes('/files?path='))return new Response(JSON.stringify({ok:true,payload:{content:summary}})); - return new Response(JSON.stringify({ok:true,payload:{}})); - }) as unknown as typeof fetch; - const catalog=createAssignmentToolCatalog({agentTools:['treeseed.content.commit'],projectId:'project-1',assignmentId:'assignment-1',workspaceMode:'workspace_write',contentRoot:'src/content',treedxProxyHandle:{id:'handle-1',teamId:'team-1',projectId:'project-1',assignmentId:'assignment-1',repositoryId:'repo-1',workspaceId:'workspace-1',status:'active',allowedOperations:['files:read','files:write','workspace:write','git:commit'],allowedPaths:['src/content/**'],allowedReadPaths:['src/content/**'],allowedWritePaths:['src/content/**']},permissionProjection:{read:{models:['discussion_message'],actions:['read']},write:{models:['discussion_message'],actions:['create']},commit:{allowed:true}}}); - const closeoutTelemetry=telemetryPath([ - completedOperational('treeseed.discussion.read',{}), - completedOperational('treeseed.discussion.follow',{}), - completedOperational('treeseed.assignment_status_update',{status:'working'}), - completedOperational('treeseed.assignment_summary',{action:'write',status:'completed'}), - ]); - const result=await callAgentTool({apiBaseUrl:'https://api.example.test',providerAccessToken:'provider-token',assignmentId:'assignment-1',leaseToken:'lease-1',descriptors:catalog.descriptors,fetchImpl,telemetryPath:closeoutTelemetry},'treeseed.discussion.respond',{discussionId:'discussion-1',replyTo:'message-human',sourceMessageRefs:['message-human'],message:'Premature final response.',recipients:['human-1'],requiredResponse:false,expectedStateVersion:4,idempotencyKey:'final-response-premature'}); - expect(result).toMatchObject({ok:false,code:'assignment_final_closeout_required',metadata:{missingReceipts:['assignment_terminal_status']}}); - expect(fetchImpl.mock.calls.some(([url])=>String(url).endsWith('/discussions/responses'))).toBe(false); - expect(fetchImpl.mock.calls.some(([url])=>String(url).endsWith('/commit'))).toBe(false); - }); - - it('refuses a response until both bounded Discussion reads are complete',async()=>{ - const fetchImpl=vi.fn(async(request:string|URL|Request)=>{ - if(/\/v1\/provider\/assignments\/assignment-1$/u.test(String(request)))return new Response(JSON.stringify(status(2,'execution'))); - return new Response(JSON.stringify({ok:true,payload:{}})); - }) as unknown as typeof fetch; - const catalog=createAssignmentToolCatalog({agentTools:['treeseed.content.commit'],projectId:'project-1',assignmentId:'assignment-1',workspaceMode:'workspace_write',contentRoot:'src/content',treedxProxyHandle:{id:'handle-1',teamId:'team-1',projectId:'project-1',assignmentId:'assignment-1',repositoryId:'repo-1',workspaceId:'workspace-1',status:'active',allowedOperations:['files:read','files:write','workspace:write','git:commit'],allowedPaths:['src/content/**'],allowedReadPaths:['src/content/**'],allowedWritePaths:['src/content/**']},permissionProjection:{read:{models:['discussion_message'],actions:['read']},write:{models:['discussion_message'],actions:['create']},commit:{allowed:true}}}); - const result=await callAgentTool({apiBaseUrl:'https://api.example.test',providerAccessToken:'provider-token',assignmentId:'assignment-1',leaseToken:'lease-1',descriptors:catalog.descriptors,fetchImpl,telemetryPath:telemetryPath([completedOperational('treeseed.discussion.read',{})])},'treeseed.discussion.respond',{discussionId:'discussion-1',replyTo:'message-human',sourceMessageRefs:['message-human'],message:'Response without follow.',recipients:['human-1'],requiredResponse:true,expectedStateVersion:2,idempotencyKey:'missing-follow'}); - expect(result).toMatchObject({ok:false,code:'assignment_discussion_context_required',metadata:{missingReceipts:['discussion_follow']}}); - expect(fetchImpl.mock.calls.some(([url])=>String(url).endsWith('/discussions/responses'))).toBe(false); - expect(fetchImpl.mock.calls.some(([url])=>String(url).endsWith('/commit'))).toBe(false); - }); -}); diff --git a/tests/integration/agents/reconciliation/operations-adapter.test.ts b/tests/integration/agents/reconciliation/operations-adapter.test.ts deleted file mode 100644 index 3409e81a..00000000 --- a/tests/integration/agents/reconciliation/operations-adapter.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { SdkOperationsAdapter } from '../../../../src/agents/adapters/operations/operations.ts'; -import type { - AgentOperationGrant, - AgentOperationRequest, -} from '@treeseed/sdk/operations/agent-tools'; - -const request: AgentOperationRequest = { - operation: 'dev', - mode: 'plan', - taskId: 'task-1', - taskKind: 'implementation', - agentSlug: 'engineer', - agentRole: 'engineer', - projectId: 'market', - environment: 'local', - repoRoot: '/repo', - permissionGrantId: 'grant-1', - input: {}, -}; - -const grant: AgentOperationGrant = { - id: 'grant-1', - operations: ['dev', 'verify'], - modes: ['plan', 'read_only', 'mutating'], - agentRoles: ['engineer'], - taskKinds: ['implementation'], - projectIds: ['market'], - environments: ['local'], - allowedPaths: ['src/content/knowledge/**'], -}; - -describe('agent operations adapter', () => { - it('returns waiting for unauthorized requests and does not execute workflow operations', async () => { - const execute = vi.fn(); - const createMessage = vi.fn(async () => ({ payload: {} })); - const adapter = new SdkOperationsAdapter({ execute }); - - const result = await adapter.runOperation({ - request: { ...request, permissionGrantId: undefined }, - grants: [], - sdk: { createMessage } as never, - }); - - expect(result).toMatchObject({ - operation: 'dev', - status: 'waiting', - error: { code: 'operation_permission_required' }, - }); - expect(execute).not.toHaveBeenCalled(); - expect(createMessage).toHaveBeenCalledWith(expect.objectContaining({ - type: 'agent.operation_event', - payload: expect.objectContaining({ operation: 'dev' }), - })); - }); - - it('executes authorized safe operations through workflow dispatch', async () => { - const execute = vi.fn(async () => ({ - operation: 'local.dev', - ok: true, - payload: { planned: true }, - stdout: ['planned dev'], - stderr: [], - })); - const createMessage = vi.fn(async () => ({ payload: {} })); - const adapter = new SdkOperationsAdapter({ execute }); - - const result = await adapter.runOperation({ - request, - grants: [grant], - sdk: { createMessage } as never, - }); - - expect(execute).toHaveBeenCalledWith({ - operationName: 'dev', - input: { - plan: true, - }, - }, expect.objectContaining({ - cwd: '/repo', - transport: 'sdk', - })); - expect(result).toMatchObject({ - operation: 'dev', - status: 'completed', - commandsRun: ['dev'], - metadata: { - workflowResult: { - operation: 'local.dev', - ok: true, - }, - }, - }); - expect(createMessage).toHaveBeenCalledWith(expect.objectContaining({ - type: 'agent.operation_event', - payload: expect.objectContaining({ - operation: 'dev', - result: expect.objectContaining({ status: 'completed' }), - }), - })); - }); - - it('maps verify to test unless explicit lifecycle code later replaces it', async () => { - const execute = vi.fn(async () => ({ - operation: 'local.test', - ok: true, - payload: null, - stdout: [], - stderr: [], - })); - const adapter = new SdkOperationsAdapter({ execute }); - - await adapter.runOperation({ - request: { - ...request, - operation: 'verify', - input: { - commands: ['npm test'], - }, - }, - grants: [{ - ...grant, - operations: ['verify'], - }], - }); - - expect(execute).toHaveBeenCalledWith(expect.objectContaining({ - operationName: 'test', - input: { - plan: true, - commands: ['npm test'], - }, - }), expect.any(Object)); - }); -}); diff --git a/tests/integration/agents/testing/message-chain.test.ts b/tests/integration/agents/testing/message-chain.test.ts deleted file mode 100644 index e0398201..00000000 --- a/tests/integration/agents/testing/message-chain.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { existsSync } from 'node:fs'; -import { describe, expect, it } from 'vitest'; -import { runMessageChainSuite } from '../../../../src/agents/testing/message-chain.ts'; - -describe('transparent message-chain suite', () => { - it('covers no-mutation research-to-knowledge and governed mutation chains with reports', async () => { - const result = await runMessageChainSuite({ - now: new Date('2026-05-19T00:00:00.000Z'), - }); - - expect(result.ok).toBe(true); - expect(result.chains.map((chain) => chain.id)).toEqual([ - 'research-to-knowledge-no-mutation', - 'governed-mutation-approval', - 'forbidden-path-failure', - ]); - expect(result.chains[0]?.mutationsAttempted).toEqual([]); - expect(result.chains[1]?.messagesEmitted).toContain('release_waiting_for_approval'); - expect(result.chains[1]?.store.mutations[0]?.approvalId).toBeTruthy(); - expect(result.chains[2]?.messagesEmitted).toContain('docs_mutation_failed'); - expect(existsSync(result.reportPath)).toBe(true); - expect(existsSync(result.jsonPath)).toBe(true); - }); -}); diff --git a/tests/integration/agents/treedx/repositories/agent-tool-runtime.preserves-commit-provenance-returned-through-the-treedx-proxy-envelope.test.ts b/tests/integration/agents/treedx/repositories/agent-tool-runtime.preserves-commit-provenance-returned-through-the-treedx-proxy-envelope.test.ts deleted file mode 100644 index f65997ff..00000000 --- a/tests/integration/agents/treedx/repositories/agent-tool-runtime.preserves-commit-provenance-returned-through-the-treedx-proxy-envelope.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { appendFileSync, mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; - -import { rm } from 'node:fs/promises'; - -import { tmpdir } from 'node:os'; - -import { join } from 'node:path'; - -import { execFileSync } from 'node:child_process'; - -import { afterEach, describe, expect, it, vi } from 'vitest'; - -import { - callAgentTool, -} from '../../../../../src/agents/tools/agent-tool-runtime.ts'; - -import { callAgentToolWithTelemetry } from '../../../../../src/agents/tools/agent-tool-telemetry.ts'; - -import { hasAssignmentPlan } from '../../../../../src/agents/tools/assignment/operational-content-tool.ts'; - -import type { ExecutionProviderToolDescriptor } from '../../../../../src/agents/runtime/runtime-types.ts'; - -import { createAssignmentToolCatalog } from '../../../../../src/provider/commerce/catalog/assignment-tool-catalog.ts'; - -import { AgentSdk } from '@treeseed/sdk/sdk'; - -const tempRoots: string[] = []; - -function assignmentStatusEnvelope() { - return { ok: true, payload: { - id: 'assignment-1', teamId: 'team-1', projectId: 'project-1', workDayId: 'workday-1', - stateVersion: 2, status: 'leased', leaseState: 'leased', assignedAt: '2026-08-14T00:00:00.000Z', - capacityEnvelope: { reservedSeconds: 900, budget: { time: { hardDeadlineAt: '2099-01-01T00:00:00.000Z' } } }, - decisionInput: { input: { activityType: 'planning' } }, metadata: { workdayRunId: 'run-1' }, - } }; -} - -const planContent = '---\nschemaVersion: treeseed.assignment-plan/v1\nid: assignment-1\nassignmentId: assignment-1\nstatus: ready\nrevision: 1\nobjective: Complete the assignment.\ncompleted: []\nremaining: []\nrisks: []\ncreatedAt: 2026-08-14T00:00:00.000Z\nupdatedAt: 2026-08-14T00:00:00.000Z\nteamId: team-1\nprojectId: project-1\n---\n'; - -function assignmentRuntimeFetch(finalPayload: Record) { - return vi.fn(async (url: string | URL | Request, init?: RequestInit) => { - if (String(url).includes('/v1/provider/assignments/')) return new Response(JSON.stringify(assignmentStatusEnvelope()), { status: 200 }); - if (init?.method === 'GET') return new Response(JSON.stringify({ content: planContent }), { status: 200 }); - return new Response(JSON.stringify(finalPayload), { status: 200 }); - }) as unknown as typeof fetch; -} - -function statusDescriptor(overrides: Partial = {}): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id: 'treeseed.status', - name: 'TreeSeed status', - description: 'Status', - inputSchema: { type: 'object', properties: {}, additionalProperties: false }, - outputSchema: { type: 'object', additionalProperties: true }, - executionTarget: 'sdk_dispatch', - mutability: 'read', - metadata: { - assignmentId: 'assignment-1', - projectId: 'project-1', - dispatchPreferredMode: 'auto', - telemetryCategory: 'treeseed', - }, - ...overrides, - }; -} - -function changedPathsDescriptor(worktreeRoot: string): ExecutionProviderToolDescriptor { - return { - kind: 'agent_tool', - id: 'treeseed.changed_paths', - name: 'Changed paths', - description: 'Changed paths', - inputSchema: { - type: 'object', - properties: { includeDiffSummary: { type: 'boolean' } }, - additionalProperties: false, - }, - outputSchema: { type: 'object', additionalProperties: true }, - executionTarget: 'provider_runner', - mutability: 'read', - metadata: { - assignmentId: 'assignment-1', - projectId: 'project-1', - worktreeRoot, - allowedPaths: ['src/content/**'], - forbiddenPaths: ['src/content/private/**'], - telemetryCategory: 'repository', - }, - }; -} -describe('agent tool runtime', () => { -afterEach(async () => { - vi.restoreAllMocks(); - await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); - }); - -it('preserves commit provenance returned through the TreeDX proxy envelope', async () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.content.commit'], - projectId: 'project-1', - assignmentId: 'assignment-1', - workspaceMode: 'workspace_write', - treedxProxyHandle: { - id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', - repositoryId: 'repo-1', workspaceId: 'workspace-1', - status: 'active', - allowedOperations: ['files:read', 'files:write', 'git:commit'], - allowedPaths: ['docs/src/content/**'], - allowedWritePaths: ['docs/src/content', 'docs/src/content/**'], - }, - permissionProjection: { - read: { models: ['note'], actions: ['read'] }, - write: { models: ['note'], actions: ['create'] }, - commit: { allowed: true }, - }, - allowedPaths: ['docs/src/content/**'], - forbiddenPaths: [], - }); - const fetchImpl = assignmentRuntimeFetch({ - ok: true, - payload: { - ok: true, - status: 'committed', - commitSha: 'abc123', - branchName: 'refs/heads/agent-work', - }, - }); - const telemetry: Array> = []; - - const commitResult = await callAgentToolWithTelemetry({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', - descriptors: catalog.descriptors, - fetchImpl, - onTelemetry: (entry) => telemetry.push(entry as unknown as Record), - }, 'treeseed.content.commit', { message: 'Persist planning note' }); - expect(commitResult, JSON.stringify(commitResult)).toMatchObject({ ok: true }); - expect(telemetry.at(-1)).toMatchObject({ - derivedEvents: [{ - type: 'content_committed', - commitSha: 'abc123', - branchRef: 'refs/heads/agent-work', - }], - }); - }); - -it('blocks workspace finalization until the required linked content artifact exists', async () => { - const root = mkdtempSync(join(tmpdir(), 'treeseed-agent-precommit-')); - tempRoots.push(root); - const telemetryPath = join(root, 'events.jsonl'); - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.content.commit'], - projectId: 'project-1', - assignmentId: 'assignment-1', - workspaceMode: 'workspace_write', - treedxProxyHandle: { - id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', - repositoryId: 'repo-1', workspaceId: 'workspace-1', status: 'active', - allowedOperations: ['files:read', 'files:write', 'git:commit'], - allowedPaths: ['docs/**'], allowedWritePaths: ['docs', 'docs/**'], - }, - permissionProjection: { - read: { models: ['note'], actions: ['read'] }, - write: { models: ['note'], actions: ['create', 'link'] }, - commit: { allowed: true }, - }, - contentRoot: 'docs/src/content', - allowedPaths: ['docs/**'], forbiddenPaths: [], - }); - const commit = catalog.descriptors.find((entry) => entry.id === 'treeseed.content.commit')!; - const descriptor = { - ...commit, - metadata: { - ...(commit.metadata ?? {}), - requiredArtifactKind: 'documentation_update', - requireContentArtifact: true, - }, - }; - const assignmentPlan = catalog.descriptors.find((entry) => entry.id === 'treeseed.assignment_plan')!; - expect(assignmentPlan).toBeDefined(); - const fetchImpl = assignmentRuntimeFetch({ ok: true, payload: { - status: 'committed', commitSha: 'abc123', branchName: 'refs/heads/agent-work', - } }); - writeFileSync(telemetryPath, `${JSON.stringify({ - status: 'completed', - derivedEvents: [{ type: 'content_created', contentRef: { model: 'note', path: 'docs/src/content/notes/release.md' } }], - })}\n${JSON.stringify({ toolId: 'treeseed.assignment_plan', status: 'completed', inputSummary: { action: 'write' }, derivedEvents: [] })}\n${JSON.stringify({ toolId: 'treeseed.assignment_status_update', status: 'completed', inputSummary: { status: 'completed' }, derivedEvents: [] })}\n${JSON.stringify({ toolId: 'treeseed.assignment_summary', status: 'completed', inputSummary: { action: 'write', status: 'completed' }, derivedEvents: [] })}\n`, 'utf8'); - - await expect(callAgentToolWithTelemetry({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', descriptors: [descriptor, assignmentPlan], fetchImpl, telemetryPath, - }, 'treeseed.content.commit', { message: 'Finalize documentation' })).resolves.toMatchObject({ - ok: false, - code: 'content_completion_required_before_commit', - metadata: { missingReceipts: [ - 'content_subject_linked:docs/src/content/notes/release.md', - ] }, - }); - expect(fetchImpl).not.toHaveBeenCalled(); - - appendFileSync(telemetryPath, `${JSON.stringify({ - status: 'completed', - derivedEvents: [{ type: 'content_updated', contentRef: { - model: 'note', path: 'docs/src/content/notes/release.md', - subjectId: 'proposal:release-channel-normalization', subjectField: 'about', - } }], - })}\n`, 'utf8'); - await expect(hasAssignmentPlan({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', descriptors: [descriptor, assignmentPlan], fetchImpl, - })).resolves.toBe(true); - const committed = await callAgentToolWithTelemetry({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', descriptors: [descriptor, assignmentPlan], fetchImpl, telemetryPath, - }, 'treeseed.content.commit', { message: 'Finalize documentation' }); - expect(committed, JSON.stringify(committed)).toMatchObject({ ok: true }); - expect(fetchImpl.mock.calls.filter(([url, init]) => String(url).includes('/commit') && init?.method === 'POST')).toHaveLength(1); - }); - -it('runs bounded verification in the assignment worktree and accepts an expected failure', async () => { - const root = mkdtempSync(join(tmpdir(), 'treeseed-agent-verify-')); - tempRoots.push(root); - mkdirSync(join(root, 'template'), { recursive: true }); - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.verify'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - worktreeRoot: root, - allowedPaths: ['template/tests/**'], - }); - const descriptor = catalog.descriptors.find((entry) => entry.id === 'treeseed.verify'); - expect(descriptor).toBeDefined(); - await expect(callAgentTool({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', - descriptors: [descriptor!], - fetchImpl: assignmentRuntimeFetch({ ok: true }), - }, 'treeseed.verify', { - commands: [{ - command: 'node', - args: ['-e', 'process.exit(3)'], - cwd: 'template', - expectedExitCode: 3, - }], - })).resolves.toMatchObject({ - ok: true, - payload: { - results: [expect.objectContaining({ command: 'node', cwd: 'template', exitCode: 3, expectedExitCode: 3, ok: true })], - }, - }); - await expect(callAgentTool({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: '', - assignmentId: 'assignment-1', - descriptors: [descriptor!], - fetchImpl: assignmentRuntimeFetch({ ok: true }), - }, 'treeseed.verify', { - commands: [{ command: 'node', args: ['--version'], cwd: '../outside' }], - })).resolves.toMatchObject({ ok: false, code: 'verification_cwd_invalid' }); - }); - -it('emits structured verification evidence for the artifact manifest', async () => { - const root = mkdtempSync(join(tmpdir(), 'treeseed-agent-verify-telemetry-')); - tempRoots.push(root); - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.verify'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - worktreeRoot: root, - }); - const telemetry: Array> = []; - await expect(callAgentToolWithTelemetry({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: '', - assignmentId: 'assignment-1', - descriptors: catalog.descriptors, - fetchImpl: assignmentRuntimeFetch({ ok: true }), - onTelemetry: (entry) => telemetry.push(entry as unknown as Record), - }, 'treeseed.verify', { - commands: [{ command: 'node', args: ['-e', 'process.exit(3)'], expectedExitCode: 3 }], - })).resolves.toMatchObject({ ok: true }); - expect(telemetry.at(-1)).toMatchObject({ - status: 'completed', - derivedEvents: [{ - type: 'verification_completed', - status: 'passed', - commands: ['node -e process.exit(3) (cwd: .)'], - }], - }); - }); - -it('records an explicit governed review disposition event', async () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.review_decision'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: { - id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', - repositoryId: 'repo-1', workspaceId: 'workspace-1', status: 'active', - allowedOperations: ['files:read', 'files:write'], allowedPaths: ['src/content/**'], - }, - workspaceMode: 'workspace_write', - }); - const telemetry: Array> = []; - expect(catalog.exposed).toContain('treeseed.assignment_plan'); - const planRead = await callAgentTool({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', descriptors: catalog.descriptors, - fetchImpl: assignmentRuntimeFetch({ ok: true }), - }, 'treeseed.assignment_plan', { action: 'read' }); - expect(planRead, JSON.stringify(planRead)).toMatchObject({ ok: true }); - await expect(hasAssignmentPlan({ - apiBaseUrl: 'https://api.example.test', providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', descriptors: catalog.descriptors, - fetchImpl: assignmentRuntimeFetch({ ok: true }), - })).resolves.toBe(true); - const reviewResult = await callAgentToolWithTelemetry({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: 'provider-key', - assignmentId: 'assignment-1', - descriptors: catalog.descriptors, - fetchImpl: assignmentRuntimeFetch({ ok: true }), - onTelemetry: (entry) => telemetry.push(entry as unknown as Record), - }, 'treeseed.review_decision', { - disposition: 'rejected', - summary: 'The implementation needs a bounded recovery correction.', - }); - expect(reviewResult, JSON.stringify(reviewResult)).toMatchObject({ - ok: true, - payload: { disposition: 'rejected' }, - }); - expect(telemetry.at(-1)).toMatchObject({ - status: 'completed', - derivedEvents: [{ - type: 'review_decision_recorded', - disposition: 'rejected', - }], - }); - }); - -it('enforces changed path scope', async () => { - const root = mkdtempSync(join(tmpdir(), 'treeseed-agent-tool-runtime-')); - tempRoots.push(root); - execFileSync('git', ['init', '-b', 'main'], { cwd: root }); - execFileSync('git', ['config', 'user.email', 'agent-tool@example.test'], { cwd: root }); - execFileSync('git', ['config', 'user.name', 'Agent Tool'], { cwd: root }); - mkdirSync(join(root, 'src/content/private'), { recursive: true }); - writeFileSync(join(root, 'src/content/private/secret.mdx'), 'secret\n', 'utf8'); - execFileSync('git', ['add', 'src/content/private/secret.mdx'], { cwd: root }); - const result = await callAgentTool({ - apiBaseUrl: 'https://api.example.test', - providerAccessToken: '', - assignmentId: 'assignment-1', - descriptors: [changedPathsDescriptor(root)], - fetchImpl: assignmentRuntimeFetch({ ok: true }), - }, 'treeseed.changed_paths'); - expect(result).toMatchObject({ ok: false, code: 'path_forbidden' }); - }); -}); diff --git a/tests/integration/provider/accounts/manager-session-recovery.test.ts b/tests/integration/provider/accounts/manager-session-recovery.test.ts deleted file mode 100644 index fb17d9f8..00000000 --- a/tests/integration/provider/accounts/manager-session-recovery.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { resolveProviderConfig, type ProviderConnectionRuntimeContext } from '../../../../src/provider/configuration/config.ts'; -import { runManagerSkeleton } from '../../../../src/provider/lifecycle/lifecycle.ts'; -import { ProviderLocalCapacityStore } from '../../../../src/provider/capacity/capacity-core/local-capacity-store.ts'; - -afterEach(() => vi.unstubAllGlobals()); - -describe('provider manager availability recovery', () => { - it('refreshes the durable membership session sequence after process recreation', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-session-recovery-')); - try { - const host = resolveProviderConfig({ env: { TREESEED_PROVIDER_DATA_DIR: root, TREESEED_PROVIDER_SOURCE_CLOSURE_DIGEST: 'closure-a', HOME: root } }); - const config: ProviderConnectionRuntimeContext = { ...host, connectionId: 'team-a', marketUrl: 'https://team-a.test', marketAudience: 'https://team-a.test', teamId: 'team-a', providerId: 'provider-a', membershipId: 'membership-a', accessToken: 'token-a', defaultExecutionProviderId: 'codex-sub' }; - const calls: Array<{ url: string; method: string; body: Record }> = []; - vi.stubGlobal('fetch', async (request: RequestInfo | URL, init?: RequestInit) => { - const body = JSON.parse(String(init?.body ?? '{}')) as Record; - calls.push({ url: String(request), method: init?.method ?? 'GET', body }); - const sequence = init?.method === 'PUT' ? 2 : 1; - return new Response(JSON.stringify({ ok: true, payload: { id: 'session-a', membershipId: 'membership-a', teamId: 'team-a', providerId: 'provider-a', status: 'open', sequence, snapshot: { sequence, availableFrom: new Date().toISOString(), pressure: 'idle', maxConcurrentAssignments: 1, activeAssignmentIds: [], executionProviders: [], capabilities: ['research'] }, openedAt: new Date().toISOString(), refreshedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString() } }), { status: init?.method === 'PUT' ? 200 : 201, headers: { 'content-type': 'application/json' } }); - }); - const availability = { offer: { capabilities: ['research'], maxConcurrentRunners: 1 }, executionProviders: [{ id: 'codex-sub', adapter: 'codex', nativeLimits: {}, capabilities: ['research'] }] }; - await runManagerSkeleton(config, { availability, localState: new ProviderLocalCapacityStore(root) }); - await runManagerSkeleton(config, { availability, localState: new ProviderLocalCapacityStore(root) }); - expect(calls.map((call) => call.method)).toEqual(['POST', 'PUT']); - expect(calls[1]?.url).toContain('/v1/provider/availability-sessions/session-a'); - expect(calls[1]?.body.expectedSequence).toBe(1); - expect(calls[0]?.body.executionProviders).toEqual([expect.objectContaining({ id: 'codex-sub', preferred: true })]); - expect(calls[0]?.body.metadata).toMatchObject({ sourceClosureDigest: 'closure-a' }); - expect(calls[1]?.body.metadata).toMatchObject({ sourceClosureDigest: 'closure-a' }); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -}); diff --git a/tests/integration/provider/capacity/assignments/assignment-runtime.fails-closed-when-a-leased-assignment-lacks-canonical-project-context.test.ts b/tests/integration/provider/capacity/assignments/assignment-runtime.fails-closed-when-a-leased-assignment-lacks-canonical-project-context.test.ts deleted file mode 100644 index eb452a0d..00000000 --- a/tests/integration/provider/capacity/assignments/assignment-runtime.fails-closed-when-a-leased-assignment-lacks-canonical-project-context.test.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { execFileSync } from 'node:child_process'; - -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; - -import { tmpdir } from 'node:os'; - -import { join, resolve } from 'node:path'; - -import { afterEach, describe, expect, it, vi } from 'vitest'; - -import { resolveProviderConfig } from '../../../../../src/provider/configuration/config.ts'; - -import { buildProviderPlan, buildProviderRunnerPlan } from '../../../../../src/provider/lifecycle/lifecycle.ts'; - -import { assignmentProjectContext, materializeAssignmentProject } from '../../../../../src/provider/projects/projects-core/project-materialization.ts'; - -import { refreshAssignmentAccessToken, releaseTerminalAssignmentResources, runProviderAssignment } from '../../../../../src/provider/operations/runner.ts'; - -import { createSingleFlightLeaseRenewal, isTerminalProviderAssignmentObservation, reportProviderLeaseRenewalFailure, runProviderRunnerOnce } from '../../../../../src/provider/operations/runner-lifecycle.ts'; - -import { providerAssignmentClientWithTerminalBoundary } from '../../../../../src/provider/coordination/lease-client.ts'; -import { reportProviderAssignmentResult } from '../../../../../src/provider/capacity/assignments/assignment-result-reporter.ts'; - -const roots: string[] = []; - -function temporaryDirectory() { - const path = mkdtempSync(join(tmpdir(), 'treeseed-assignment-runtime-')); - roots.push(path); - return path; -} - -function repository() { - const path = temporaryDirectory(); - mkdirSync(resolve(path, 'src/content/agents'), { recursive: true }); - writeFileSync(resolve(path, 'package.json'), '{"name":"assignment-project"}\n'); - execFileSync('git', ['init', '-b', 'main'], { cwd: path, stdio: 'ignore' }); - return path; -} - -function config(overrides: NodeJS.ProcessEnv = {}) { - return resolveProviderConfig({ env: { - TREESEED_PROVIDER_DATA_DIR: temporaryDirectory(), - TREESEED_PROVIDER_ENVIRONMENT: 'local', - HOME: temporaryDirectory(), - ...overrides, - } }); -} - -function connectionConfig(overrides: NodeJS.ProcessEnv = {}) { - return { - ...config(overrides), - connectionId: 'connection-a', - marketUrl: 'https://market.test', - marketAudience: 'https://market.test', - teamId: 'team-a', - providerId: 'provider-a', - membershipId: 'membership-a', - accessToken: 'short-lived-token', - }; -} - -function projectContext(repoRoot: string) { - return { - id: 'project-a', - slug: 'project-a', - name: 'Project A', - architecture: { rootPath: '.', sitePath: '.', contentPath: 'src/content', localContentMaterialization: 'existing_path' }, - agentSpecs: { root: 'src/content/agents', testsRoot: 'src/content/agent-tests' }, - repository: { provider: 'git', owner: 'local', name: 'project-a', defaultBranch: 'main', cloneUrl: repoRoot, checkoutPath: '.' }, - }; -} - -function leasedAssignment(repoRoot: string) { - return { - id: 'assignment-a', - membershipId: 'membership-a', - stateVersion: 1, - teamId: 'team-a', - projectId: 'project-a', - capacityProviderId: 'provider-a', - providerSessionId: 'session-a', - executionProviderId: null, - laneId: null, - allocationSetId: 'allocation-set-a', - projectAgentClassId: 'researcher', - reservationId: 'reservation-a', - workDayId: 'workday-a', - taskId: 'task-a', - agentId: 'researcher', - handlerId: 'execution-content', - mode: 'planning' as const, - status: 'leased' as const, - leaseState: 'leased' as const, - leaseExpiresAt: new Date(Date.now() + 300_000).toISOString(), - leaseToken: 'lease-a', - leaseRenewedAt: null, - runnerId: 'runner-a', - decisionInput: { teamId: 'team-a', projectId: 'project-a', projectAgentClassId: 'researcher', agentId: 'researcher', handlerId: 'execution-content', mode: 'planning', input: {}, metadata: { demandId: 'demand-a' } }, - capacityEnvelope: { teamId: 'team-a', projectId: 'project-a', projectAgentClassId: 'researcher', capacityProviderId: 'provider-a', workDayId: 'workday-a', mode: 'planning' as const, reservedSeconds: 3, metadata: { demandId: 'demand-a' } }, - workspaceContext: { project: projectContext(repoRoot) }, - allowedOutputs: {}, - explanation: {}, - attemptCount: 1, - assignedAt: new Date().toISOString(), - claimedAt: new Date().toISOString(), - completedAt: null, - returnedAt: null, - failedAt: null, - lifecycleReason: null, - lifecycleCode: null, - lifecycleOutput: {}, - synthesizedFrom: 'workday_demand' as const, - synthesisKey: 'workday-a:agent-a', - decisionId: null, - proposalId: null, - fallbackOutputId: null, - treedxProxyHandle: null, - capabilityHandles: null, - metadata: { demandId: 'demand-a', workdayRunId: 'workday-run-a' }, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; -} - -afterEach(() => { - vi.unstubAllGlobals(); - delete process.env.TREESEED_PROVIDER_TREEDX_REQUEST_TIMEOUT_MS; - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); -}); -describe('assignment-scoped provider runtime', () => { -async function runTerminalLifecycle(status: 'returned' | 'failed', expected: 'return' | 'fail', retryable: boolean) { - const repoRoot = repository(); - const calls: string[] = []; - await runProviderRunnerOnce({ - config: connectionConfig({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }), - leasedAssignment: { ok: true, leaseToken: 'lease-a', payload: leasedAssignment(repoRoot) }, - client: { - async nextAssignment() { throw new Error('runner must not poll'); }, - async createAssignmentModeRun() { return { ok: true, payload: {} }; }, - async settleAssignment() { calls.push('settle'); return { ok: true }; }, - async completeAssignment() { calls.push('complete'); return { ok: true }; }, - async returnAssignment() { calls.push('return'); return { ok: true }; }, - async failAssignment() { calls.push('fail'); return { ok: true }; }, - }, - kernel: { - async runAssignment() { - return { - status, mode: 'planning' as const, assignmentId: 'assignment-a', projectId: 'project-a', projectAgentClassId: 'researcher', summary: status, - selectedInput: {}, capacityEnvelope: leasedAssignment(repoRoot).capacityEnvelope, - fallback: { code: `assignment_${status}`, reason: status, retryable }, - }; - }, - }, - }); - expect(calls).toEqual(status==='returned'?['settle',expected]:[expected]); - } - -it('fails closed when a leased assignment lacks canonical project context', async () => { - const calls: Array<{ method: string; body?: Record }> = []; - const result = await runProviderRunnerOnce({ - config: connectionConfig(), - runnerId: 'runner-a', - leasedAssignment: { ok: true, leaseToken: 'lease-a', payload: { id: 'assignment-a', membershipId: 'membership-a', stateVersion: 1, projectId: 'project-a', agentId: 'researcher', mode: 'planning', decisionInput: { input: {} }, capacityEnvelope: { projectId: 'project-a', mode: 'planning' } } }, - client: { - async nextAssignment() { throw new Error('runner must not poll'); }, - async createAssignmentModeRun(_id, body) { calls.push({ method: 'mode-run', body }); return { ok: true, payload: {} }; }, - async completeAssignment(_id, body) { calls.push({ method: 'complete', body }); return { ok: true, payload: {} }; }, - async failAssignment(_id, body) { calls.push({ method: 'fail', body }); return { ok: true, payload: { status: 'failed' } }; }, - }, - }); - expect(result).toMatchObject({ ok: true, role: 'runner' }); - expect(calls.find((entry) => entry.method === 'fail')?.body).toMatchObject({ code: 'assignment_project_context_missing', retryable: true }); - }); - -it('refreshes provider authority before preparing each assignment tool runtime', async () => { - const config = connectionConfig(); - config.accessToken = 'expired-token'; - config.accessTokenProvider = vi.fn(async () => 'fresh-assignment-token'); - const hardDeadlineAt = new Date(Date.now() + 3 * 60_000).toISOString(); - const authorityDeadlineAt = new Date(Date.now() + 8 * 60_000).toISOString(); - expect(await refreshAssignmentAccessToken(config, { capacityEnvelope:{ budget:{ time:{ hardDeadlineAt,authorityDeadlineAt } } } })).toMatchObject({ - accessToken: 'fresh-assignment-token', - }); - expect(config.accessTokenProvider).toHaveBeenCalledWith(expect.any(Number)); - expect(vi.mocked(config.accessTokenProvider).mock.calls[0]?.[0]).toBeGreaterThan(8 * 60_000); -}); - -it('executes a manager-created durable dispatch without polling for another assignment', async () => { - const nextAssignment = vi.fn(async () => ({ ok: true, payload: null })); - const result = await runProviderRunnerOnce({ - config: connectionConfig(), - runnerId: 'manager-selected-runner', - leasedAssignment: { ok: true, leaseToken: 'lease-a', payload: { id: 'assignment-a', membershipId: 'membership-a', stateVersion: 1, projectId: 'project-a', agentId: 'researcher', mode: 'planning', decisionInput: { input: {} }, capacityEnvelope: { projectId: 'project-a', mode: 'planning' } } }, - client: { - nextAssignment, - async createAssignmentModeRun() { return { ok: true, payload: {} }; }, - async completeAssignment() { return { ok: true, payload: {} }; }, - async failAssignment() { return { ok: true, payload: { status: 'failed' } }; }, - }, - }); - expect(nextAssignment).not.toHaveBeenCalled(); - expect(result).toMatchObject({ ok: true, role: 'runner' }); - }); - -it('describes the canonical lease lifecycle in plan mode', async () => { - expect(buildProviderRunnerPlan(config())).toMatchObject({ - role: 'runner', - mode: 'plan', - flow: [ - 'claim a manager-created durable leased-assignment dispatch', - 'record provider-local mode-run telemetry', - 'complete or fail assignment without widening scope', - ], - }); - }); - -it('renews, settles exactly once, then completes with the forensic artifact manifest', async () => { - const repoRoot = repository(); - const calls: Array<{ method: string; body?: Record; key?: string }> = []; - let renewAttempts = 0; - const artifactManifest = { - schemaVersion: 1 as const, - assignmentId: 'assignment-a', modeRunId: 'mode-run-a', teamId: 'team-a', projectId: 'project-a', providerId: 'provider-a', mode: 'planning' as const, - agentClassId: 'researcher', agentId: 'researcher', handlerId: 'execution-content', activityType: 'research', status: 'completed' as const, - summary: 'done', - toolEvents: [{ id: 'tool-a', toolId: 'treedx.write', status: 'completed' as const, derivedEventTypes: ['content_created'] }], - contentReferences: [{ model: 'note', contentPath: 'notes/research/result.mdx', receiptId: 'receipt-a', toolEventId: 'tool-a', subjectId: 'question-a', subjectField: 'related_questions' }], - verification: [], citations: [], usage: [{ - kind: 'codex', - unit: 'wall_minute', - amount: 1.5, - source: 'codex', - partial: false, - metadata: { wallMinutes: 1.5, inputTokens: 120, outputTokens: 30, cachedInputTokens: 20 }, - }], signals: [], diagnostics: [], createdAt: new Date().toISOString(), - }; - const result = await runProviderRunnerOnce({ - config: connectionConfig({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }), - runnerId: 'runner-a', - leasedAssignment: { ok: true, leaseToken: 'lease-a', leaseSeconds: 300, payload: leasedAssignment(repoRoot) }, - client: { - async nextAssignment() { throw new Error('runner must not poll'); }, - async renewAssignment(_id, body) { - renewAttempts += 1; calls.push({ method: 'renew', body }); - if (renewAttempts === 1) throw new TypeError('fetch failed'); - return { ok: true, payload: {} }; - }, - async createAssignmentModeRun(_id, body) { calls.push({ method: 'mode-run', body }); return { ok: true, payload: {} }; }, - async reportAssignmentUsage(_id, body, key) { calls.push({ method: 'usage', body, key }); return { ok: true, payload: {} }; }, - async settleAssignment(_id, body, key) { calls.push({ method: 'settle', body, key }); return { ok: true, payload: {} }; }, - async preflightAssignmentCompletion(_id, body) { calls.push({ method: 'preflight', body }); return { ok: true, payload: { required: true,receiptDigest:'a'.repeat(64) } }; }, - async completeAssignment(_id, body) { calls.push({ method: 'complete', body }); return { ok: true, payload: {} }; }, - async failAssignment(_id, body) { calls.push({ method: 'fail', body }); return { ok: true, payload: {} }; }, - }, - kernel: { - async runAssignment() { - return { - status: 'completed' as const, mode: 'planning' as const, assignmentId: 'assignment-a', projectId: 'project-a', projectAgentClassId: 'researcher', - agentId: 'researcher', handlerId: 'execution-content', summary: 'done', outputs: { metadata: { usageActual: { activeSeconds: 2 } } }, - selectedInput: {}, capacityEnvelope: leasedAssignment(repoRoot).capacityEnvelope, traceRefs: { agentRunId: 'run-a' }, artifactManifest, - }; - }, - }, - }); - expect(result).toMatchObject({ ok: true, assigned: 1 }); - expect(renewAttempts).toBe(2); - expect(calls.findIndex((entry) => entry.method === 'renew')).toBeGreaterThanOrEqual(0); - expect(calls.findIndex((entry) => entry.method === 'preflight')).toBeLessThan(calls.findIndex((entry) => entry.method === 'usage')); - expect(calls.findIndex((entry) => entry.method === 'usage')).toBeLessThan(calls.findIndex((entry) => entry.method === 'settle')); - expect(calls.findIndex((entry) => entry.method === 'settle')).toBeLessThan(calls.findIndex((entry) => entry.method === 'complete')); - expect(calls.find((entry) => entry.method === 'usage')).toMatchObject({ - key: 'assignment:assignment-a:usage:codex.0', - body: { usageDimension: 'codex.0', accountingMode: 'informational', activeSeconds: 0, elapsedSeconds: 0, providerUnits: 1.5 }, - }); - expect(calls.find((entry) => entry.method === 'usage')?.body).not.toHaveProperty('actualCredits'); - expect(calls.find((entry) => entry.method === 'settle')?.key).toBe('assignment:assignment-a:terminal-settlement'); - expect(calls.find((entry) => entry.method === 'settle')?.body).toMatchObject({ - activeSeconds: 90, - providerUnits: 1.5, - usageActual: { - nativeUsage: { executionUsage: [{ kind: 'codex', amount: 1.5 }] }, - inputTokens: 120, - outputTokens: 30, - cachedInputTokens: 20, - wallMinutes: 1.5, - taskSignature: 'researcher:planning', - }, - }); - expect(calls.find((entry) => entry.method === 'settle')?.body).not.toHaveProperty('actualCredits'); - expect(calls.find((entry) => entry.method === 'complete')?.body).toMatchObject({ metadata:{semanticCompletionPreflightReceiptDigest:'a'.repeat(64)},output: { artifactManifest: { assignmentId: 'assignment-a' } } }); - }); - -it('routes a returned kernel result through the canonical return lifecycle', async () => { - await runTerminalLifecycle('returned', 'return', true); - }); - -it('routes a failed kernel result through the canonical fail lifecycle', async () => { - await runTerminalLifecycle('failed', 'fail', false); - }); - -it('refuses successful terminalization when pre-settlement workspace cleanup remains unavailable', async () => { - const repoRoot = repository(); - const calls: Array<{ method: string; body?: Record }> = []; - const assignment = leasedAssignment(repoRoot); - let cleanupAttempts = 0; - await expect(reportProviderAssignmentResult({ - client: { - async nextAssignment() { throw new Error('not used'); }, - async createAssignmentModeRun() { return { ok: true }; }, - async createAssignmentEvent(_id, body) { calls.push({ method: 'event', body }); return { ok: true }; }, - async reportAssignmentUsage() { return { ok: true }; }, - async settleAssignment() { calls.push({ method: 'settle' }); return { ok: true }; }, - async preflightAssignmentCompletion() { calls.push({ method: 'preflight' }); return { ok: true, payload: { required: true,receiptDigest:'a'.repeat(64) } }; }, - async completeAssignment(_id, body) { calls.push({ method: 'complete', body }); return { ok: true, payload: { status: 'completed' } }; }, - async failAssignment() { throw new Error('successful execution must not fail'); }, - }, - assignment, - assignmentId: assignment.id, - modeResult: { - status: 'completed', mode: 'reviewing', assignmentId: assignment.id, projectId: assignment.projectId, - projectAgentClassId: assignment.projectAgentClassId, agentId: assignment.agentId, handlerId: assignment.handlerId, - summary: 'review completed', outputs: { metadata: { modeRunId: 'mode-run-a', usageActual: { activeSeconds: 1 } } }, - selectedInput: {}, capacityEnvelope: assignment.capacityEnvelope, traceRefs: {}, - }, - capacityEnvelope: assignment.capacityEnvelope, - leaseToken: 'lease-a', runnerId: 'runner-a', projectId: assignment.projectId, agentSlug: assignment.agentId, - fallbackOutput: null, - async closeWorkspace() { cleanupAttempts += 1; throw Object.assign(new TypeError('fetch failed'), { operation: 'workspace.close', path: '/v1/dx/projects/project-a/workspaces/workspace-a/close' }); }, - })).rejects.toMatchObject({ message: 'fetch failed' }); - expect(cleanupAttempts).toBe(3); - expect(calls).toEqual([{method:'preflight'}]); -}); - -it('bounds assignment-scoped TreeDX response bodies before AgentKernel execution', async () => { - const repoRoot = repository(); - const runtimeConfig = connectionConfig({ - TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot, - TREESEED_TREEDX_BASE_URL: 'https://treedx.test', - TREESEED_TREEDX_TOKEN: 'treedx-token', - }); - process.env.TREESEED_PROVIDER_TREEDX_REQUEST_TIMEOUT_MS = '25'; - vi.stubGlobal('fetch', async (_input: RequestInfo | URL, init?: RequestInit) => new Promise((_resolve, reject) => { - init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true }); - })); - const assignment = { - ...leasedAssignment(repoRoot), - metadata: { ...leasedAssignment(repoRoot).metadata, assignmentSource: 'capacity_workday_demand', contentRoot: 'src/content' }, - treedxProxyHandle: { - id: 'handle-a', teamId: 'team-a', projectId: 'project-a', assignmentId: 'assignment-a', repositoryId: 'repo-a', workspaceId: 'workspace-a', - allowedOperations: ['files:read'], allowedPaths: ['**'], expiresAt: new Date(Date.now() + 60_000).toISOString(), - }, - }; - await expect(runProviderAssignment({ - config: runtimeConfig, - client: { - async nextAssignment() { return { ok: true }; }, - async createAssignmentModeRun() { return { ok: true, payload: {} }; }, - async completeAssignment() { return { ok: true }; }, - async failAssignment() { return { ok: true }; }, - }, - assignment, - leaseToken: 'lease-a', - runnerId: 'runner-a', - leaseSeconds: 300, - async renewLease() {}, - treeDx: runtimeConfig.treeDx ?? undefined, - })).rejects.toThrow(/TreeDX proxy request timed out after 25ms/u); - }); -}); diff --git a/tests/integration/provider/capacity/assignments/assignment-runtime.releases-internal-execution-provider-resources-only-after-a-durable-terminal-response.test.ts b/tests/integration/provider/capacity/assignments/assignment-runtime.releases-internal-execution-provider-resources-only-after-a-durable-terminal-response.test.ts deleted file mode 100644 index 33d638f5..00000000 --- a/tests/integration/provider/capacity/assignments/assignment-runtime.releases-internal-execution-provider-resources-only-after-a-durable-terminal-response.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { execFileSync } from 'node:child_process'; - -import { existsSync,mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; - -import { tmpdir } from 'node:os'; - -import { join, resolve } from 'node:path'; - -import { afterEach, describe, expect, it, vi } from 'vitest'; - -import { resolveProviderConfig } from '../../../../../src/provider/configuration/config.ts'; - -import { buildProviderPlan, buildProviderRunnerPlan } from '../../../../../src/provider/lifecycle/lifecycle.ts'; - -import { assignmentProjectContext, materializeAssignmentProject,releaseMaterializedAssignmentProject } from '../../../../../src/provider/projects/projects-core/project-materialization.ts'; - -import { releaseTerminalAssignmentResources, runProviderAssignment } from '../../../../../src/provider/operations/runner.ts'; - -import { createSingleFlightLeaseRenewal, isTerminalProviderAssignmentObservation, providerAssignmentCancellationRequest, reportProviderLeaseRenewalFailure, runProviderRunnerOnce } from '../../../../../src/provider/operations/runner-lifecycle.ts'; - -import { providerAssignmentClientWithTerminalBoundary } from '../../../../../src/provider/coordination/lease-client.ts'; - -const roots: string[] = []; - -function temporaryDirectory() { - const path = mkdtempSync(join(tmpdir(), 'treeseed-assignment-runtime-')); - roots.push(path); - return path; -} - -function repository() { - const path = temporaryDirectory(); - mkdirSync(resolve(path, 'src/content/agents'), { recursive: true }); - writeFileSync(resolve(path, 'package.json'), '{"name":"assignment-project"}\n'); - execFileSync('git', ['init', '-b', 'main'], { cwd: path, stdio: 'ignore' }); - execFileSync('git', ['config', 'user.name', 'TreeSeed Test'], { cwd: path, stdio: 'ignore' }); - execFileSync('git', ['config', 'user.email', 'test@treeseed.local'], { cwd: path, stdio: 'ignore' }); - execFileSync('git', ['add', '-A'], { cwd: path, stdio: 'ignore' }); - execFileSync('git', ['commit', '-m', 'test fixture'], { cwd: path, stdio: 'ignore' }); - return path; -} - -function config(overrides: NodeJS.ProcessEnv = {}) { - return resolveProviderConfig({ env: { - TREESEED_PROVIDER_DATA_DIR: temporaryDirectory(), - TREESEED_PROVIDER_ENVIRONMENT: 'local', - HOME: temporaryDirectory(), - ...overrides, - } }); -} - -function connectionConfig(overrides: NodeJS.ProcessEnv = {}) { - return { - ...config(overrides), - connectionId: 'connection-a', - marketUrl: 'https://market.test', - marketAudience: 'https://market.test', - teamId: 'team-a', - providerId: 'provider-a', - membershipId: 'membership-a', - accessToken: 'short-lived-token', - }; -} - -function projectContext(repoRoot: string) { - return { - id: 'project-a', - slug: 'project-a', - name: 'Project A', - architecture: { rootPath: '.', sitePath: '.', contentPath: 'src/content', localContentMaterialization: 'existing_path' }, - agentSpecs: { root: 'src/content/agents', testsRoot: 'src/content/agent-tests' }, - repository: { provider: 'git', owner: 'local', name: 'project-a', defaultBranch: 'main', cloneUrl: repoRoot, checkoutPath: '.' }, - }; -} - -function leasedAssignment(repoRoot: string) { - return { - id: 'assignment-a', - membershipId: 'membership-a', - stateVersion: 1, - teamId: 'team-a', - projectId: 'project-a', - capacityProviderId: 'provider-a', - providerSessionId: 'session-a', - executionProviderId: null, - laneId: null, - allocationSetId: 'allocation-set-a', - projectAgentClassId: 'researcher', - reservationId: 'reservation-a', - workDayId: 'workday-a', - taskId: 'task-a', - agentId: 'researcher', - handlerId: 'execution-content', - mode: 'planning' as const, - status: 'leased' as const, - leaseState: 'leased' as const, - leaseExpiresAt: new Date(Date.now() + 300_000).toISOString(), - leaseToken: 'lease-a', - leaseRenewedAt: null, - runnerId: 'runner-a', - decisionInput: { teamId: 'team-a', projectId: 'project-a', projectAgentClassId: 'researcher', agentId: 'researcher', handlerId: 'execution-content', mode: 'planning', input: {}, metadata: { demandId: 'demand-a' } }, - capacityEnvelope: { teamId: 'team-a', projectId: 'project-a', projectAgentClassId: 'researcher', capacityProviderId: 'provider-a', workDayId: 'workday-a', mode: 'planning' as const, reservedCredits: 3, metadata: { demandId: 'demand-a' } }, - workspaceContext: { project: projectContext(repoRoot) }, - allowedOutputs: {}, - explanation: {}, - attemptCount: 1, - assignedAt: new Date().toISOString(), - claimedAt: new Date().toISOString(), - completedAt: null, - returnedAt: null, - failedAt: null, - lifecycleReason: null, - lifecycleCode: null, - lifecycleOutput: {}, - synthesizedFrom: 'workday_demand' as const, - synthesisKey: 'workday-a:agent-a', - decisionId: null, - proposalId: null, - fallbackOutputId: null, - treedxProxyHandle: null, - capabilityHandles: null, - metadata: { demandId: 'demand-a', workdayRunId: 'workday-run-a' }, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; -} - -afterEach(() => { - vi.unstubAllGlobals(); - delete process.env.TREESEED_PROVIDER_TREEDX_REQUEST_TIMEOUT_MS; - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); -}); -describe('assignment-scoped provider runtime', () => { -async function runTerminalLifecycle(status: 'returned' | 'failed', expected: 'return' | 'fail', retryable: boolean) { - const repoRoot = repository(); - const calls: string[] = []; - await runProviderRunnerOnce({ - config: connectionConfig({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }), - leasedAssignment: { ok: true, leaseToken: 'lease-a', payload: leasedAssignment(repoRoot) }, - client: { - async nextAssignment() { throw new Error('runner must not poll'); }, - async createAssignmentModeRun() { return { ok: true, payload: {} }; }, - async completeAssignment() { calls.push('complete'); return { ok: true }; }, - async returnAssignment() { calls.push('return'); return { ok: true }; }, - async failAssignment() { calls.push('fail'); return { ok: true }; }, - }, - kernel: { - async runAssignment() { - return { - status, mode: 'planning' as const, assignmentId: 'assignment-a', projectId: 'project-a', projectAgentClassId: 'researcher', summary: status, - selectedInput: {}, capacityEnvelope: leasedAssignment(repoRoot).capacityEnvelope, - fallback: { code: `assignment_${status}`, reason: status, retryable }, - }; - }, - }, - }); - expect(calls).toEqual([expected]); - } - -it('releases internal execution-provider resources only after a durable terminal response', async () => { - const release = vi.fn(async () => undefined); - await expect(releaseTerminalAssignmentResources({ payload: { status: 'leased' } }, release)).resolves.toBe(false); - expect(release).not.toHaveBeenCalled(); - await expect(releaseTerminalAssignmentResources({ payload: { status: 'completed' } }, release)).resolves.toBe(true); - expect(release).toHaveBeenCalledWith('completed'); - await expect(releaseTerminalAssignmentResources({ payload: { status: 'returned' } }, release)).resolves.toBe(true); - expect(release).toHaveBeenCalledWith('returned'); - await expect(releaseTerminalAssignmentResources({ payload: { status: 'cancelled' } }, release)).resolves.toBe(true); - expect(release).toHaveBeenCalledWith('cancelled'); - expect(release).toHaveBeenCalledTimes(3); - }); - -it('recognizes authoritative terminal observations when a renewal races completion', () => { - expect(isTerminalProviderAssignmentObservation({ payload: { status: 'completed', leaseState: 'released' } })).toBe(true); - expect(isTerminalProviderAssignmentObservation({ assignment: { status: 'returned', leaseState: 'released' } })).toBe(true); - expect(isTerminalProviderAssignmentObservation({ payload: { status: 'leased', leaseState: 'released' } })).toBe(true); - expect(isTerminalProviderAssignmentObservation({ payload: { status: 'leased', leaseState: 'leased' } })).toBe(false); - expect(providerAssignmentCancellationRequest({payload:{status:'leased',leaseState:'leased',executionKind:'conversation',metadata:{cancellationRequested:true,cancellationReason:'discussion_archived'}}})).toEqual({code:'discussion_archived',reason:'The source Discussion was archived.'}); - expect(providerAssignmentCancellationRequest({payload:{status:'leased',leaseState:'leased',executionKind:'workday',metadata:{cancellationRequested:true,cancellationReason:'discussion_archived'}}})).toBeNull(); - }); - -it('suppresses a detached renewal rejection after the terminal boundary wins the race', () => { - const error = vi.spyOn(console, 'error').mockImplementation(() => undefined); - expect(reportProviderLeaseRenewalFailure({ - terminalizing: () => true, - assignmentId: 'assignment-a', - runnerId: 'runner-a', - error: new Error('late lease transition rejection'), - })).toBe(false); - expect(error).not.toHaveBeenCalled(); - }); - -it('serializes timer and execution-lifecycle lease renewals', async () => { - let release!: () => void; - const attempt = vi.fn(async () => new Promise((resolve) => { release = resolve; })); - const renew = createSingleFlightLeaseRenewal(attempt); - const first = renew(); - const second = renew(); - expect(attempt).toHaveBeenCalledTimes(1); - release(); - await Promise.all([first, second]); - const third = renew(); - expect(attempt).toHaveBeenCalledTimes(2); - release(); - await third; - }); - -it('closes lease renewal authority before invoking any terminal lifecycle request', async () => { - const events: string[] = []; - const client = providerAssignmentClientWithTerminalBoundary({ - async nextAssignment() { return {}; }, - async createAssignmentModeRun() { return {}; }, - async settleAssignment() { events.push('settle'); return {}; }, - async completeAssignment() { events.push('complete'); return {}; }, - async failAssignment() { events.push('fail'); return {}; }, - async returnAssignment() { events.push('return'); return {}; }, - }, () => events.push('terminalizing')); - await client.settleAssignment?.('assignment-a', {}, 'settlement-a'); - await client.completeAssignment('assignment-a', {}); - await client.returnAssignment?.('assignment-a', {}); - await client.failAssignment('assignment-a', {}); - expect(events).toEqual(['terminalizing', 'settle', 'terminalizing', 'complete', 'terminalizing', 'return', 'terminalizing', 'fail']); - }); - -it('renders a provider plan without fetching team-wide portfolio state', async () => { - const hostConfig = config({ TREESEED_MARKET_URL: 'https://legacy-single-team.invalid', TREESEED_MARKET_ID: 'legacy-team' }); - expect(hostConfig).not.toHaveProperty('marketUrl'); - expect(hostConfig).not.toHaveProperty('marketId'); - expect(hostConfig).not.toHaveProperty('accessToken'); - const report = await buildProviderPlan(hostConfig); - expect(report).toMatchObject({ ok: true, role: 'plan', mode: 'plan' }); - expect(report).not.toHaveProperty('portfolio'); - expect(report).not.toHaveProperty('marketUrl'); - expect(JSON.stringify(report.redactedEnv)).not.toContain('ACCESS_TOKEN'); - }); - -it('materializes only the project carried by the assignment envelope', async () => { - const repoRoot = repository(); - const runtimeConfig = config({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }); - const assignment = { workspaceContext: { project: projectContext(repoRoot) } }; - const exact = assignmentProjectContext(assignment); - expect(exact).toMatchObject({ id: 'project-a', slug: 'project-a' }); - const materialized = await materializeAssignmentProject(runtimeConfig, exact!, { assignmentId: 'assignment-a' }); - expect(materialized.repository).toMatchObject({ ok: true, materialization: 'clone' }); - expect(materialized.repository.path).not.toBe(repoRoot); - expect(materialized.repository.path).toContain(resolve(runtimeConfig.dataDir, 'assignments/assignment-a')); - expect(materialized.repository.mirrorPath).toContain(resolve(runtimeConfig.dataDir, 'repositories')); - }); - -it('uses an isolated empty execution context instead of cloning for context-only planning', async () => { - const runtimeConfig = config(); - const project = projectContext('https://invalid.example/private-project.git'); - project.architecture = { rootPath: '.', sitePath: '.', contentPath: 'src/content', localContentMaterialization: 'remote' }; - const materialized = await materializeAssignmentProject(runtimeConfig, project, { workspaceAccessMode: 'context_only' }); - expect(materialized.repository).toMatchObject({ - ok: true, - materialization: 'context', - commitSha: expect.stringMatching(/^[a-f0-9]{40}$/u), - }); - expect(materialized.repository.path).toContain(resolve(runtimeConfig.dataDir, 'assignment-contexts')); - expect(execFileSync('git', ['rev-parse', '--verify', 'HEAD^{commit}'], { cwd: materialized.repository.path, encoding: 'utf8' }).trim()).toBe(materialized.repository.commitSha); -}); - -it('releases a terminal assignment materialization while retaining shared repository mirrors', async () => { - const repoRoot = repository(); - const runtimeConfig = config({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }); - const materialized = await materializeAssignmentProject(runtimeConfig, projectContext(repoRoot), { assignmentId: 'assignment-release' }); - const assignmentRoot = resolve(materialized.repository.path, '..'); - expect(existsSync(materialized.repository.path)).toBe(true); - await releaseMaterializedAssignmentProject(runtimeConfig, materialized); - expect(existsSync(assignmentRoot)).toBe(false); - expect(existsSync(materialized.repository.mirrorPath!)).toBe(true); -}); - -it('materializes the repository checkout, not the logical project root, for exact-ref work', async () => { - const workspaceRoot = temporaryDirectory(); - const repoRoot = resolve(workspaceRoot, 'starters/engineering'); - mkdirSync(repoRoot, { recursive: true }); - writeFileSync(resolve(repoRoot, 'package.json'), '{"name":"engineering-starter"}\n'); - execFileSync('git', ['init', '-b', 'main'], { cwd: repoRoot, stdio: 'ignore' }); - execFileSync('git', ['config', 'user.name', 'TreeSeed Test'], { cwd: repoRoot, stdio: 'ignore' }); - execFileSync('git', ['config', 'user.email', 'test@treeseed.local'], { cwd: repoRoot, stdio: 'ignore' }); - execFileSync('git', ['add', '-A'], { cwd: repoRoot, stdio: 'ignore' }); - execFileSync('git', ['commit', '-m', 'test fixture'], { cwd: repoRoot, stdio: 'ignore' }); - const runtimeConfig = config({ TREESEED_PROVIDER_WORKSPACE_ROOT: workspaceRoot }); - const project = projectContext(repoRoot); - project.architecture = { rootPath: 'template', sitePath: 'template', contentPath: 'template/src/content', localContentMaterialization: 'existing_path' }; - project.repository.checkoutPath = 'starters/engineering'; - const materialized = await materializeAssignmentProject(runtimeConfig, project, { assignmentId: 'assignment-exact', workspaceAccessMode: 'workspace_write', requiresRepository: true }); - expect(materialized.repository).toMatchObject({ ok: true, materialization: 'clone' }); - expect(materialized.repository.path).not.toBe(repoRoot); - }); - -it('fails local materialization before kernel execution when the governed exact ref is unavailable', async () => { - const repoRoot = repository(); - const runtimeConfig = config({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }); - const project = projectContext(repoRoot); - const missingRef = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; - const materialized = await materializeAssignmentProject(runtimeConfig, project, { - assignmentId: 'assignment-missing', - workspaceAccessMode: 'context_only', - requiresRepository: true, - exactRef: missingRef, - }); - expect(materialized.repository).toMatchObject({ ok: false, materialization: 'clone' }); - expect(materialized.repository.path).not.toBe(repoRoot); - expect(materialized.repository.error).toContain(`governed exact ref ${missingRef}`); - }); - -it('fails closed before kernel execution when API-issued workday authority is incomplete', async () => { - const repoRoot = repository(); - const assignment = leasedAssignment(repoRoot); - delete (assignment as Partial).membershipId; - const failures: Record[] = []; - const result = await runProviderAssignment({ - config: connectionConfig({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }), - client: { - async nextAssignment() { return { ok: true }; }, - async createAssignmentModeRun() { return { ok: true, payload: {} }; }, - async completeAssignment() { return { ok: true }; }, - async failAssignment(_id, body) { failures.push(body); return { ok: true, payload: { status: 'failed' } }; }, - }, - assignment, - leaseToken: 'lease-a', - runnerId: 'runner-a', - leaseSeconds: 300, - async renewLease() {}, - kernel: { async runAssignment() { throw new Error('kernel must not execute'); } }, - }); - expect(result).toMatchObject({ ok: true, payload: { status: 'failed' } }); - expect(failures).toEqual([expect.objectContaining({ code: 'workday_assignment_authority_invalid', retryable: false })]); - }); -}); diff --git a/tests/integration/provider/capacity/assignments/assignment-runtime.returns-a-leased-assignment-after-an-unexpected-preparation-failure.test.ts b/tests/integration/provider/capacity/assignments/assignment-runtime.returns-a-leased-assignment-after-an-unexpected-preparation-failure.test.ts deleted file mode 100644 index c4ac9bf3..00000000 --- a/tests/integration/provider/capacity/assignments/assignment-runtime.returns-a-leased-assignment-after-an-unexpected-preparation-failure.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { execFileSync } from 'node:child_process'; - -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; - -import { tmpdir } from 'node:os'; - -import { join, resolve } from 'node:path'; - -import { afterEach, describe, expect, it, vi } from 'vitest'; - -import { resolveProviderConfig } from '../../../../../src/provider/configuration/config.ts'; - -import { buildProviderPlan, buildProviderRunnerPlan } from '../../../../../src/provider/lifecycle/lifecycle.ts'; - -import { assignmentProjectContext, materializeAssignmentProject } from '../../../../../src/provider/projects/projects-core/project-materialization.ts'; - -import { releaseTerminalAssignmentResources, runProviderAssignment } from '../../../../../src/provider/operations/runner.ts'; - -import { createSingleFlightLeaseRenewal, isTerminalProviderAssignmentObservation, reportProviderLeaseRenewalFailure, runProviderRunnerOnce } from '../../../../../src/provider/operations/runner-lifecycle.ts'; - -import { providerAssignmentClientWithTerminalBoundary } from '../../../../../src/provider/coordination/lease-client.ts'; - -const roots: string[] = []; - -function temporaryDirectory() { - const path = mkdtempSync(join(tmpdir(), 'treeseed-assignment-runtime-')); - roots.push(path); - return path; -} - -function repository() { - const path = temporaryDirectory(); - mkdirSync(resolve(path, 'src/content/agents'), { recursive: true }); - writeFileSync(resolve(path, 'package.json'), '{"name":"assignment-project"}\n'); - execFileSync('git', ['init', '-b', 'main'], { cwd: path, stdio: 'ignore' }); - return path; -} - -function config(overrides: NodeJS.ProcessEnv = {}) { - return resolveProviderConfig({ env: { - TREESEED_PROVIDER_DATA_DIR: temporaryDirectory(), - TREESEED_PROVIDER_ENVIRONMENT: 'local', - HOME: temporaryDirectory(), - ...overrides, - } }); -} - -function connectionConfig(overrides: NodeJS.ProcessEnv = {}) { - return { - ...config(overrides), - connectionId: 'connection-a', - marketUrl: 'https://market.test', - marketAudience: 'https://market.test', - teamId: 'team-a', - providerId: 'provider-a', - membershipId: 'membership-a', - accessToken: 'short-lived-token', - }; -} - -function projectContext(repoRoot: string) { - return { - id: 'project-a', - slug: 'project-a', - name: 'Project A', - architecture: { rootPath: '.', sitePath: '.', contentPath: 'src/content', localContentMaterialization: 'existing_path' }, - agentSpecs: { root: 'src/content/agents', testsRoot: 'src/content/agent-tests' }, - repository: { provider: 'git', owner: 'local', name: 'project-a', defaultBranch: 'main', cloneUrl: repoRoot, checkoutPath: '.' }, - }; -} - -function leasedAssignment(repoRoot: string) { - return { - id: 'assignment-a', - membershipId: 'membership-a', - stateVersion: 1, - teamId: 'team-a', - projectId: 'project-a', - capacityProviderId: 'provider-a', - providerSessionId: 'session-a', - executionProviderId: null, - laneId: null, - allocationSetId: 'allocation-set-a', - projectAgentClassId: 'researcher', - reservationId: 'reservation-a', - workDayId: 'workday-a', - taskId: 'task-a', - agentId: 'researcher', - handlerId: 'execution-content', - mode: 'planning' as const, - status: 'leased' as const, - leaseState: 'leased' as const, - leaseExpiresAt: new Date(Date.now() + 300_000).toISOString(), - leaseToken: 'lease-a', - leaseRenewedAt: null, - runnerId: 'runner-a', - decisionInput: { teamId: 'team-a', projectId: 'project-a', projectAgentClassId: 'researcher', agentId: 'researcher', handlerId: 'execution-content', mode: 'planning', input: {}, metadata: { demandId: 'demand-a' } }, - capacityEnvelope: { teamId: 'team-a', projectId: 'project-a', projectAgentClassId: 'researcher', capacityProviderId: 'provider-a', workDayId: 'workday-a', mode: 'planning' as const, reservedSeconds: 3, metadata: { demandId: 'demand-a' } }, - workspaceContext: { project: projectContext(repoRoot) }, - allowedOutputs: {}, - explanation: {}, - attemptCount: 1, - assignedAt: new Date().toISOString(), - claimedAt: new Date().toISOString(), - completedAt: null, - returnedAt: null, - failedAt: null, - lifecycleReason: null, - lifecycleCode: null, - lifecycleOutput: {}, - synthesizedFrom: 'workday_demand' as const, - synthesisKey: 'workday-a:agent-a', - decisionId: null, - proposalId: null, - fallbackOutputId: null, - treedxProxyHandle: null, - capabilityHandles: null, - metadata: { demandId: 'demand-a', workdayRunId: 'workday-run-a' }, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; -} - -afterEach(() => { - vi.unstubAllGlobals(); - delete process.env.TREESEED_PROVIDER_TREEDX_REQUEST_TIMEOUT_MS; - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); -}); -describe('assignment-scoped provider runtime', () => { -async function runTerminalLifecycle(status: 'returned' | 'failed', expected: 'return' | 'fail', retryable: boolean) { - const repoRoot = repository(); - const calls: string[] = []; - await runProviderRunnerOnce({ - config: connectionConfig({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }), - leasedAssignment: { ok: true, leaseToken: 'lease-a', payload: leasedAssignment(repoRoot) }, - client: { - async nextAssignment() { throw new Error('runner must not poll'); }, - async createAssignmentModeRun() { return { ok: true, payload: {} }; }, - async completeAssignment() { calls.push('complete'); return { ok: true }; }, - async returnAssignment() { calls.push('return'); return { ok: true }; }, - async failAssignment() { calls.push('fail'); return { ok: true }; }, - }, - kernel: { - async runAssignment() { - return { - status, mode: 'planning' as const, assignmentId: 'assignment-a', projectId: 'project-a', projectAgentClassId: 'researcher', summary: status, - selectedInput: {}, capacityEnvelope: leasedAssignment(repoRoot).capacityEnvelope, - fallback: { code: `assignment_${status}`, reason: status, retryable }, - }; - }, - }, - }); - expect(calls).toEqual([expected]); - } - -it('returns a leased assignment after an unexpected preparation failure', async () => { - const repoRoot = repository(); - const runtimeConfig = connectionConfig({ - TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot, - TREESEED_TREEDX_BASE_URL: 'https://treedx.test', - TREESEED_TREEDX_TOKEN: 'treedx-token', - }); - process.env.TREESEED_PROVIDER_TREEDX_REQUEST_TIMEOUT_MS = '25'; - vi.stubGlobal('fetch', async (_input: RequestInfo | URL, init?: RequestInit) => new Promise((_resolve, reject) => { - init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true }); - })); - const calls: Array<{ method: string; body?: Record }> = []; - const assignment = { - ...leasedAssignment(repoRoot), - metadata: { ...leasedAssignment(repoRoot).metadata, assignmentSource: 'capacity_workday_demand', contentRoot: 'src/content' }, - decisionInput: { ...leasedAssignment(repoRoot).decisionInput, input: { exactBaseRef: 'immutable-ref-from-treedx' } }, - workspaceContext: { project: { ...projectContext(repoRoot), repository: { ...projectContext(repoRoot).repository, cloneUrl: 'https://invalid.example/must-not-clone.git' } } }, - treedxProxyHandle: { - id: 'handle-a', teamId: 'team-a', projectId: 'project-a', assignmentId: 'assignment-a', repositoryId: 'repo-a', workspaceId: 'workspace-a', - allowedOperations: ['files:read'], allowedPaths: ['**'], expiresAt: new Date(Date.now() + 60_000).toISOString(), - }, - }; - const result = await runProviderRunnerOnce({ - config: runtimeConfig, - runnerId: 'runner-a', - leasedAssignment: { ok: true, leaseToken: 'lease-a', leaseSeconds: 300, payload: assignment }, - client: { - async nextAssignment() { throw new Error('runner must not poll'); }, - async renewAssignment() { return { ok: true, payload: assignment }; }, - async createAssignmentModeRun(_id, body) { calls.push({ method: 'mode-run', body }); return { ok: true, payload: {} }; }, - async completeAssignment() { return { ok: true }; }, - async returnAssignment(_id, body) { calls.push({ method: 'return', body }); return { ok: true, payload: { status: 'returned' } }; }, - async failAssignment(_id, body) { calls.push({ method: 'fail', body }); return { ok: true }; }, - }, - treeDx: runtimeConfig.treeDx ?? undefined, - }); - expect(result).toMatchObject({ ok: true, assigned: 1 }); - expect(calls.find((entry) => entry.method === 'return')?.body).toMatchObject({ - code: 'treedx_proxy_timeout', retryable: true, leaseToken: 'lease-a', runnerId: 'runner-a', - }); - expect(calls.some((entry) => entry.method === 'fail')).toBe(false); - }); - -it('returns the lease with explicit diagnostics when required telemetry cannot be persisted', async () => { - const repoRoot = repository(); - const calls: Array<{ method: string; body?: Record }> = []; - const createAssignmentModeRun = vi.fn().mockRejectedValue(new Error('mode-run database unavailable')); - const result = await runProviderRunnerOnce({ - config: connectionConfig({ TREESEED_PROVIDER_WORKSPACE_ROOT: repoRoot }), - runnerId: 'runner-a', - leasedAssignment: { ok: true, leaseToken: 'lease-a', leaseSeconds: 300, payload: leasedAssignment(repoRoot) }, - client: { - async nextAssignment() { throw new Error('runner must not poll'); }, - createAssignmentModeRun, - async completeAssignment() { return { ok: true }; }, - async returnAssignment(_id, body) { calls.push({ method: 'return', body }); return { ok: true, payload: { status: 'returned' } }; }, - async failAssignment(_id, body) { calls.push({ method: 'fail', body }); return { ok: true }; }, - }, - }); - expect(result).toMatchObject({ ok: true, assigned: 1 }); - expect(createAssignmentModeRun).toHaveBeenCalledTimes(6); - expect(calls).toEqual([{ - method: 'return', - body: expect.objectContaining({ - code: 'provider_mode_run_telemetry_delivery_failed', - leaseToken: 'lease-a', - runnerId: 'runner-a', - retryable: true, - metadata: expect.objectContaining({ telemetryDeliveryFailed: true }), - }), - }]); - }); -}); diff --git a/tests/integration/provider/capacity/capacity-core/local-capacity-store.test.ts b/tests/integration/provider/capacity/capacity-core/local-capacity-store.test.ts deleted file mode 100644 index 826f45cc..00000000 --- a/tests/integration/provider/capacity/capacity-core/local-capacity-store.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { ProviderLocalCapacityStore } from '../../../../../src/provider/capacity/capacity-core/local-capacity-store.ts'; -import { compileProviderLocalNativeLimit } from '../../../../../src/provider/capacity/capacity-core/native-capacity-limits.ts'; - -describe('durable provider-local capacity claims', () => { - it('admits one simultaneous final slot and survives a new store instance', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-capacity-state-')); - try { - const firstProcess = new ProviderLocalCapacityStore(root); - const secondProcess = new ProviderLocalCapacityStore(root); - const claims = await Promise.all([ - firstProcess.claim({ connectionId: 'team-a', globalLimit: 1, connectionLimit: 1 }), - secondProcess.claim({ connectionId: 'team-b', globalLimit: 1, connectionLimit: 1 }), - ]); - expect(claims.filter(Boolean)).toHaveLength(1); - const winner = claims.find((claim) => claim !== null)!; - await firstProcess.attachLease(winner.id, { assignmentId: 'assignment-a', leaseToken: 'lease-secret', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: { ok: true, payload: { id: 'assignment-a' }, leaseToken: 'lease-secret' } }); - const renewedExpiry = new Date(Date.now() + 120_000).toISOString(); - await secondProcess.renewLease(winner.id, { assignmentId: 'assignment-a', leaseExpiresAt: renewedExpiry }); - expect((await new ProviderLocalCapacityStore(root).snapshot()).claims).toEqual([expect.objectContaining({ id: winner.id, status: 'ready', leaseToken: '' })]); - expect((await new ProviderLocalCapacityStore(root).snapshot()).claims).toEqual([expect.objectContaining({ leaseExpiresAt: renewedExpiry, expiresAt: renewedExpiry })]); - expect(await new ProviderLocalCapacityStore(root).claimsForRecovery()).toEqual([]); - expect(await secondProcess.claimDispatch()).toEqual(expect.objectContaining({ id: winner.id, status: 'running', dispatchEnvelope: expect.any(Object) })); - const recovered = await new ProviderLocalCapacityStore(root).claimsForRecovery(); - expect(recovered).toEqual([expect.objectContaining({ id: winner.id, status: 'running', assignmentId: 'assignment-a', leaseToken: 'lease-secret' })]); - expect(await secondProcess.claim({ connectionId: 'team-b', globalLimit: 1, connectionLimit: 1 })).toBeNull(); - await secondProcess.release(winner.id); - expect(await firstProcess.finalize(winner.id, 'assignment-lifecycle-confirmed')).toBe(false); - expect(await secondProcess.claim({ connectionId: 'team-b', globalLimit: 1, connectionLimit: 1 })).not.toBeNull(); - await firstProcess.saveSession('team-a|membership-a', { id: 'session-a', sequence: 2 }); - expect(await new ProviderLocalCapacityStore(root).session('team-a|membership-a')).toMatchObject({ id: 'session-a', sequence: 2 }); - await firstProcess.saveToken('team-a', { id: 'token-a', teamId: 'team-a', providerId: 'provider-a', membershipId: 'membership-a', credentialId: 'credential-a', status: 'active', scopes: ['provider:assignments:read'], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(), accessToken: 'token-secret', identityVersion: 1 }); - expect(await new ProviderLocalCapacityStore(root).token('team-a')).toMatchObject({ id: 'token-a', accessToken: 'token-secret' }); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('retains unconfirmed running work and records bounded terminal evidence', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-lifecycle-evidence-')); - try { - const store = new ProviderLocalCapacityStore(root); - const claim = await store.claim({ connectionId: 'team-a', globalLimit: 1, connectionLimit: 1 }); - if (!claim) throw new Error('Expected a local claim.'); - await store.attachLease(claim.id, { assignmentId: 'assignment-a', leaseToken: 'secret', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: {} }); - await store.claimDispatch(['team-a']); - await store.recordFailure(claim.id, 'control plane unavailable'); - expect((await store.snapshot()).claims).toEqual([expect.objectContaining({ id: claim.id, status: 'recovery' })]); - await store.finalize(claim.id, 'assignment-lifecycle-confirmed'); - const snapshot = await store.snapshot(); - expect(snapshot.claims).toEqual([]); - expect(snapshot.events).toEqual(expect.arrayContaining([ - expect.objectContaining({ claimId: claim.id, outcome: 'lifecycle-unconfirmed', message: 'control plane unavailable' }), - expect.objectContaining({ claimId: claim.id, outcome: 'assignment-lifecycle-confirmed' }), - ])); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('moves an expired leased claim into recovery instead of discarding its cleanup identity', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-expired-claim-')); - try { - const store = new ProviderLocalCapacityStore(root); - const claim = await store.claim({ connectionId: 'team-a', globalLimit: 1, connectionLimit: 1 }); - if (!claim) throw new Error('Expected a local claim.'); - await store.attachLease(claim.id, { - assignmentId: 'assignment-expired', leaseToken: 'lease-expired', - leaseExpiresAt: new Date(Date.now() - 1_000).toISOString(), dispatchEnvelope: {}, - }); - const recovered = await store.claimsForRecovery(); - expect(recovered).toEqual([expect.objectContaining({ - id: claim.id, assignmentId: 'assignment-expired', status: 'recovery', leaseToken: 'lease-expired', - })]); - expect((await store.snapshot()).events).toEqual(expect.arrayContaining([ - expect.objectContaining({ claimId: claim.id, outcome: 'lease-expired-recovery-required' }), - ])); - expect(await store.claim({ connectionId: 'team-b', globalLimit: 1, connectionLimit: 1 })).toBeNull(); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('serializes execution-provider and lane concurrency and native allowance', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-native-state-')); - try { - const store = new ProviderLocalCapacityStore(root); - const first = await store.claim({ connectionId: 'team-a', globalLimit: 2, connectionLimit: 2 }); - const second = await store.claim({ connectionId: 'team-b', globalLimit: 2, connectionLimit: 2 }); - if (!first || !second) throw new Error('Expected two generic claims.'); - const limit = { maxConcurrentRunners: 1, availableCredits: 5 }; - await store.attachLease(first.id, { assignmentId: 'assignment-a', leaseToken: 'lease-a', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: {}, executionProviderId: 'codex', laneId: 'research', requestedCredits: 4, executionProviderLimit: limit, laneLimit: limit }); - await expect(store.attachLease(second.id, { assignmentId: 'assignment-b', leaseToken: 'lease-b', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: {}, executionProviderId: 'codex', laneId: 'research', requestedCredits: 2, executionProviderLimit: limit, laneLimit: limit })).rejects.toThrow(/concurrency is exhausted/u); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('accounts arbitrary native units with configured reserve buffers', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-native-units-')); - try { - const store = new ProviderLocalCapacityStore(root); - const first = await store.claim({ connectionId: 'team-a', globalLimit: 2, connectionLimit: 2 }); - const second = await store.claim({ connectionId: 'team-b', globalLimit: 2, connectionLimit: 2 }); - if (!first || !second) throw new Error('Expected two generic claims.'); - const limit = compileProviderLocalNativeLimit({ - executionProviderId: 'codex', nativeLimits: { maxConcurrentRunners: 2 }, - budgets: { nativeCapacity: { executionProviders: [{ id: 'codex', name: 'Codex', kind: 'codex', nativeUnit: 'wall_minute', nativeLimits: [{ nativeUnit: 'wall_minute', limitAmount: 10, reserveBufferPercent: 20 }] }] } }, - }); - expect(limit.nativeAllowances).toEqual({ wall_minute: 8 }); - await store.attachLease(first.id, { assignmentId: 'assignment-a', leaseToken: 'lease-a', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: {}, executionProviderId: 'codex', nativeUnit: 'wall_minute', requestedNativeAmount: 6, executionProviderLimit: limit }); - await expect(store.attachLease(second.id, { assignmentId: 'assignment-b', leaseToken: 'lease-b', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: {}, executionProviderId: 'codex', nativeUnit: 'wall_minute', requestedNativeAmount: 3, executionProviderLimit: limit })).rejects.toThrow(/wall_minute allowance is exhausted/u); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -}); diff --git a/tests/integration/provider/capacity/providers/agent-tool-provider-runtime.test.ts b/tests/integration/provider/capacity/providers/agent-tool-provider-runtime.test.ts deleted file mode 100644 index 7e4a1942..00000000 --- a/tests/integration/provider/capacity/providers/agent-tool-provider-runtime.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { createAssignmentToolCatalog } from '../../../../../src/provider/commerce/catalog/assignment-tool-catalog.ts'; - -const writeHandle = { - id: 'handle-1', - teamId: 'team-1', - projectId: 'project-1', - assignmentId: 'assignment-1', - repositoryId: 'repo-1', - workspaceId: 'workspace-1', - status: 'active', - tokenHash: 'secret_should_not_leak', - allowedOperations: ['files:read', 'files:search', 'files:write', 'git:commit'], - allowedPaths: ['src/content/**'], -}; - -describe('provider agent tool catalog', () => { - it('exposes research tools only with a non-empty provider/project policy intersection', () => { - const denied = createAssignmentToolCatalog({ - agentTools: ['research.search_sources', 'research.fetch_source'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - }); - expect(denied.exposed).toEqual(['treeseed.status', 'treeseed.discussion.read', 'treeseed.discussion.follow', 'treeseed.discussion.respond', 'treeseed.discussion.request_handoff', 'treeseed.operation.prepare_handoff', 'treeseed.client_session.request_action']); - expect(denied.omitted).toEqual(expect.arrayContaining([ - { id: 'research.search_sources', missing: ['research_source_policy'] }, - { id: 'research.fetch_source', missing: ['research_source_policy'] }, - ])); - - const allowed = createAssignmentToolCatalog({ - agentTools: ['research.search_sources', 'research.fetch_source'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - researchNetworkPolicy: { allowWeb: true, allowedDomains: ['sources.example.test'] }, - providerResearchSourcePolicy: { allowedDomains: ['example.test'] }, - }); - expect(allowed.exposed).toEqual(expect.arrayContaining(['research.search_sources', 'research.fetch_source', 'treeseed.status'])); - expect(allowed.descriptors[0]?.metadata).toMatchObject({ researchAllowedDomains: ['sources.example.test'] }); - - const disjoint = createAssignmentToolCatalog({ - agentTools: ['research.fetch_source'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - researchNetworkPolicy: { allowWeb: true, allowedDomains: ['sources.example.test'] }, - providerResearchSourcePolicy: { allowedDomains: ['other.test'] }, - }); - expect(disjoint.omitted).toContainEqual({ id: 'research.fetch_source', missing: ['research_source_policy'] }); - }); - - it('omits TreeDX tools when the assignment lacks a valid proxy handle', () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treedx.read_workspace_file', 'treeseed.status'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - allowedPaths: ['src/content/**'], - forbiddenPaths: [], - }); - - expect(catalog.exposed).toEqual(['treeseed.status', 'treeseed.discussion.read', 'treeseed.discussion.follow', 'treeseed.discussion.respond', 'treeseed.discussion.request_handoff', 'treeseed.operation.prepare_handoff', 'treeseed.client_session.request_action']); - expect(catalog.omitted).toContainEqual({ id: 'treedx.read_workspace_file', missing: ['treedx_proxy_handle'] }); - expect(catalog.descriptors.some((descriptor) => descriptor.executionTarget === 'treedx_proxy')).toBe(false); - }); - - it('exposes only read-capable TreeDX tools in context-only mode', () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treedx.read_workspace_file', 'treedx.apply_workspace_changeset', 'treedx.commit_workspace'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: writeHandle, - workspaceMode: 'context_only', - allowedPaths: ['src/content/**'], - forbiddenPaths: [], - }); - - expect(catalog.exposed).toEqual(expect.arrayContaining(['treedx.read_workspace_file', 'treeseed.status', 'treeseed.discussion.read', 'treeseed.discussion.follow', 'treeseed.discussion.respond'])); - expect(catalog.omitted).toEqual(expect.arrayContaining([ - { id: 'treedx.apply_workspace_changeset', missing: ['treedx_writable_workspace'] }, - { id: 'treedx.commit_workspace', missing: ['treedx_writable_workspace', 'content_commit'] }, - ])); - }); - - it('exposes writable TreeDX descriptors with handle metadata in workspace-write mode', () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treedx.read_workspace_file', 'treedx.apply_workspace_changeset', 'treedx.commit_workspace'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: writeHandle, - workspaceMode: 'workspace_write', - permissionProjection: { write: { models: ['*'], actions: ['commit'] }, commit: { allowed: true } }, - allowedPaths: ['src/content/**'], - forbiddenPaths: ['src/content/private/**'], - }); - - expect(catalog.omitted).toEqual([]); - expect(catalog.descriptors[0]).toMatchObject({ - kind: 'agent_tool', - executionTarget: 'treedx_proxy', - projectId: 'project-1', - assignmentId: 'assignment-1', - handleId: 'handle-1', - repositoryId: 'repo-1', - workspaceId: 'workspace-1', - allowedOperations: ['files:read', 'files:search', 'files:write', 'git:commit'], - metadata: expect.objectContaining({ - assignmentId: 'assignment-1', - projectId: 'project-1', - allowedPaths: ['src/content/**'], - forbiddenPaths: ['src/content/private/**'], - }), - }); - expect(JSON.stringify(catalog)).not.toContain('secret_should_not_leak'); - }); - - it('marks dispatch preference only for SDK tools and keeps verify provider-owned', () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.status', 'treeseed.verify', 'treeseed.changed_paths'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - worktreeRoot: '/repo/.agent-worktrees/task', - allowedPaths: ['src/content/**'], - forbiddenPaths: [], - }); - - expect(catalog.descriptors.find((descriptor) => descriptor.id === 'treeseed.status')?.metadata).toMatchObject({ - dispatchPreferredMode: 'auto', - }); - expect(catalog.descriptors.find((descriptor) => descriptor.id === 'treeseed.verify')?.metadata).toMatchObject({ - worktreeRoot: '/repo/.agent-worktrees/task', - }); - expect(catalog.descriptors.find((descriptor) => descriptor.id === 'treeseed.verify')).toMatchObject({ - executionTarget: 'provider_runner', - mutability: 'read', - }); - expect(catalog.descriptors.find((descriptor) => descriptor.id === 'treeseed.verify')?.metadata?.dispatchPreferredMode).toBeUndefined(); - - const withoutWorktree = createAssignmentToolCatalog({ - agentTools: ['treeseed.verify'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - }); - expect(withoutWorktree.exposed).not.toContain('treeseed.verify'); - expect(withoutWorktree.omitted).toContainEqual({ - id: 'treeseed.verify', - missing: ['assignment_worktree'], - }); - const providerManagedWorktree = createAssignmentToolCatalog({ - agentTools: ['treeseed.verify', 'treeseed.checkpoint'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: {}, - worktreeRoot: null, - providerManagesWorktree: true, - }); - expect(providerManagedWorktree.exposed).toEqual(expect.arrayContaining(['treeseed.verify', 'treeseed.checkpoint', 'treeseed.status'])); - expect(providerManagedWorktree.descriptors.every((descriptor) => descriptor.metadata?.worktreeRoot === null)).toBe(true); - }); - - it('filters model-aware content tools through the canonical permission projection', () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.questions.create', 'treeseed.proposals.create', 'treeseed.content.commit'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: writeHandle, - workspaceMode: 'workspace_write', - permissionProjection: { - read: { models: ['question'], actions: ['query', 'read'] }, - write: { models: ['question'], actions: ['create', 'update', 'validate'] }, - commit: { allowed: false }, - }, - allowedPaths: ['src/content/**'], - forbiddenPaths: [], - }); - - expect(catalog.exposed).toEqual(expect.arrayContaining(['treeseed.questions.create', 'treeseed.status', 'treeseed.assignment_plan', 'treeseed.assignment_status_update', 'treeseed.assignment_summary'])); - expect(catalog.omitted).toEqual(expect.arrayContaining([ - { id: 'treeseed.proposals.create', missing: ['content_access'] }, - { id: 'treeseed.content.commit', missing: ['content_access', 'content_commit'] }, - ])); - expect(catalog.descriptors[0]).toMatchObject({ - executionTarget: 'treeseed_content', - metadata: expect.objectContaining({ - contentAction: 'create', - contentModel: 'question', - contentPreset: 'treeseed.questions.create', - }), - }); - expect(JSON.stringify(catalog)).not.toContain('secret_should_not_leak'); - }); - - it('omits writable content tools for context-only workspaces', () => { - const catalog = createAssignmentToolCatalog({ - agentTools: ['treeseed.questions.query', 'treeseed.questions.create'], - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: writeHandle, - workspaceMode: 'context_only', - permissionProjection: { - read: { models: ['question'], actions: ['query', 'read'] }, - write: { models: ['question'], actions: ['create'] }, - commit: { allowed: false }, - }, - allowedPaths: ['src/content/**'], - forbiddenPaths: [], - }); - - expect(catalog.exposed).toEqual(expect.arrayContaining(['treeseed.questions.query', 'treeseed.status'])); - expect(catalog.omitted).toContainEqual({ id: 'treeseed.questions.create', missing: ['treedx_writable_workspace'] }); - }); -}); diff --git a/tests/integration/provider/capacity/providers/execution-provider-selection.test.ts b/tests/integration/provider/capacity/providers/execution-provider-selection.test.ts deleted file mode 100644 index 4329e604..00000000 --- a/tests/integration/provider/capacity/providers/execution-provider-selection.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - AssignmentExecutionProviderSelectionError, - buildExecutionProviderRuntimeConfiguration, - resolveAssignmentExecutionProvider, -} from '../../../../../src/provider/capacity/providers/execution-provider-selection.ts'; - -const providers = [ - { id: 'codex-primary', adapter: 'codex', nativeLimits: { maxConcurrentRunners: 2 } }, - { id: 'research-workflow', adapter: 'workflow', nativeLimits: { maxConcurrentRunners: 1 } }, -]; - -describe('assignment execution-provider selection', () => { - it('resolves an arbitrary provider-global id to its manifest adapter', () => { - expect(resolveAssignmentExecutionProvider({ - assignment: { executionProviderId: 'codex-primary' }, - executionProviders: providers, - })).toEqual(providers[0]); - }); - - it('uses the sole configured provider only when the assignment is unambiguous', () => { - expect(resolveAssignmentExecutionProvider({ - assignment: {}, - executionProviders: [providers[1]!], - })).toEqual(providers[1]); - }); - - it('uses the manifest default when an assignment does not select a provider', () => { - expect(resolveAssignmentExecutionProvider({ - assignment: {}, - executionProviders: providers, - defaultExecutionProviderId: 'codex-primary', - })).toEqual(providers[0]); - }); - - it('fails closed for an unknown provider id', () => { - expect(() => resolveAssignmentExecutionProvider({ - assignment: { executionProviderId: 'codex-missing' }, - executionProviders: providers, - })).toThrowError(expect.objectContaining>({ - code: 'assignment_execution_provider_not_configured', - })); - }); - - it('fails closed when multiple providers exist without an assignment id', () => { - expect(() => resolveAssignmentExecutionProvider({ - assignment: {}, - executionProviders: providers, - })).toThrowError(expect.objectContaining>({ - code: 'assignment_execution_provider_id_required', - })); - }); - - it('wires the TreeSeed Codex profile to the private appliance gateway', () => { - const runtime = buildExecutionProviderRuntimeConfiguration({ - executionProvider: { - id: 'codex-treeseed', adapter: 'codex', profile: 'treeseed', protocol: 'responses', - model: { baseUrl: 'http://host.docker.internal:4771/v1', model: 'treeseed-qwen3.5-4b' }, - nativeLimits: { maxConcurrentRunners: 1 }, - }, - accessToken: 'provider-token', apiBaseUrl: 'https://api.treeseed.dev', - env: { TREESEED_AI_GATEWAY_TOKEN: 'inference-token' }, - }); - expect(runtime.env).toMatchObject({ - TREESEED_CODEX_MODEL_PROVIDER: 'treeseed', - TREESEED_CODEX_BASE_URL: 'http://host.docker.internal:4771/v1', - TREESEED_CODEX_DEFAULT_MODEL: 'treeseed-qwen3.5-4b', - OPENAI_API_KEY: 'inference-token', - }); - }); - - it('builds TreeSeed model configuration for Copilot and OpenCode profiles', () => { - const provider = { - id: 'ghcopilot-treeseed', adapter: 'copilot', profile: 'treeseed', protocol: 'responses', - model: { baseUrl: 'http://gateway/v1', model: 'treeseed-qwen3.5-4b' }, - nativeLimits: { maxConcurrentRunners: 1 }, - }; - const runtime = buildExecutionProviderRuntimeConfiguration({ executionProvider: provider, accessToken: 'provider', apiBaseUrl: 'https://api.treeseed.dev', env: { TREESEED_AI_GATEWAY_TOKEN: 'inference' } }); - expect(runtime).toMatchObject({ model: 'treeseed-qwen3.5-4b', openCodeProviderId: 'treeseed', copilotProvider: { type: 'openai', baseUrl: 'http://gateway/v1', apiKey: 'inference', wireApi: 'responses' } }); - }); -}); diff --git a/tests/integration/provider/coordination/provider-market-client.test.ts b/tests/integration/provider/coordination/provider-market-client.test.ts deleted file mode 100644 index 12e98570..00000000 --- a/tests/integration/provider/coordination/provider-market-client.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { - createProviderMarketClient, - PROVIDER_ASSIGNMENT_POLLING_TTL_MS, - PROVIDER_MARKET_REQUEST_TIMEOUT_MS, -} from '../../../../src/provider/coordination/client.ts'; -import type { ProviderConnectionRuntimeContext } from '../../../../src/provider/configuration/config.ts'; - -afterEach(() => { - vi.useRealTimers(); - vi.unstubAllGlobals(); -}); - -describe('provider Market client', () => { - it('keeps the provider-local polling claim alive beyond the Market request deadline', () => { - expect(PROVIDER_ASSIGNMENT_POLLING_TTL_MS).toBeGreaterThan(PROVIDER_MARKET_REQUEST_TIMEOUT_MS); - }); - - it('keeps assignment acquisition open beyond the control-plane synthesis window', async () => { - vi.useFakeTimers(); - vi.stubGlobal('fetch', vi.fn(async () => { - await new Promise((resolve) => setTimeout(resolve, 30_001)); - return new Response(JSON.stringify({ ok: true, payload: null }), { - status: 200, - headers: { 'content-type': 'application/json' }, - }); - })); - const client = createProviderMarketClient({ - marketUrl: 'https://market.test', - accessToken: 'provider-token', - } as ProviderConnectionRuntimeContext); - - const request = client.nextAssignment(); - await vi.advanceTimersByTimeAsync(30_001); - - await expect(request).resolves.toMatchObject({ ok: true, payload: null }); - expect(PROVIDER_MARKET_REQUEST_TIMEOUT_MS).toBeGreaterThan(30_001); - }); -}); diff --git a/tests/integration/provider/projects/projects-core/availability-projection.test.ts b/tests/integration/provider/projects/projects-core/availability-projection.test.ts deleted file mode 100644 index 07b7d36e..00000000 --- a/tests/integration/provider/projects/projects-core/availability-projection.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { compileConnectionAvailability } from '../../../../../src/provider/projects/projects-core/availability-projection.ts'; - -describe('team-scoped provider availability', () => { - it('narrows global capabilities, lanes, and concurrency to one connection offer', () => { - const projection = compileConnectionAvailability({ - connection: { id: 'team-a', marketUrl: 'https://team-a.test', teamId: 'team-a', providerId: 'provider-a', membershipId: 'membership-a', membershipCredentialRef: 'data://team-a', membershipCredentialId: 'credential-a', offer: { capabilities: ['research'], maxConcurrentRunners: 2 } }, - hostMaxConcurrentRunners: 1, - defaultExecutionProviderId: 'codex', - executionProviders: [{ id: 'codex', adapter: 'codex', minimumAssignmentDuration: { amount: 600, unit: 'seconds' }, nativeLimits: { maxConcurrentRunners: 4 }, capabilities: ['engineering', 'research'], lanes: [{ id: 'shared', maxConcurrentRunners: 4, capabilities: ['engineering', 'research'] }] }], - }); - expect(projection).toMatchObject({ capabilities: ['research'], maxConcurrentRunners: 1, executionProviders: [{ preferred: true, minimumAssignmentDuration: { amount: 600, unit: 'seconds' }, nativeLimits: { maxConcurrentRunners: 1 }, capabilities: ['research'], lanes: [{ maxConcurrentRunners: 1, capabilities: ['research'] }] }] }); - }); -}); diff --git a/tests/integration/provider/teams/multi-team-coordinator.test.ts b/tests/integration/provider/teams/multi-team-coordinator.test.ts deleted file mode 100644 index 882b45b5..00000000 --- a/tests/integration/provider/teams/multi-team-coordinator.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { CapacityProviderCoordinator } from '../../../../src/provider/coordination/coordinator.ts'; -import { initializeCapacityProviderIdentity } from '../../../../src/provider/accounts/identity.ts'; -import { loadProviderManifest, providerConnectionMarketAudience, providerConnectionMarketUrl } from '../../../../src/provider/configuration/manifest.ts'; - -function json(payload: unknown, status = 200) { - return new Response(JSON.stringify({ ok: status < 400, payload }), { status, headers: { 'content-type': 'application/json' } }); -} - -const providerContract = [ - 'providerClass: agent', 'ownership:', ' type: external', 'configuration:', ' generation: test-v1', - 'supplyCeilings:', ' maxConcurrentAssignments: 2', -]; - -describe('multi-team capacity provider coordinator', () => { - it('fails clearly when prelaunch connection state has no durable offer', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-stale-state-')); - try { - const dataDirectory = resolve(root, 'state'); - await initializeCapacityProviderIdentity({ ref: 'data://identity.jwk', baseDirectory: root, dataDirectory }); - const manifestPath = resolve(root, 'treeseed.capacity-provider.yaml'); - await writeFile(manifestPath, [ - 'schemaVersion: 2', ...providerContract, 'identity:', ' privateKeyRef: data://identity.jwk', ' displayName: Stale Provider', - 'executionProviders:', ' - id: codex', ' adapter: codex', ' nativeLimits: { maxConcurrentRunners: 1 }', - 'connections: []', - ].join('\n')); - await mkdir(resolve(dataDirectory, 'connections'), { recursive: true }); - await writeFile(resolve(dataDirectory, 'connections', 'stale.json'), JSON.stringify({ - schemaVersion: 1, connectionId: 'stale', marketUrl: 'https://stale.example.test', - registrationRequestId: 'request-stale', updatedAt: new Date().toISOString(), - })); - const coordinator = new CapacityProviderCoordinator(await loadProviderManifest(manifestPath), dataDirectory); - await expect(coordinator.pollRegistrationStatus('stale')).rejects.toThrow('missing its durable supply offer'); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('separates private transport URLs from canonical proof audiences', () => { - const connection = { id: 'local', marketProfile: 'local', offer: { capabilities: ['engineering'] } }; - const env = { - TREESEED_MARKET_PROFILE_LOCAL_URL: 'http://host.docker.internal:3000', - TREESEED_MARKET_PROFILE_LOCAL_AUDIENCE: 'http://127.0.0.1:3000', - }; - expect(providerConnectionMarketUrl(connection, env)).toBe('http://host.docker.internal:3000'); - expect(providerConnectionMarketAudience(connection, env)).toBe('http://127.0.0.1:3000'); - }); - - it('registers, waits for approval, and establishes independent team tokens', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-coordinator-')); - try { - const dataDirectory = resolve(root, 'state'); - await initializeCapacityProviderIdentity({ ref: 'data://identity.jwk', baseDirectory: root, dataDirectory }); - const manifestPath = resolve(root, 'treeseed.capacity-provider.yaml'); - await writeFile(manifestPath, [ - 'schemaVersion: 2', - ...providerContract, - 'identity:', - ' privateKeyRef: data://identity.jwk', - ' displayName: Shared Build Provider', - 'executionProviders:', - ' - id: codex', - ' adapter: codex', - ' nativeLimits: { maxConcurrentRunners: 2 }', - 'connections: []', - ].join('\n')); - const calls: Array<{ url: string; init?: RequestInit }> = []; - const credentialGenerations = new Map(); - const fetchMock: typeof fetch = async (request, init) => { - const url = String(request); - calls.push({ url, init }); - const team = url.includes('team-a') ? 'team-a' : 'team-b'; - if (url.endsWith('/v1/provider-registrations') && init?.method === 'POST') return json({ id: `request-${team}`, teamId: team, providerId: 'provider-shared', providerFingerprint: 'fingerprint', registrationKeyGeneration: 1, status: 'pending', capabilitySummary: [], supplyOffer: { capabilities: [] }, expiresAt: new Date(Date.now() + 60_000).toISOString(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, 201); - if (url.includes('/v1/provider-registrations/request-') && init?.method === 'GET') return json({ id: `request-${team}`, teamId: team, providerId: 'provider-shared', providerFingerprint: 'fingerprint', registrationKeyGeneration: 1, status: 'approved', membershipId: `membership-${team}`, capabilitySummary: [], supplyOffer: { capabilities: [] }, expiresAt: new Date(Date.now() + 60_000).toISOString(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); - if (url.endsWith('/v1/provider/credential-rotation')) return json({ id: `authorization-${team}-2`, membershipId: `membership-${team}`, teamId: team, providerId: 'provider-shared', generation: 2, status: 'pending', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, 201); - if (url.endsWith('/credential')) { - const generation = (credentialGenerations.get(team) ?? 0) + 1; - credentialGenerations.set(team, generation); - return json({ id: `credential-${team}-${generation}`, membershipId: `membership-${team}`, teamId: team, providerId: 'provider-shared', keyPrefix: 'tspc_test', issuanceGeneration: generation, status: 'active', scopes: ['provider:assignments:read'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), credential: `tspc_${team}_${generation}_secret` }, 201); - } - if (url.endsWith('/v1/provider/access-tokens')) { - const body = JSON.parse(String(init?.body)) as { credentialId: string }; - return json({ id: `token-${team}`, teamId: team, providerId: 'provider-shared', membershipId: `membership-${team}`, credentialId: body.credentialId, status: 'active', scopes: ['provider:assignments:read'], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(), accessToken: `tspa_${team}_token`, identityVersion: 1 }, 201); - } - return json({ error: 'unexpected request' }, 500); - }; - const loaded = await loadProviderManifest(manifestPath); - const coordinator = new CapacityProviderCoordinator(loaded, dataDirectory, { env: { TEAM_A_REGISTRATION_KEY: 'tsreg_a_secret', TEAM_B_REGISTRATION_KEY: 'tsreg_b_secret' }, fetch: fetchMock }); - const pending = await Promise.all([ - coordinator.beginJoin({ id: 'team-a', marketUrl: 'https://team-a.example.test', registrationKeyRef: 'env://TEAM_A_REGISTRATION_KEY', offer: { sharePercent: 50, maxConcurrentRunners: 1, capabilities: ['engineering'] } }), - coordinator.beginJoin({ id: 'team-b', marketUrl: 'https://team-b.example.test', registrationKeyRef: 'env://TEAM_B_REGISTRATION_KEY', offer: { sharePercent: 50, maxConcurrentRunners: 1, capabilities: ['research'] } }), - ]); - expect(pending.map((entry) => entry.status)).toEqual(['pending-approval', 'pending-approval']); - const connected = await Promise.all([coordinator.exchangeRegistrationCredential('team-a'), coordinator.exchangeRegistrationCredential('team-b')]); - expect(connected.map((entry) => entry.status)).toEqual(['connected', 'connected']); - expect(connected.map((entry) => entry.runtime?.teamId)).toEqual(['team-a', 'team-b']); - expect(new Set(connected.map((entry) => entry.runtime?.accessToken.accessToken)).size).toBe(2); - expect(calls.filter((entry) => entry.url.endsWith('/v1/provider-registrations'))).toHaveLength(2); - const durable = await loadProviderManifest(manifestPath); - expect(durable.manifest.connections).toHaveLength(2); - expect(JSON.stringify(durable.manifest)).not.toContain('registrationKeyRef'); - expect(durable.manifest.connections.map((entry) => entry.membershipCredentialRef)).toEqual(['data://secrets/team-a.credential', 'data://secrets/team-b.credential']); - const teamA = loaded.manifest.connections.find((entry) => entry.id === 'team-a'); - if (!teamA) throw new Error('Expected the approved team-a connection.'); - teamA.offer = { ...teamA.offer, capabilities: [...teamA.offer.capabilities, 'agent-execution'] }; - const rotated = await coordinator.rotateConnectionCredential('team-a'); - expect(rotated.runtime?.credentialId).toBe('credential-team-a-2'); - const rotatedManifest = await loadProviderManifest(manifestPath); - expect(rotatedManifest.manifest.connections.find((entry) => entry.id === 'team-a')?.offer.capabilities).toContain('agent-execution'); - expect(rotatedManifest.manifest.connections.find((entry) => entry.id === 'team-b')?.membershipCredentialId).toBe('credential-team-b-1'); - const reloaded = await new CapacityProviderCoordinator(await loadProviderManifest(manifestPath), dataDirectory, { fetch: fetchMock }).reconcileAll(); - expect(reloaded.map((entry) => entry.status)).toEqual(['connected', 'connected']); - expect(calls.filter((entry) => entry.url.endsWith('/v1/provider/access-tokens'))).toHaveLength(3); - const offlineReload = await new CapacityProviderCoordinator(await loadProviderManifest(manifestPath), dataDirectory, { fetch: async () => { throw new Error('control plane offline'); } }).reconcileAll(); - expect(offlineReload.map((entry) => entry.status)).toEqual(['connected', 'connected']); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('isolates a failing team connection from healthy connections', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-isolation-')); - try { - const identityPath = resolve(root, 'identity.jwk'); - await initializeCapacityProviderIdentity({ ref: `file://${identityPath}`, baseDirectory: root }); - const manifestPath = resolve(root, 'treeseed.capacity-provider.yaml'); - await writeFile(manifestPath, [ - 'schemaVersion: 2', - ...providerContract, - 'identity:', - ` privateKeyRef: file://${identityPath}`, - ' displayName: Isolated Provider', - 'executionProviders:', - ' - id: codex', - ' adapter: codex', - ' nativeLimits: { maxConcurrentRunners: 1 }', - 'connections:', - ' - id: healthy', - ' marketUrl: https://healthy.example.test', - ' teamId: healthy', - ' providerId: provider-shared', - ' membershipId: membership-healthy', - ' membershipCredentialRef: env://HEALTHY_CREDENTIAL', - ' membershipCredentialId: credential-healthy', - ' offer: { capabilities: [engineering] }', - ' - id: broken', - ' marketUrl: https://broken.example.test', - ' teamId: broken', - ' providerId: provider-shared', - ' membershipId: membership-broken', - ' membershipCredentialRef: env://MISSING_CREDENTIAL', - ' membershipCredentialId: credential-broken', - ' offer: { capabilities: [research] }', - ].join('\n')); - const fetchMock: typeof fetch = async (request) => { - const team = String(request).includes('healthy') ? 'healthy' : 'broken'; - return json({ id: `token-${team}`, teamId: team, providerId: 'provider-shared', membershipId: `membership-${team}`, credentialId: `credential-${team}`, status: 'active', scopes: ['provider:assignments:read'], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(), accessToken: `tspa_${team}_token`, identityVersion: 1 }, 201); - }; - const coordinator = new CapacityProviderCoordinator(await loadProviderManifest(manifestPath), resolve(root, 'state'), { env: { HEALTHY_CREDENTIAL: 'tspc_healthy_secret' }, fetch: fetchMock }); - const results = await coordinator.reconcileAll(); - expect(results.map((entry) => entry.status)).toEqual(['connected', 'error']); - expect(results[1].error).toContain('MISSING_CREDENTIAL'); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('removes revoked or unreachable connections and local credentials while reporting unconfirmed remote revocation', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-leave-revoked-')); - try { - const dataDirectory = resolve(root, 'state'); - await initializeCapacityProviderIdentity({ ref: 'data://identity.jwk', baseDirectory: root, dataDirectory }); - const manifestPath = resolve(root, 'treeseed.capacity-provider.yaml'); - await writeFile(manifestPath, [ - 'schemaVersion: 2', ...providerContract, 'identity:', ' privateKeyRef: data://identity.jwk', ' displayName: Revoked Provider', - 'executionProviders:', ' - id: codex', ' adapter: codex', ' nativeLimits: { maxConcurrentRunners: 1 }', - 'connections:', ' - id: revoked', ' marketUrl: https://revoked.example.test', ' teamId: team-revoked', - ' providerId: provider-revoked', ' membershipId: membership-revoked', - ' membershipCredentialRef: data://secrets/revoked.credential', ' membershipCredentialId: credential-revoked', - ' offer: { capabilities: [research] }', - ].join('\n')); - await mkdir(resolve(dataDirectory, 'connections'), { recursive: true }); - await mkdir(resolve(dataDirectory, 'secrets'), { recursive: true }); - await writeFile(resolve(dataDirectory, 'connections', 'revoked.json'), JSON.stringify({ - schemaVersion: 1, - connectionId: 'revoked', - marketUrl: 'https://revoked.example.test', - teamId: 'team-revoked', - providerId: 'provider-revoked', - membershipId: 'membership-revoked', - credentialId: 'credential-revoked', - generatedCredentialRef: 'data://secrets/revoked.credential', - registrationStatus: 'approved', - updatedAt: new Date().toISOString(), - })); - await writeFile(resolve(dataDirectory, 'secrets', 'revoked.credential'), 'revoked-secret\n'); - const coordinator = new CapacityProviderCoordinator(await loadProviderManifest(manifestPath), dataDirectory, { - fetch: async () => json({ error: 'revoked' }, 401), - }); - const result = await coordinator.leaveConnection('revoked'); - expect(result).toMatchObject({ - connectionId: 'revoked', - remoteRevocationConfirmed: false, - remoteError: expect.stringMatching(/invalid|401|credential/iu), - }); - expect((await loadProviderManifest(manifestPath)).manifest.connections).toEqual([]); - await expect(access(resolve(dataDirectory, 'connections', 'revoked.json'))).rejects.toThrow(); - await expect(access(resolve(dataDirectory, 'secrets', 'revoked.credential'))).rejects.toThrow(); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('rejects an access token cross-wired to another membership', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-token-binding-')); - try { - const identityPath = resolve(root, 'identity.jwk'); - await initializeCapacityProviderIdentity({ ref: `file://${identityPath}`, baseDirectory: root }); - const manifestPath = resolve(root, 'treeseed.capacity-provider.yaml'); - await writeFile(manifestPath, [ - 'schemaVersion: 2', ...providerContract, 'identity:', ` privateKeyRef: file://${identityPath}`, ' displayName: Bound Provider', - 'executionProviders:', ' - id: codex', ' adapter: codex', ' nativeLimits: { maxConcurrentRunners: 1 }', - 'connections:', ' - id: team-a', ' marketUrl: https://team-a.example.test', ' teamId: team-a', - ' providerId: provider-shared', ' membershipId: membership-team-a', ' membershipCredentialRef: env://TEAM_A_CREDENTIAL', - ' membershipCredentialId: credential-team-a', ' offer: { capabilities: [research] }', - ].join('\n')); - const coordinator = new CapacityProviderCoordinator(await loadProviderManifest(manifestPath), resolve(root, 'state'), { - env: { TEAM_A_CREDENTIAL: 'tspc_team_a_secret' }, - fetch: async () => json({ id: 'token-wrong', teamId: 'team-b', providerId: 'provider-other', membershipId: 'membership-team-b', credentialId: 'credential-team-a', status: 'active', scopes: ['provider:assignments:read'], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(), accessToken: 'tspa_wrong', identityVersion: 1 }, 201), - }); - await expect(coordinator.reconcileAll()).resolves.toEqual([expect.objectContaining({ status: 'error', error: expect.stringContaining('team, provider, membership, and credential binding') })]); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -}); diff --git a/tests/integration/provider/teams/multi-team-runtime.test.ts b/tests/integration/provider/teams/multi-team-runtime.test.ts deleted file mode 100644 index 841393f4..00000000 --- a/tests/integration/provider/teams/multi-team-runtime.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { resolveProviderConfig } from '../../../../src/provider/configuration/config.ts'; -import { initializeCapacityProviderIdentity } from '../../../../src/provider/accounts/identity.ts'; -import { ProviderLocalCapacityStore } from '../../../../src/provider/capacity/capacity-core/local-capacity-store.ts'; -import { runMultiTeamProviderManager, runMultiTeamProviderRunners } from '../../../../src/provider/teams/multi-team-runtime.ts'; - -afterEach(() => vi.unstubAllGlobals()); - -describe('multi-team provider runner isolation', () => { - it('continues a healthy team when another membership token is revoked', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-revocation-')); - try { - const dataDirectory = resolve(root, 'data'); - await initializeCapacityProviderIdentity({ ref: 'data://identity.jwk', baseDirectory: root, dataDirectory }); - await mkdir(resolve(dataDirectory, 'secrets'), { recursive: true }); - await writeFile(resolve(dataDirectory, 'secrets/healthy.credential'), 'healthy-secret\n', { mode: 0o600 }); - await writeFile(resolve(dataDirectory, 'secrets/revoked.credential'), 'revoked-secret\n', { mode: 0o600 }); - const manifestPath = resolve(root, 'treeseed.capacity-provider.yaml'); - await writeFile(manifestPath, [ - 'schemaVersion: 2', 'providerClass: agent', 'ownership:', ' type: external', - 'configuration:', ' generation: multi-team-runtime-test-v1', 'supplyCeilings:', ' maxConcurrentAssignments: 2', - 'identity:', ' privateKeyRef: data://identity.jwk', ' displayName: Shared Provider', - 'executionProviders:', ' - id: codex', ' adapter: codex', ' nativeLimits: { maxConcurrentRunners: 2 }', - 'connections:', - ' - id: healthy', ' marketUrl: https://healthy.test', ' teamId: healthy', ' providerId: provider-shared', ' membershipId: membership-healthy', ' membershipCredentialRef: data://secrets/healthy.credential', ' membershipCredentialId: credential-healthy', ' offer: { capabilities: [research], maxConcurrentRunners: 1 }', - ' - id: revoked', ' marketUrl: https://revoked.test', ' teamId: revoked', ' providerId: provider-shared', ' membershipId: membership-revoked', ' membershipCredentialRef: data://secrets/revoked.credential', ' membershipCredentialId: credential-revoked', ' offer: { capabilities: [research], maxConcurrentRunners: 1 }', - ].join('\n')); - const state = new ProviderLocalCapacityStore(dataDirectory); - const token = (connection: string) => ({ id: `token-${connection}`, teamId: connection, providerId: 'provider-shared', membershipId: `membership-${connection}`, credentialId: `credential-${connection}`, status: 'active' as const, scopes: ['provider:assignments:read' as const], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(), accessToken: `token-${connection}`, identityVersion: 1 }); - await state.saveToken('healthy', token('healthy')); - await state.saveToken('revoked', token('revoked')); - vi.stubGlobal('fetch', async (request: RequestInfo | URL) => String(request).includes('revoked.test') - ? new Response(JSON.stringify({ error: 'membership revoked' }), { status: 401, headers: { 'content-type': 'application/json' } }) - : new Response(JSON.stringify(String(request).includes('/availability-sessions') ? { ok: true, payload: { id: 'session-healthy', membershipId: 'membership-healthy', teamId: 'healthy', providerId: 'provider-shared', status: 'open', sequence: 1 } } : { ok: true, payload: null }), { status: 200, headers: { 'content-type': 'application/json' } })); - const config = resolveProviderConfig({ env: { TREESEED_PROVIDER_DATA_DIR: dataDirectory, TREESEED_CAPACITY_PROVIDER_MANIFEST: manifestPath, TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS: '2', HOME: root } }); - const manager = await runMultiTeamProviderManager(config); - expect(manager.connections).toEqual(expect.arrayContaining([expect.objectContaining({ ok: true }), expect.objectContaining({ ok: false, status: 'error' })])); - expect(manager.dispatches).toEqual(expect.arrayContaining([expect.objectContaining({ connectionId: 'healthy', assigned: 0 })])); - expect(manager.dispatches.every((dispatch) => dispatch.connectionId === 'healthy')).toBe(true); - expect(await state.schedulableConnections()).toEqual(['healthy']); - const revokedClaim = await state.claim({ connectionId: 'revoked', globalLimit: 2, connectionLimit: 1 }); - if (!revokedClaim) throw new Error('Expected a revoked-team test claim.'); - await state.attachLease(revokedClaim.id, { assignmentId: 'revoked-assignment', leaseToken: 'revoked-lease', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: { ok: true, payload: { id: 'revoked-assignment' }, leaseToken: 'revoked-lease' } }); - const runner = await runMultiTeamProviderRunners(config); - expect(runner).toMatchObject({ ok: true, dispatched: 0, results: [] }); - expect((await state.snapshot()).claims).toEqual([expect.objectContaining({ connectionId: 'revoked', status: 'ready' })]); - } finally { - await rm(root, { recursive: true, force: true }); - } - }, 30_000); -}); diff --git a/tests/integration/provider/teams/multi-team-service.test.ts b/tests/integration/provider/teams/multi-team-service.test.ts deleted file mode 100644 index 30698379..00000000 --- a/tests/integration/provider/teams/multi-team-service.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; -import { resolveProviderConfig } from '../../../../src/provider/configuration/config.ts'; -import { initializeCapacityProviderIdentity } from '../../../../src/provider/accounts/identity.ts'; -import { ProviderLocalCapacityStore } from '../../../../src/provider/capacity/capacity-core/local-capacity-store.ts'; -import { recoverMultiTeamProviderRunners, runMultiTeamProviderManager, runMultiTeamProviderRunners } from '../../../../src/provider/teams/multi-team-runtime.ts'; - -const cleanup: Array<() => Promise> = []; -afterEach(async () => Promise.all(cleanup.splice(0).map((entry) => entry()))); - -function json(response: ServerResponse, status: number, body: unknown) { - response.writeHead(status, { 'content-type': 'application/json' }); - response.end(JSON.stringify(body)); -} - -async function teamService(teamId: string) { - const state = { polls: 0, returns: 0, revoked: false, tokenOutage: false, assignment: null as Record | null }; - const server = createServer((request: IncomingMessage, response: ServerResponse) => { - const path = request.url ?? ''; - if (path === '/v1/provider/access-tokens') { - if (state.tokenOutage) return json(response, 503, { error: 'token service unavailable' }); - return json(response, 201, { ok: true, payload: { id: `refreshed-${teamId}`, teamId, providerId: 'provider-shared', membershipId: `membership-${teamId}`, credentialId: `credential-${teamId}`, status: 'active', scopes: ['provider:assignments:read'], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(), accessToken: `refreshed-access-${teamId}`, identityVersion: 1 } }); - } - if (state.revoked && path.includes('/availability-sessions')) return json(response, 401, { error: 'membership revoked' }); - if (path.includes('/availability-sessions')) return json(response, 200, { ok: true, payload: { id: `session-${teamId}`, membershipId: `membership-${teamId}`, teamId, providerId: 'provider-shared', status: 'open', sequence: 1 } }); - if (path === '/v1/provider/assignments/next') { - state.polls += 1; - if (!state.assignment) return json(response, 200, { ok: true, payload: null }); - const assignment = state.assignment; - state.assignment = null; - return json(response, 200, { ok: true, payload: assignment, leaseToken: `lease-${teamId}`, leaseSeconds: 300 }); - } - if (path.endsWith('/renew')) return json(response, 200, { ok: true, payload: { status: 'leased' } }); - if (path.endsWith('/mode-runs')) return json(response, 200, { ok: true, payload: {} }); - if (request.method === 'GET' && path.includes('/v1/provider/assignments/')) return json(response, 200, { ok: true, payload: { id: path.split('/').pop(), status: 'leased' } }); - if (path.endsWith('/return')) { - state.returns += 1; - return json(response, 200, { ok: true, payload: { status: 'queued' } }); - } - return json(response, 404, { error: `Unhandled ${request.method} ${path}` }); - }); - await new Promise((resolveListen) => server.listen(0, '127.0.0.1', resolveListen)); - const address = server.address(); - if (!address || typeof address === 'string') throw new Error('Expected an ephemeral TCP port.'); - cleanup.push(() => new Promise((resolveClose, rejectClose) => server.close((error) => error ? rejectClose(error) : resolveClose()))); - return { url: `http://127.0.0.1:${address.port}`, state }; -} - -describe('multi-team provider local service workflow', () => { - it('weights team polls, survives dispatch restart, and blocks revoked-team work', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-service-')); - cleanup.push(() => rm(root, { recursive: true, force: true })); - const teamA = await teamService('team-a'); - const teamB = await teamService('team-b'); - const dataDirectory = resolve(root, 'data'); - await initializeCapacityProviderIdentity({ ref: 'data://identity.jwk', baseDirectory: root, dataDirectory }); - await mkdir(resolve(dataDirectory, 'secrets'), { recursive: true }); - await writeFile(resolve(dataDirectory, 'secrets/team-a.credential'), 'team-a-secret\n', { mode: 0o600 }); - await writeFile(resolve(dataDirectory, 'secrets/team-b.credential'), 'team-b-secret\n', { mode: 0o600 }); - const manifestPath = resolve(root, 'treeseed.capacity-provider.yaml'); - await writeFile(manifestPath, [ - 'schemaVersion: 2', 'providerClass: agent', 'ownership:', ' type: external', - 'configuration:', ' generation: multi-team-service-test-v1', 'supplyCeilings:', ' maxConcurrentAssignments: 1', - 'identity:', ' privateKeyRef: data://identity.jwk', ' displayName: Service Provider', - 'executionProviders:', ' - id: codex', ' adapter: codex', ' nativeLimits: { maxConcurrentRunners: 1 }', - 'connections:', - ' - id: team-a', ` marketUrl: ${teamA.url}`, ' teamId: team-a', ' providerId: provider-shared', ' membershipId: membership-team-a', ' membershipCredentialRef: data://secrets/team-a.credential', ' membershipCredentialId: credential-team-a', ' offer: { weight: 3, maxConcurrentRunners: 1, capabilities: [research] }', - ' - id: team-b', ` marketUrl: ${teamB.url}`, ' teamId: team-b', ' providerId: provider-shared', ' membershipId: membership-team-b', ' membershipCredentialRef: data://secrets/team-b.credential', ' membershipCredentialId: credential-team-b', ' offer: { weight: 1, maxConcurrentRunners: 1, capabilities: [research] }', - ].join('\n')); - const localState = new ProviderLocalCapacityStore(dataDirectory); - const token = (teamId: string) => ({ id: `token-${teamId}`, teamId, providerId: 'provider-shared', membershipId: `membership-${teamId}`, credentialId: `credential-${teamId}`, status: 'active' as const, scopes: ['provider:assignments:read' as const], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(), accessToken: `access-${teamId}`, identityVersion: 1 }); - await localState.saveToken('team-a', token('team-a')); - await localState.saveToken('team-b', token('team-b')); - const config = resolveProviderConfig({ env: { TREESEED_PROVIDER_DATA_DIR: dataDirectory, TREESEED_CAPACITY_PROVIDER_MANIFEST: manifestPath, TREESEED_PROVIDER_MAX_CONCURRENT_RUNNERS: '1', HOME: root } }); - - for (let cycle = 0; cycle < 8; cycle += 1) await runMultiTeamProviderManager(config); - expect([teamA.state.polls, teamB.state.polls]).toEqual([6, 2]); - - teamA.state.assignment = { id: 'assignment-a', membershipId: 'membership-team-a', stateVersion: 1, projectId: 'project-a', agentId: 'researcher', mode: 'planning', executionProviderId: 'codex', decisionInput: { input: {} }, capacityEnvelope: { projectId: 'project-a', mode: 'planning', reservedCredits: 1 } }; - await runMultiTeamProviderManager(config); - expect((await new ProviderLocalCapacityStore(dataDirectory).snapshot()).claims).toEqual([expect.objectContaining({ connectionId: 'team-a', status: 'ready', assignmentId: 'assignment-a' })]); - const pollsBeforeRunner = teamA.state.polls + teamB.state.polls; - const runner = await runMultiTeamProviderRunners(config); - expect(runner).toMatchObject({ ok: true, dispatched: 1 }); - expect(teamA.state.polls + teamB.state.polls).toBe(pollsBeforeRunner); - expect(teamA.state.returns).toBe(1); - expect((await localState.snapshot()).events).toEqual(expect.arrayContaining([expect.objectContaining({ assignmentId: 'assignment-a', outcome: 'assignment-lifecycle-confirmed' })])); - - const recoveryClaim = await localState.claim({ connectionId: 'team-a', globalLimit: 1, connectionLimit: 1 }); - if (!recoveryClaim) throw new Error('Expected a restart-recovery claim fixture.'); - await localState.attachLease(recoveryClaim.id, { assignmentId: 'recovery-assignment', leaseToken: 'recovery-lease', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: {} }); - await localState.claimDispatch(['team-a']); - expect(await recoverMultiTeamProviderRunners(config)).toEqual([expect.objectContaining({ assignmentId: 'recovery-assignment', status: 'returned' })]); - expect(teamA.state.returns).toBe(2); - - await localState.saveToken('team-a', { ...token('team-a'), expiresAt: new Date(Date.now() + 4 * 60_000).toISOString() }); - teamA.state.tokenOutage = true; - await runMultiTeamProviderManager(config); - expect(await localState.schedulableConnections()).toEqual(['team-b']); - teamA.state.tokenOutage = false; - await runMultiTeamProviderManager(config); - expect(await localState.schedulableConnections()).toEqual(['team-a', 'team-b']); - - teamB.state.revoked = true; - await runMultiTeamProviderManager(config); - expect(await localState.schedulableConnections()).toEqual(['team-a']); - const revokedClaim = await localState.claim({ connectionId: 'team-b', globalLimit: 1, connectionLimit: 1 }); - if (!revokedClaim) throw new Error('Expected a revoked-team claim fixture.'); - await localState.attachLease(revokedClaim.id, { assignmentId: 'assignment-b', leaseToken: 'lease-b', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: { ok: true, payload: { id: 'assignment-b' }, leaseToken: 'lease-b' } }); - expect(await runMultiTeamProviderRunners(config)).toMatchObject({ dispatched: 0 }); - expect((await localState.snapshot()).claims).toEqual([expect.objectContaining({ connectionId: 'team-b', status: 'ready' })]); - - const pollsBeforeDiskFence = teamA.state.polls + teamB.state.polls; - const diskConstrainedConfig = { - ...config, - env: { ...config.env, TREESEED_PROVIDER_ASSIGNMENT_DISK_HEADROOM_BYTES: String(Number.MAX_SAFE_INTEGER) }, - }; - const diskConstrained = await runMultiTeamProviderManager(diskConstrainedConfig); - expect(diskConstrained).toMatchObject({ admission: { ok: false }, dispatches: [] }); - expect(teamA.state.polls + teamB.state.polls).toBe(pollsBeforeDiskFence); - }, 30_000); -}); diff --git a/tests/integration/provider/testing/connection-scheduler.test.ts b/tests/integration/provider/testing/connection-scheduler.test.ts deleted file mode 100644 index 20da115a..00000000 --- a/tests/integration/provider/testing/connection-scheduler.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ProviderGlobalSlotScheduler, providerManifestConcurrency } from '../../../../src/provider/coordination/connection-scheduler.ts'; - -function connection(id: string, weight: number, maxConcurrentRunners = 4) { - return { connection: { id, marketUrl: `https://${id}.example.test`, teamId: id, providerId: 'provider-shared', membershipId: `membership-${id}`, membershipCredentialRef: `env://${id}`, membershipCredentialId: `credential-${id}`, offer: { weight, maxConcurrentRunners, capabilities: ['engineering'] } } }; -} - -describe('provider-global connection scheduler', () => { - it('never allocates more than the provider-global or connection concurrency limits', () => { - const scheduler = new ProviderGlobalSlotScheduler(2); - scheduler.updateConnections([connection('team-a', 1, 1), connection('team-b', 1, 2)]); - const first = scheduler.acquire(); - const second = scheduler.acquire(); - expect(first).not.toBeNull(); - expect(second).not.toBeNull(); - expect(scheduler.acquire()).toBeNull(); - expect(scheduler.snapshot().activeTotal).toBe(2); - expect(new Set([first?.connection.connection.id, second?.connection.connection.id])).toEqual(new Set(['team-a', 'team-b'])); - first?.release(); - expect(scheduler.acquire()).not.toBeNull(); - }); - - it('uses weighted deficit scheduling across repeated slots', () => { - const scheduler = new ProviderGlobalSlotScheduler(1); - scheduler.updateConnections([connection('team-a', 3), connection('team-b', 1)]); - const selected: string[] = []; - for (let index = 0; index < 8; index += 1) { - const lease = scheduler.acquire(); - if (!lease) throw new Error('Expected an available provider slot.'); - selected.push(lease.connection.connection.id); - lease.release(); - } - expect(selected.filter((id) => id === 'team-a')).toHaveLength(6); - expect(selected.filter((id) => id === 'team-b')).toHaveLength(2); - }); - - it('sums independent execution-provider native runner limits', () => { - expect(providerManifestConcurrency({ executionProviders: [{ nativeLimits: { maxConcurrentRunners: 2 } }, { nativeLimits: { maxConcurrentRunners: 3 } }], hostLimit: 8 })).toBe(5); - expect(providerManifestConcurrency({ executionProviders: [{ nativeLimits: { maxConcurrentRunners: 2 } }, { nativeLimits: { maxConcurrentRunners: 3 } }], hostLimit: 3 })).toBe(3); - }); -}); diff --git a/tests/integration/provider/testing/lease-recovery.test.ts b/tests/integration/provider/testing/lease-recovery.test.ts deleted file mode 100644 index 511c6ca7..00000000 --- a/tests/integration/provider/testing/lease-recovery.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { mkdir,mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; -import { describe, expect, it, vi } from 'vitest'; -import { recoverProviderLocalLeases } from '../../../../src/provider/coordination/lease-recovery.ts'; -import { ProviderLocalCapacityStore } from '../../../../src/provider/capacity/capacity-core/local-capacity-store.ts'; -import { resolveProviderConfig } from '../../../../src/provider/configuration/config.ts'; -import type { ProviderConnectionRuntime } from '../../../../src/provider/coordination/coordinator.ts'; - -describe('provider-local lease restart recovery', () => { - function connection(): ProviderConnectionRuntime { - return { - connection: { id: 'team-a', marketUrl: 'https://team-a.test', teamId: 'team-a', providerId: 'provider-a', membershipId: 'membership-a', membershipCredentialRef: 'data://team-a', membershipCredentialId: 'credential-a', offer: { capabilities: ['research'] } }, - marketUrl: 'https://team-a.test', marketAudience: 'https://team-a.test', teamId: 'team-a', providerId: 'provider-a', membershipId: 'membership-a', credentialId: 'credential-a', - accessToken: { id: 'token-a', teamId: 'team-a', providerId: 'provider-a', membershipId: 'membership-a', credentialId: 'credential-a', status: 'active', scopes: ['provider:assignments:read'], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), accessToken: 'token-a', identityVersion: 1 }, - }; - } - - it('returns an observed active lease before releasing its durable local claim', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-lease-recovery-')); - try { - const store = new ProviderLocalCapacityStore(root); - const claim = await store.claim({ connectionId: 'team-a', globalLimit: 1, connectionLimit: 1 }); - if (!claim) throw new Error('Expected a local claim.'); - await store.attachLease(claim.id, { assignmentId: 'assignment-a', leaseToken: 'lease-a', leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), dispatchEnvelope: {} }); - await store.claimDispatch(); - const returnAssignment = vi.fn(async () => ({ ok: true })); - const config = resolveProviderConfig({ env: { TREESEED_PROVIDER_DATA_DIR: root, HOME: root } }); - const results = await recoverProviderLocalLeases({ - config, - connections: [connection()], - store, - clientFactory: () => ({ assignment: async () => ({ ok: true, payload: { id: 'assignment-a', status: 'leased' } }), returnAssignment }), - }); - expect(returnAssignment).toHaveBeenCalledWith('assignment-a', expect.objectContaining({ leaseToken: 'lease-a', runnerId: claim.runnerId, code: 'provider_restart_recovery' })); - expect(results).toEqual([expect.objectContaining({ status: 'returned', assignmentId: 'assignment-a' })]); - expect((await store.snapshot()).claims).toEqual([]); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - it('removes an orphan context only after authoritative terminal read-back', async () => { - const root = await mkdtemp(resolve(tmpdir(), 'treeseed-provider-orphan-recovery-')); - try { - const assignmentId = 'assignment-orphan'; - const contextRoot = resolve(root, 'assignment-contexts', assignmentId); - await mkdir(resolve(contextRoot, '.treeseed', 'generated'), { recursive: true }); - const config = resolveProviderConfig({ env: { TREESEED_PROVIDER_DATA_DIR: root, HOME: root } }); - const results = await recoverProviderLocalLeases({ - config, - connections: [connection()], - clientFactory: () => ({ - assignment: async () => ({ ok: true, payload: { id: assignmentId, status: 'cancelled' } }), - returnAssignment: async () => ({ ok: true }), - }), - }); - expect(results).toEqual([expect.objectContaining({ - assignmentId, status: 'released', observedStatus: 'cancelled', - materialization: { status: 'released', kind: 'context' }, - })]); - expect(await mkdir(contextRoot, { recursive: false }).then(() => true, () => false)).toBe(true); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -}); diff --git a/tests/integration/provider/testing/mode-run-telemetry.test.ts b/tests/integration/provider/testing/mode-run-telemetry.test.ts deleted file mode 100644 index 5a745e72..00000000 --- a/tests/integration/provider/testing/mode-run-telemetry.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { - deliverProviderModeRunTelemetry, - ProviderModeRunTelemetryError, - providerModeRunTelemetryId, -} from '../../../../src/provider/reporting/mode-run-telemetry.ts'; -import { createProviderMessageRecorder } from '../../../../src/provider/reporting/message-recorder.ts'; -import { loadAssignmentRawAgentSpecs } from '../../../../src/provider/capacity/assignments/assignment-agent-spec-loader.ts'; - -describe('provider mode-run telemetry delivery', () => { - it('retries one stable event identity without creating a second logical mode run', async () => { - const requests: Record[] = []; - let attempts = 0; - const recorder = { - async createAssignmentModeRun(_assignmentId: string, request: Record) { - attempts += 1; - requests.push(request); - if (attempts < 3) throw new Error('transient database failure'); - return { ok: true, payload: { id: request.id } }; - }, - }; - await expect(deliverProviderModeRunTelemetry({ - recorder, - assignmentId: 'assignment-a', - eventId: 'agent-spec-load:succeeded', - request: { status: 'running', metadata: { source: 'test' } }, - })).resolves.toMatchObject({ ok: true }); - expect(requests).toHaveLength(3); - expect(new Set(requests.map((request) => request.id))).toEqual(new Set([ - providerModeRunTelemetryId('assignment-a', 'agent-spec-load:succeeded'), - ])); - expect(requests.every((request) => (request.metadata as Record).telemetryEventId === 'agent-spec-load:succeeded')).toBe(true); - }); - - it('fails visibly after the bounded delivery budget', async () => { - const createAssignmentModeRun = vi.fn().mockRejectedValue(new Error('database unavailable')); - await expect(deliverProviderModeRunTelemetry({ - recorder: { createAssignmentModeRun }, - assignmentId: 'assignment-a', - eventId: 'required-event', - request: { status: 'running' }, - maxAttempts: 2, - })).rejects.toMatchObject({ - code: 'provider_mode_run_telemetry_delivery_failed', - retryable: true, - assignmentId: 'assignment-a', - eventId: 'required-event', - attempts: 2, - }); - expect(createAssignmentModeRun).toHaveBeenCalledTimes(2); - }); - - it('does not acknowledge or renumber a provider message until its evidence is durable', async () => { - const requests: Record[] = []; - let unavailable = true; - const recorder = createProviderMessageRecorder({ - recorder: { - async createAssignmentModeRun(_assignmentId, request) { - requests.push(request); - if (unavailable) throw new Error('database unavailable'); - return { ok: true }; - }, - }, - assignmentId: 'assignment-message', - mode: 'planning', - selectedInput: {}, - capacityEnvelope: {}, - runnerId: 'runner-a', - }); - await expect(recorder({ body: 'hello' })).rejects.toBeInstanceOf(ProviderModeRunTelemetryError); - unavailable = false; - await expect(recorder({ body: 'replacement must not overwrite pending evidence' })).resolves.toMatchObject({ - ok: true, - payload: { id: 'provider-message-assignment-message-1', body: 'hello' }, - }); - expect(new Set(requests.map((request) => request.id))).toEqual(new Set([ - providerModeRunTelemetryId('assignment-message', 'message:provider-message-assignment-message-1'), - ])); - const messages = requests.map((request) => ((request.outputs as Record).metadata as Record).message as Record); - expect(new Set(messages.map((message) => message.createdAt)).size).toBe(1); - expect(new Set(messages.map((message) => message.body))).toEqual(new Set(['hello'])); - }); - - it('requires successful spec-load telemetry before returning loaded definitions', async () => { - const createAssignmentModeRun = vi.fn().mockRejectedValue(new Error('database unavailable')); - const treeDx = { - async readWorkspaceFile() { - return { - file: { path: 'src/content/agents/researcher.mdx', content: '---\nname: Researcher\nenabled: true\n---\nResearch.' }, - }; - }, - } as never; - await expect(loadAssignmentRawAgentSpecs({ - treeDx, - assignmentId: 'assignment-spec', - agentSlug: 'researcher', - workspaceId: 'workspace-a', - contentRoot: 'src/content', - client: { createAssignmentModeRun }, - mode: 'planning', - capacityEnvelope: {}, - decisionPayload: {}, - runnerId: 'runner-a', - })).rejects.toMatchObject({ code: 'provider_mode_run_telemetry_delivery_failed' }); - expect(createAssignmentModeRun).toHaveBeenCalledTimes(3); - expect(createAssignmentModeRun.mock.calls.map(([, request]) => request.id)).toEqual([ - providerModeRunTelemetryId('assignment-spec', 'agent-spec-load:succeeded'), - providerModeRunTelemetryId('assignment-spec', 'agent-spec-load:succeeded'), - providerModeRunTelemetryId('assignment-spec', 'agent-spec-load:succeeded'), - ]); - }); - - it('loads a nested agent definition from its synchronized canonical content path', async () => { - const paths: string[] = []; - const treeDx = { - async readWorkspaceFile(input: { path: string }) { - paths.push(input.path); - return { - file: { path: input.path, content: '---\nslug: guide-steward\nhandler: writer\nenabled: true\n---\nGuide.' }, - }; - }, - } as never; - const result = await loadAssignmentRawAgentSpecs({ - treeDx, - assignmentId: 'assignment-editorial', - agentSlug: 'guide-steward', - agentContentPath: 'src/content/agents/editorial/guide-steward.mdx', - workspaceId: 'workspace-a', - contentRoot: 'src/content', - client: { createAssignmentModeRun: vi.fn().mockResolvedValue({ ok: true }) }, - mode: 'planning', - capacityEnvelope: {}, - decisionPayload: {}, - runnerId: 'runner-a', - }); - expect(paths).toEqual(['src/content/agents/editorial/guide-steward.mdx']); - expect(result).toMatchObject([{ id: 'editorial/guide-steward', frontmatter: { slug: 'guide-steward' } }]); - }); -}); diff --git a/tests/integration/services/agent-worktrees.test.ts b/tests/integration/services/agent-worktrees.test.ts deleted file mode 100644 index 74de274d..00000000 --- a/tests/integration/services/agent-worktrees.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, expect, it, vi } from 'vitest'; -import { AgentWorktreeManager, changedPathViolations } from '../../../src/services/agent-worktrees.ts'; - -describe('agent worktree path authority', () => { - it('enforces distinct tester and engineer write scopes', () => { - const testerPolicy = { allowedPaths: ['template/test/**', 'template/tests/**', 'template/spec/**', 'template/specs/**'], forbiddenPaths: ['template/src/**'] }; - expect(changedPathViolations({ ...testerPolicy, changedPaths: ['template/tests/feature.test.ts'] })).toEqual([]); - expect(changedPathViolations({ ...testerPolicy, changedPaths: ['template/src/feature.ts'] })).toEqual(['template/src/feature.ts']); - - const engineerPolicy = { allowedPaths: ['template/src/**'], forbiddenPaths: ['template/test/**', 'template/tests/**', 'template/spec/**', 'template/specs/**'] }; - expect(changedPathViolations({ ...engineerPolicy, changedPaths: ['template/src/feature.ts'] })).toEqual([]); - expect(changedPathViolations({ ...engineerPolicy, changedPaths: ['template/tests/feature.test.ts'] })).toEqual(['template/tests/feature.test.ts']); - - const writerPolicy = { allowedPaths: ['template/docs/**'], forbiddenPaths: ['template/src/**', 'template/tests/**'] }; - expect(changedPathViolations({ ...writerPolicy, changedPaths: ['template/docs/feature.md'] })).toEqual([]); - expect(changedPathViolations({ ...writerPolicy, changedPaths: ['template/src/feature.ts'] })).toEqual(['template/src/feature.ts']); - }); - - it('creates an isolated worktree from the governed exact base ref', async () => { - const repoRoot = await mkdtemp(join(tmpdir(), 'treeseed-worktree-contract-')); - const exactBaseRef = '0123456789abcdef0123456789abcdef01234567'; - const exec = vi.fn(async (_command: string, args: string[]) => ({ - stdout: args[0] === 'rev-parse' ? `${exactBaseRef}\n` : '', stderr: '', - })); - try { - const result = await new AgentWorktreeManager(repoRoot, { exec: exec as never }).createOrResumeWorktree( - 'agent/tester/assignment-1', 'assignment-1', exactBaseRef, - ); - expect(result).toMatchObject({ created: true, exactBaseRef }); - expect(exec).toHaveBeenCalledWith('git', ['worktree', 'add', '-B', 'agent/tester/assignment-1', result.worktreeRoot, exactBaseRef], expect.objectContaining({ cwd: repoRoot })); - } finally { - await rm(repoRoot, { recursive: true, force: true }); - } - }); - - it('releases only a registered non-primary assignment worktree', async () => { - const repoRoot = await mkdtemp(join(tmpdir(), 'treeseed-worktree-release-')); - const worktreeRoot = join(repoRoot, '.agent-worktrees', 'assignment-1'); - const exec = vi.fn(async (_command: string, args: string[]) => ({ - stdout: args[0] === 'worktree' && args[1] === 'list' - ? `worktree ${repoRoot}\nHEAD abc\n\nworktree ${worktreeRoot}\nHEAD def\n` - : '', - stderr: '', - })); - try { - await expect(new AgentWorktreeManager(repoRoot, { exec: exec as never }).releaseWorktree(repoRoot)) - .rejects.toThrow('cannot remove the repository root'); - await expect(new AgentWorktreeManager(repoRoot, { exec: exec as never }).releaseWorktree(worktreeRoot)) - .resolves.toMatchObject({ removed: true, reason: 'terminal_assignment' }); - expect(exec).toHaveBeenCalledWith('git', ['worktree', 'remove', '--force', worktreeRoot], expect.objectContaining({ cwd: repoRoot })); - expect(exec).toHaveBeenCalledWith('git', ['worktree', 'prune'], expect.objectContaining({ cwd: repoRoot })); - } finally { - await rm(repoRoot, { recursive: true, force: true }); - } - }); -}); diff --git a/tests/modern/provider-boundary.test.ts b/tests/modern/provider-boundary.test.ts new file mode 100644 index 00000000..293053bc --- /dev/null +++ b/tests/modern/provider-boundary.test.ts @@ -0,0 +1,36 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { CONTROL_PLANE_OPERATIONS } from '@treeseed/sdk/operator-contracts'; +import { providerOperationPath } from '../../src/provider/coordination/client.ts'; +import { resolveAgentExecutor } from '../../src/provider/execution/executor-loader.ts'; +import { resolveProviderConfig } from '../../src/provider/configuration/config.ts'; + +function sourceFiles(root: string): string[] { + return readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const path = resolve(root, entry.name); + return entry.isDirectory() ? sourceFiles(path) : entry.name.endsWith('.ts') ? [path] : []; + }); +} + +describe('Agent package ownership boundary', () => { + it('derives provider proof paths from the SDK operation catalog', () => { + expect(providerOperationPath(CONTROL_PLANE_OPERATIONS.providers.registration, { requestId: 'a/b' })) + .toContain('a%2Fb'); + expect(() => providerOperationPath(CONTROL_PLANE_OPERATIONS.providers.registration)) + .toThrow(/requires path parameter requestId/u); + }); + + it('fails closed when no trusted executor module is configured', async () => { + const config = resolveProviderConfig({ env: { TREESEED_PROVIDER_DATA_DIR: '/tmp/provider' } }); + expect(config.executorModule).toBeNull(); + await expect(resolveAgentExecutor(config, 'codex')).resolves.toBeNull(); + }); + + it('contains no raw control-plane paths or removed Market/API runtime terms', () => { + const source = sourceFiles(resolve(process.cwd(), 'src')).map((path) => readFileSync(path, 'utf8')).join('\n'); + expect(source).not.toMatch(/\/v1\//u); + expect(source).not.toMatch(/MarketClient|marketId|marketUrl|marketAudience|TREESEED_MARKET/u); + expect(source).not.toMatch(/@treeseed\/sdk\/(?:sdk|platform|operations|copilot|git-runtime|frontmatter|content-operations|agent-tools)(?:['"]|\/)/u); + }); +}); diff --git a/tests/modern/provider-runner.test.ts b/tests/modern/provider-runner.test.ts new file mode 100644 index 00000000..a7608d54 --- /dev/null +++ b/tests/modern/provider-runner.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it, vi } from 'vitest'; +import { runProviderAssignment } from '../../src/provider/operations/runner.ts'; +import type { AgentExecutor } from '../../src/provider/execution/contracts.ts'; + +function client() { + return { + startAssignmentExecution: vi.fn().mockResolvedValue({ ok: true }), + startAssignmentCloseout: vi.fn().mockResolvedValue({ ok: true }), + preflightAssignmentCompletion: vi.fn().mockResolvedValue({ ok: true }), + completeAssignment: vi.fn().mockResolvedValue({ ok: true, payload: { status: 'completed' } }), + returnAssignment: vi.fn().mockResolvedValue({ ok: true, payload: { status: 'returned' } }), + failAssignment: vi.fn().mockResolvedValue({ ok: true, payload: { status: 'failed' } }), + reportAssignmentUsage: vi.fn().mockResolvedValue({ ok: true }), + }; +} + +describe('catalog-driven provider assignment runner', () => { + it('records start, usage, closeout, preflight, and completion in order', async () => { + const api = client(); + const executor: AgentExecutor = { + id: 'fake', + observe: async () => ({ available: true }), + execute: async () => ({ + status: 'completed', + summary: 'done', + outputs: { commit: 'abc' }, + usage: [{ unit: 'agent_second', amount: 2 }], + }), + }; + await runProviderAssignment({ + client: api, + executor, + assignment: { id: 'assignment-1' }, + leaseToken: 'lease', + runnerId: 'runner', + }); + expect(api.startAssignmentExecution).toHaveBeenCalledWith('assignment-1', expect.objectContaining({ executorId: 'fake' })); + expect(api.reportAssignmentUsage).toHaveBeenCalledOnce(); + expect(api.startAssignmentCloseout).toHaveBeenCalledOnce(); + expect(api.preflightAssignmentCompletion).toHaveBeenCalledOnce(); + expect(api.completeAssignment).toHaveBeenCalledWith('assignment-1', expect.objectContaining({ + summary: { text: 'done' }, + output: { commit: 'abc', artifacts: [] }, + })); + }); + + it('turns executor exceptions into an exact retryable failure receipt', async () => { + const api = client(); + const executor: AgentExecutor = { + id: 'fake', + observe: async () => ({ available: true }), + execute: async () => { throw new Error('provider unavailable'); }, + }; + await runProviderAssignment({ + client: api, + executor, + assignment: { id: 'assignment-2' }, + leaseToken: 'lease', + runnerId: 'runner', + }); + expect(api.failAssignment).toHaveBeenCalledWith('assignment-2', expect.objectContaining({ + code: 'agent_executor_failed', + retryable: true, + })); + expect(api.completeAssignment).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/support/setup-runtime.ts b/tests/support/setup-runtime.ts deleted file mode 100644 index 4282ffda..00000000 --- a/tests/support/setup-runtime.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { resolve } from 'node:path'; -import { loadManifest } from '@treeseed/sdk/platform/tenant-config'; -import { loadDeployConfig } from '@treeseed/sdk/platform/deploy-config'; - -const fixtureRoot = resolve(process.cwd(), 'tests/fixtures/runtime-site'); -const tenantConfig = loadManifest(resolve(fixtureRoot, 'src/manifest.yaml')); -const deployConfig = loadDeployConfig(resolve(fixtureRoot, 'treeseed.site.yaml')); - -Object.defineProperties(globalThis, { - PROJECT_ROOT: { - configurable: true, - value: fixtureRoot, - }, - TENANT_CONFIG: { - configurable: true, - value: tenantConfig, - }, - SITE_CONFIG: { - configurable: true, - value: {}, - }, - DEPLOY_CONFIG: { - configurable: true, - value: deployConfig, - }, -}); diff --git a/tests/unit/agents/adapters/codex-child-pipe-guard.test.ts b/tests/unit/agents/adapters/codex-child-pipe-guard.test.ts deleted file mode 100644 index 369cb27d..00000000 --- a/tests/unit/agents/adapters/codex-child-pipe-guard.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { withCodexChildPipeGuard } from '../../../../src/agents/adapters/codex/codex-child-pipe-guard.ts'; - -describe('Codex child pipe guard', () => { - it('contains an SDK child-stdin EPIPE while the turn is active', async () => { - const error = Object.assign(new Error('write EPIPE'), { code: 'EPIPE' }); - const warning = vi.spyOn(console, 'error').mockImplementation(() => undefined); - await expect(withCodexChildPipeGuard(async () => { - process.emit('uncaughtException', error, 'uncaughtException'); - return 'continued'; - })).resolves.toBe('continued'); - expect(warning).toHaveBeenCalledWith(expect.stringContaining('codex.child_pipe_closed')); - warning.mockRestore(); - }); -}); diff --git a/tests/unit/agents/adapters/codex-stream.test.ts b/tests/unit/agents/adapters/codex-stream.test.ts deleted file mode 100644 index c762a355..00000000 --- a/tests/unit/agents/adapters/codex-stream.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { runCodexThread } from '../../../../src/agents/adapters/codex/execution-codex-stream.ts'; -import { codexEventMessage } from '../../../../src/provider/reporting/codex-event-message.ts'; - -describe('Codex streamed execution activity', () => { - it('records events in order and rebuilds the buffered Codex result', async () => { - const events = [ - { type: 'thread.started', thread_id: 'thread-a' }, - { type: 'item.started', item: { id: 'reason-a', type: 'reasoning', text: 'Inspecting evidence.' } }, - { type: 'item.completed', item: { id: 'message-a', type: 'agent_message', text: 'Created the review note.' } }, - { type: 'turn.completed', usage: { input_tokens: 10, cached_input_tokens: 2, output_tokens: 4, reasoning_output_tokens: 1 } }, - ]; - const recorded: Record[] = []; - const result = await runCodexThread({ - async run() { throw new Error('buffered execution should not be used'); }, - async runStreamed() { - return { events: (async function* () { for (const event of events) yield event; })() }; - }, - }, 'Review the Guide.', async (event) => { recorded.push(event); }); - expect(recorded).toEqual(events); - expect(result).toMatchObject({ - finalResponse: 'Created the review note.', - usage: { input_tokens: 10, output_tokens: 4 }, - items: [ - { id: 'reason-a', type: 'reasoning', text: 'Inspecting evidence.' }, - { id: 'message-a', type: 'agent_message', text: 'Created the review note.' }, - ], - }); - }); - - it('treats turn.completed as terminal without waiting for stream teardown', async () => { - let released = false; - const result = await Promise.race([ - runCodexThread({ - async run() { throw new Error('buffered execution should not be used'); }, - async runStreamed() { return { events: (async function* () { - try { - yield { type: 'item.completed', item: { id: 'message-a', type: 'agent_message', text: 'Durable work completed.' } }; - yield { type: 'turn.completed', usage: { output_tokens: 8 } }; - await new Promise(() => undefined); - } finally { released = true; } - })() }; }, - }, 'Complete the bounded assignment.', vi.fn()), - new Promise((_, reject) => setTimeout(() => reject(new Error('completed turn did not return')), 100)), - ]); - expect(result).toMatchObject({ finalResponse: 'Durable work completed.', usage: { output_tokens: 8 } }); - expect(released).toBe(true); - }); - - it('fails closed when durable event recording fails', async () => { - await expect(runCodexThread({ - async run() { return {}; }, - async runStreamed() { - return { events: (async function* () { yield { type: 'turn.started' }; })() }; - }, - }, 'Review the Guide.', vi.fn(async () => { throw new Error('telemetry unavailable'); }))) - .rejects.toThrow('telemetry unavailable'); - }); - - it('redacts credentials before events and buffered trace items leave the provider', async () => { - const recorded: Record[] = []; - const result = await runCodexThread({ - async run() { return {}; }, - async runStreamed() { - return { events: (async function* () { - yield { type: 'item.completed', item: { id: 'command-a', type: 'command_execution', command: 'curl -H "Authorization: Bearer secret-token-value"', accessToken: 'tsk_secret_value' } }; - })() }; - }, - }, 'Review the Guide.', async (event) => { recorded.push(event); }); - expect(JSON.stringify({ recorded, result })).not.toContain('secret-token-value'); - expect(JSON.stringify({ recorded, result })).not.toContain('tsk_secret_value'); - expect(JSON.stringify({ recorded, result })).toContain(''); - }); - - it('preserves full agent text while bounding command activity to safe fields', () => { - expect(codexEventMessage({ type: 'item.updated', item: { id: 'message-a', type: 'agent_message', text: 'Full progress message.' } })) - .toMatchObject({ payload: { eventType: 'item.updated', item: { text: 'Full progress message.' } } }); - expect(codexEventMessage({ type: 'item.completed', item: { id: 'command-a', type: 'command_execution', command: 'npm test', aggregated_output: 'sensitive output', status: 'completed', exit_code: 0 } })) - .toMatchObject({ payload: { item: { command: 'npm test', status: 'completed', exitCode: 0 } } }); - expect(JSON.stringify(codexEventMessage({ type: 'item.completed', item: { id: 'command-a', type: 'command_execution', command: 'npm test', aggregated_output: 'sensitive output' } }))) - .not.toContain('sensitive output'); - }); - - it('retains sanitized MCP arguments and results for forensic provider messages', () => { - const message = codexEventMessage({ type: 'item.completed', item: { - id: 'tool-a', type: 'mcp_tool_call', server: 'treeseed_tools', tool: 'treeseed_content_query', - arguments: { model: 'note', accessToken: 'tsk_secret_value' }, result: { items: [{ id: 'note:one' }] }, status: 'completed', - } }); - expect(message).toMatchObject({ payload: { item: { arguments: { model: 'note', accessToken: '' }, result: { items: [{ id: 'note:one' }] } } } }); - }); -}); diff --git a/tests/unit/agents/adapters/content-receipts.test.ts b/tests/unit/agents/adapters/content-receipts.test.ts deleted file mode 100644 index 497b83d7..00000000 --- a/tests/unit/agents/adapters/content-receipts.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { treeDxContentReceipts } from '../../../../src/agents/adapters/codex/execution-codex-receipts.ts'; -import { deriveToolEvents } from '../../../../src/agents/tools/agent-tool-telemetry.ts'; - -function completed(toolId: string, derivedEvents: Record[]) { - return { toolId, status: 'completed', derivedEvents }; -} - -describe('TreeDX content receipts', () => { - it('emits a durable receipt for an update-only content revision', () => { - const receipts = treeDxContentReceipts([ - completed('treeseed.content.update', [{ - type: 'content_updated', - contentRef: { - id: 'proposal:governed-workday', - model: 'proposal', - path: 'src/content/proposals/governed-workday.mdx', - subjectId: 'objective:governed-workday', - subjectField: 'relatedObjectives', - }, - }]), - completed('treeseed.content.commit', [{ - type: 'content_committed', - commitSha: '712a574e96bbf76e5997a6dba6309de9188faf15', - branchRef: 'refs/heads/assignment-revision', - }]), - ]); - - expect(receipts).toEqual([expect.objectContaining({ - kind: 'treedx_content_receipt', - uri: 'treedx://src/content/proposals/governed-workday.mdx', - metadata: expect.objectContaining({ - toolId: 'treeseed.content.update', - contentRef: expect.objectContaining({ - model: 'proposal', - commitSha: '712a574e96bbf76e5997a6dba6309de9188faf15', - ref: 'refs/heads/assignment-revision', - }), - }), - })]); - }); - - it('deduplicates create and update events and retains the latest relation fields', () => { - const receipts = treeDxContentReceipts([ - completed('treeseed.content.create', [{ - type: 'content_created', - contentRef: { model: 'note', path: 'src/content/notes/review.mdx' }, - }]), - completed('treeseed.content.link', [{ - type: 'content_updated', - contentRef: { - model: 'note', path: 'src/content/notes/review.mdx', - subjectId: 'proposal:change', subjectField: 'relatedProposals', - }, - }]), - completed('treeseed.content.commit', [{ type: 'content_committed', commitSha: 'abc123' }]), - ]); - - expect(receipts).toHaveLength(1); - expect(receipts[0]?.metadata).toMatchObject({ - contentRef: { - model: 'note', - subjectId: 'proposal:change', - subjectField: 'relatedProposals', - commitSha: 'abc123', - }, - }); - }); - - it('does not represent uncommitted workspace mutations as durable artifacts', () => { - expect(treeDxContentReceipts([ - completed('treeseed.content.update', [{ - type: 'content_updated', - contentRef: { model: 'proposal', path: 'src/content/proposals/change.mdx' }, - }]), - ])).toEqual([]); - }); - - it('projects an authoritative Discussion response as its own committed content receipt',()=>{ - const commitSha='d'.repeat(40);const path='src/content/discussion-messages/discussion-a/response-a.mdx'; - const derivedEvents=deriveToolEvents('treeseed.discussion.respond',{id:'treeseed.discussion.respond',kind:'agent_tool',executionTarget:'provider_runner',mutability:'content_write'} as never, - {replyTo:'message-a'},{ok:true,payload:{message:{id:'response-a',path},commitSha,changeset:{baseCommitSha:'c'.repeat(40),resultCommitSha:commitSha,changedPaths:[path]}}}); - expect(derivedEvents).toEqual([expect.objectContaining({type:'content_created',contentRef:expect.objectContaining({model:'discussion_message',artifactKind:'discussion_response',path,commitSha,subjectId:'message-a'})})]); - expect(treeDxContentReceipts([completed('treeseed.discussion.respond',derivedEvents)])).toEqual([expect.objectContaining({ - kind:'treedx_content_receipt',uri:`treedx://${path}`,metadata:expect.objectContaining({contentRef:expect.objectContaining({artifactKind:'discussion_response',commitSha})}), - })]); - }); -}); diff --git a/tests/unit/agents/adapters/opencode.test.ts b/tests/unit/agents/adapters/opencode.test.ts deleted file mode 100644 index 3031a127..00000000 --- a/tests/unit/agents/adapters/opencode.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { OpenCodeExecutionProviderAdapter } from '../../../../src/agents/adapters/opencode/execution-opencode.ts'; - -describe('OpenCode execution provider', () => { - it('uses headless sessions, records authoritative token/cost classes, and aborts without serializing brokered credentials', async () => { - const requests: Array<{ url: string; init?: RequestInit }> = []; - const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { - requests.push({ url: String(url), init }); - if (String(url).endsWith('/global/health')) return Response.json({ healthy: true }); - if (String(url).endsWith('/session') && init?.method === 'POST') return Response.json({ id: 'session-1' }); - if (String(url).endsWith('/message')) return Response.json({ parts: [{ type: 'text', text: 'Complete.' }], info: { tokens: { input: 120, output: 30, reasoning: 10, cache: { read: 20 } }, cost: 0.004 } }); - if (String(url).endsWith('/abort')) return Response.json({ ok: true }); - return Response.json({ files: [] }); - }) as typeof fetch; - const adapter = new OpenCodeExecutionProviderAdapter({ fetchImpl, env: { OPENROUTER_API_KEY: 'brokered-secret', TREESEED_OPENCODE_SERVER_URL: 'http://127.0.0.1:4096' } }); - const result = await adapter.start({ assignment: { id: 'assignment-1' }, agent: { systemPrompt: 'System', cli: {}, execution: {} }, workPackage: { instructions: 'Do the work.' }, capacityEnvelope: { budget: { deadline: '2026-08-06T12:00:00Z' } } } as never); - expect(result).toMatchObject({ status: 'completed', runId: 'session-1', summary: 'Complete.' }); - expect(result.usage).toEqual(expect.arrayContaining([ - expect.objectContaining({ kind: 'input_tokens', amount: 120 }), - expect.objectContaining({ kind: 'reasoning_tokens', amount: 10 }), - expect.objectContaining({ kind: 'provider_cost', amount: 0.004 }), - ])); - await adapter.cancel({ assignmentId: 'assignment-1', runId: 'session-1', reason: 'deadline' }); - expect(requests.some((request) => request.url.endsWith('/session/session-1/abort'))).toBe(true); - expect(JSON.stringify(requests)).not.toContain('brokered-secret'); - }); -}); diff --git a/tests/unit/agents/agent-authoring-diagnostics.test.ts b/tests/unit/agents/agent-authoring-diagnostics.test.ts deleted file mode 100644 index 1f5e641d..00000000 --- a/tests/unit/agents/agent-authoring-diagnostics.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { diagnoseAgentAuthoring } from '../../../src/agents/testing/agent-authoring-diagnostics.ts'; - -describe('agent authoring diagnostics', () => { - it('accepts a generic, classed, capability-scoped agent', () => { - const diagnostics = diagnoseAgentAuthoring({ - slug: 'docs-engineer', - projectAgentClassId: 'implementation', - activityProfiles: { - acting: { - handler: 'actor', - contentAccess: { readModels: ['decision', 'note'] }, - context: { queries: [{ kind: 'treedx', paths: ['src/content/**'] }] }, - execution: { - allowedPaths: ['src/content/**'], - forbiddenPaths: ['src/content/private/**'], - requiredCapabilities: ['files:write', 'treedx_workspace'], - }, - outputs: { content: ['implementation_report'] }, - }, - }, - }); - expect(diagnostics.filter((diagnostic) => diagnostic.severity === 'error')).toEqual([]); - }); - - it('flags old handlers and incomplete profile contracts', () => { - const diagnostics = diagnoseAgentAuthoring({ - slug: 'legacy-engineer', - activityProfiles: { - acting: { - handler: 'engineer', - context: {}, - execution: {}, - outputs: {}, - }, - }, - }); - expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain('agent.handler_not_profile_handler'); - expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain('agent.missing_project_agent_class'); - expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain('agent.missing_context_queries'); - expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain('agent.missing_required_capabilities'); - expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain('agent.output_contract_ambiguous'); - }); - - it('rejects mutation output contracts on read-only profiles', () => { - const diagnostics = diagnoseAgentAuthoring({ - slug: 'read-only-writer', - projectAgentClassId: 'documentation', - activityProfiles: { - planning: { - handler: 'writer', - branchPolicy: { kind: 'read-only', base: 'main' }, - contentAccess: { - readModels: ['objective'], - commit: { allowed: false }, - }, - tools: { allowed: ['treeseed.content.read'] }, - execution: { requiredCapabilities: ['treedx_workspace'] }, - outputs: { modelMutations: ['linked_note:create'] }, - }, - }, - }); - expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain('agent.mutation_output_requires_content_commit'); - }); -}); diff --git a/tests/unit/agents/agent-tool-schema.test.ts b/tests/unit/agents/agent-tool-schema.test.ts deleted file mode 100644 index b39aa2f6..00000000 --- a/tests/unit/agents/agent-tool-schema.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { validateAgentToolInput } from '../../../src/agents/tools/agent-tool-schema.ts'; - -const schema = { - type: 'object', - properties: { - query: { type: 'string', minLength: 3, maxLength: 10 }, - maxResults: { type: 'integer', minimum: 1, maximum: 20 }, - }, - required: ['query'], - additionalProperties: false, -}; - -describe('agent tool JSON schema validation', () => { - it('accepts bounded integers declared by canonical tool descriptors', () => { - expect(validateAgentToolInput(schema, { query: 'capacity', maxResults: 5 })).toEqual({ ok: true }); - }); - - it('rejects fractional and out-of-range integers', () => { - expect(validateAgentToolInput(schema, { query: 'capacity', maxResults: 1.5 })).toMatchObject({ ok: false, code: 'invalid_tool_input' }); - expect(validateAgentToolInput(schema, { query: 'capacity', maxResults: 21 })).toMatchObject({ ok: false, code: 'invalid_tool_input' }); - }); - - it('enforces declared string bounds', () => { - expect(validateAgentToolInput(schema, { query: 'no' })).toMatchObject({ ok: false, code: 'invalid_tool_input' }); - expect(validateAgentToolInput(schema, { query: 'far-too-long' })).toMatchObject({ ok: false, code: 'invalid_tool_input' }); - }); -}); diff --git a/tests/unit/agents/artifact-manifest.test.ts b/tests/unit/agents/artifact-manifest.test.ts deleted file mode 100644 index 44363d61..00000000 --- a/tests/unit/agents/artifact-manifest.test.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { ProviderAssignment } from '@treeseed/sdk/agent-capacity'; -import { buildAgentArtifactManifest, validateAgentArtifactManifest } from '../../../src/agents/kernel/artifacts/artifact-manifest.ts'; -import { recordAssignmentModeRun } from '../../../src/agents/kernel/telemetry/telemetry.ts'; - -function assignment(): ProviderAssignment { - return { - id: 'assignment-1', - teamId: 'team-1', - projectId: 'project-1', - capacityProviderId: 'provider-1', - executionProviderId: 'codex', - projectAgentClassId: 'researcher', - workDayId: 'workday-1', - mode: 'planning', - status: 'leased', - leaseState: 'leased', - capacityEnvelope: { - teamId: 'team-1', - projectId: 'project-1', - projectAgentClassId: 'researcher', - capacityProviderId: 'provider-1', - mode: 'planning', - reservedSeconds: 3, - }, - decisionInput: { input: {} }, - }; -} - -describe('AgentArtifactManifest', () => { - it('persists kernel lifecycle telemetry under the artifact manifest mode-run identity', async () => { - const writes: Array> = []; - const options = { - assignment: assignment(), - modeRunId: 'mode-run-authority', - recordModeRun: async (run: Record) => { - writes.push(run); - return run; - }, - }; - await recordAssignmentModeRun(options, { - status: 'running', - selectedInput: {}, - capacityEnvelope: assignment().capacityEnvelope, - }); - await recordAssignmentModeRun(options, { - status: 'succeeded', - selectedInput: {}, - capacityEnvelope: assignment().capacityEnvelope, - outputs: { artifactManifest: { modeRunId: 'mode-run-authority' } }, - }); - expect(writes.map((entry) => entry.id)).toEqual(['mode-run-authority', 'mode-run-authority']); - }); - - it('retries the same durable mode-run identity after an interrupted response', async () => { - const writes: Array> = []; - await recordAssignmentModeRun({ - assignment: assignment(), - modeRunId: 'mode-run-retry', - recordModeRun: async (run: Record) => { - writes.push(run); - if (writes.length === 1) throw new TypeError('fetch failed'); - return run; - }, - }, { - status: 'succeeded', - selectedInput: {}, - capacityEnvelope: assignment().capacityEnvelope, - }); - expect(writes).toHaveLength(2); - expect(writes.map((entry) => entry.id)).toEqual(['mode-run-retry', 'mode-run-retry']); - }); - - it('normalizes durable content, code, citations, verification, usage, and tool evidence', () => { - const governedAssignment = assignment(); - governedAssignment.treedxProxyHandle = { baseCommitSha: 'base-content-ref-123' }; - const manifest = buildAgentArtifactManifest({ - assignment: governedAssignment, - modeRunId: 'mode-run-1', - runnerId: 'runner-1', - agentId: 'researcher', - handlerId: 'execution-content', - activityType: 'research', - status: 'completed', - createdAt: '2026-07-16T20:00:00.000Z', - output: { - status: 'completed', - summary: 'Completed cited research and implementation.', - metadata: { - providerToken: 'must-never-be-copied', - classifiedContentReferences: [{ - model: 'note', - contentPath: 'notes/research/result.mdx', - subjectId: 'question-1', - subjectField: 'relatedQuestions', - artifactKind: 'failing_test_proof', - producedByAgent: 'researcher', - }], - executionSnapshot: { - status: 'completed', - summary: 'done', - metadata: { baseRef: 'base-source-ref-456' }, - outputs: { - citations: [{ - sourceUrl: 'https://example.test/paper?token=secret', - title: 'Paper', - retrievedAt: '2026-07-16T19:00:00.000Z', - contentHash: 'sha256:paper', - claimIds: ['claim-1'], - confidence: 'high', - }], - toolTelemetry: [ - { - toolId: 'treedx.write', - status: 'completed', - derivedEvents: [{ type: 'content_created' }], - }, - { - toolId: 'treeseed.checkpoint', status: 'completed', - derivedEvents: [{ type: 'source_checkpoint_committed', commitSha: 'abc123', branchRef: 'agent/tester/assignment-1', changedPaths: ['src/result.ts'] }], - }, - { - toolId: 'treeseed.verify', status: 'completed', - derivedEvents: [{ - type: 'verification_completed', - status: 'passed', - summary: 'Tests passed.', - commands: ['npm test'], - }], - }, - { - toolId: 'treeseed.review_decision', status: 'completed', - derivedEvents: [{ - type: 'review_decision_recorded', - disposition: 'approved', - summary: 'Evidence passed review.', - }], - }, - { operation: 'test', status: 'failed', error: 'failed' }, - ], - }, - usage: [{ kind: 'tokens', unit: 'token', amount: 42, source: 'codex' }], - artifacts: [ - { kind: 'changed_path', name: 'src/result.ts', uri: 'repo://src/result.ts' }, - { - kind: 'treedx_content_receipt', - metadata: { - toolId: 'treedx.write', - contentRef: { - model: 'note', - path: 'notes/research/result.mdx', - subjectId: 'question-1', - subjectField: 'relatedQuestions', - }, - }, - }, - ], - }, - }, - }, - }); - - expect(manifest).toMatchObject({ - schemaVersion: 1, - assignmentId: 'assignment-1', - modeRunId: 'mode-run-1', - agentClassId: 'researcher', - activityType: 'research', - contentReferences: [{ model: 'note', contentPath: 'notes/research/result.mdx', subjectId: 'question-1', artifactKind: 'failing_test_proof', producedByAgent: 'researcher' }], - sourceWorktree: { changedPaths: ['src/result.ts'] }, - commit: { sha: 'abc123', ref: 'agent/tester/assignment-1' }, - verification: [{ status: 'passed', summary: 'Tests passed.' }], - signals: [{ code: 'review_approved', severity: 'info' }], - toolEvents: [{ toolId: 'treedx.write', status: 'completed' }, { toolId: 'treeseed.checkpoint', status: 'completed' }, { toolId: 'treeseed.verify', status: 'completed' }, { toolId: 'treeseed.review_decision', status: 'completed' }, { status: 'failed' }], - usage: [{ kind: 'tokens', unit: 'token', amount: 42 }], - }); - expect(manifest.citations[0]?.sourceUrl).toContain('token=%5Bredacted%5D'); - expect(manifest.mutationReceipts).toEqual(expect.arrayContaining([ - expect.objectContaining({ kind: 'source-checkpoint', phase: 'provisional', baseRef: 'base-source-ref-456', effectiveRef: 'abc123' }), - ])); - expect(JSON.stringify(manifest)).not.toContain('must-never-be-copied'); - expect(validateAgentArtifactManifest(manifest)).toEqual({ ok: true }); - expect(validateAgentArtifactManifest(manifest, { publishedSignals: ['review-approved'] })).toEqual({ ok: true }); - expect(validateAgentArtifactManifest(manifest, { publishedSignals: ['planning-proposal'] })).toMatchObject({ - ok: false, reason: expect.stringContaining('signal:planning-proposal'), - }); - }); - - it('rejects a completed execution with no durable evidence', () => { - const manifest = buildAgentArtifactManifest({ - assignment: assignment(), - modeRunId: 'mode-run-1', - agentId: 'researcher', - handlerId: 'execution-content', - activityType: 'research', - status: 'completed', - output: { - status: 'completed', - summary: 'Only a transient response.', - metadata: { - classifiedContentReferences: [{ model: 'note', contentPath: 'notes/fabricated.mdx', subjectId: 'question-1' }], - executionSnapshot: { status: 'completed', summary: 'done', artifacts: [{ kind: 'assistant_final_response' }] }, - }, - }, - }); - expect(manifest.contentReferences).toEqual([]); - expect(validateAgentArtifactManifest(manifest)).toMatchObject({ ok: false }); - }); - - it('projects governed fetch, claim, and research-review tool receipts into workflow evidence', () => { - const researchAssignment = assignment(); - researchAssignment.decisionInput = { input: { researchStage: 'citation-review-rejection' } }; - const manifest = buildAgentArtifactManifest({ - assignment: researchAssignment, - modeRunId: 'mode-run-research', - agentId: 'reviewer', - handlerId: 'writer', - activityType: 'planning', - status: 'completed', - output: { - status: 'completed', - summary: 'Recorded governed research evidence.', - metadata: { - executionSnapshot: { - status: 'completed', - summary: 'done', - outputs: { - toolTelemetry: [ - { status: 'completed', derivedEvents: [{ type: 'research_citation_fetched', citation: { - sourceUrl: 'https://example.com/', title: 'Example', publisher: 'example.com', - retrievedAt: '2026-07-21T00:00:00.000Z', contentHash: 'sha256:example', - claimIds: ['claim-1'], confidence: 'medium', - } }] }, - { status: 'completed', derivedEvents: [{ type: 'research_claims_recorded', claims: [{ - id: 'claim-1', text: 'Material claim', material: true, status: 'unsupported', citationIds: [], - }] }] }, - { toolId: 'treeseed.review_decision', status: 'completed', derivedEvents: [{ - type: 'review_decision_recorded', disposition: 'rejected', summary: 'Unsupported material claim.', - }] }, - ], - }, - }, - }, - }, - }); - expect(manifest.citations).toEqual([expect.objectContaining({ sourceUrl: 'https://example.com/', claimIds: ['claim-1'] })]); - expect(manifest.signals).toEqual(expect.arrayContaining([ - expect.objectContaining({ code: 'research_claim', metadata: expect.objectContaining({ id: 'claim-1', status: 'unsupported' }) }), - expect.objectContaining({ code: 'research_review_rejected' }), - ])); - }); - - it('accepts a validated structured estimate as a durable control-plane output', () => { - const manifest = buildAgentArtifactManifest({ - assignment: assignment(), - modeRunId: 'mode-run-estimate', - agentId: 'engineer', - handlerId: 'estimate', - activityType: 'estimating', - status: 'completed', - output: { - status: 'completed', - summary: 'Estimated the linked proposal.', - metadata: { - structuredEstimate: { - id: 'estimate-assignment-1', - decisionId: 'decision-1', - proposalId: 'proposal-1', - agentId: 'engineer', - }, - estimateValidation: { ok: true }, - executionSnapshot: { status: 'completed', summary: 'done' }, - }, - }, - }); - expect(manifest.controlPlaneReferences).toEqual([expect.objectContaining({ - kind: 'structured_agent_estimate', id: 'estimate-assignment-1', status: 'submitted', - })]); - expect(validateAgentArtifactManifest(manifest)).toEqual({ ok: true }); - }); - - it('does not project a TreeDX content commit as source-repository authority', () => { - const manifest = buildAgentArtifactManifest({ - assignment: assignment(), modeRunId: 'mode-run-content', agentId: 'researcher', - handlerId: 'writer', activityType: 'acting', status: 'completed', - output: { status: 'completed', summary: 'Created content evidence.', metadata: { - classifiedContentReferences: [{ model: 'note', contentPath: 'notes/research/evidence.mdx', subjectId: 'decision-1', subjectField: 'relatedDecisions' }], - executionSnapshot: { - status: 'completed', summary: 'done', outputs: { toolTelemetry: [{ toolId: 'treeseed.content.commit', status: 'completed', derivedEvents: [{ type: 'content_created' }] }] }, - artifacts: [{ kind: 'treedx_content_receipt', metadata: { toolId: 'treeseed.content.commit', contentRef: { model: 'note', path: 'notes/research/evidence.mdx', subjectId: 'decision-1', subjectField: 'relatedDecisions', commitSha: 'abcdef1234567890', ref: 'refs/heads/assignment' } } }], - }, - } }, - }); - expect(manifest.contentReferences[0]).toMatchObject({ commitSha: 'abcdef1234567890' }); - expect(manifest.commit).toBeUndefined(); - }); - - it('selects the final authenticated source checkpoint as downstream authority', () => { - const manifest = buildAgentArtifactManifest({ - assignment: assignment(), modeRunId: 'mode-run-checkpoints', agentId: 'engineer', - handlerId: 'actor', activityType: 'acting', status: 'completed', - output: { status: 'completed', summary: 'Implemented and checkpointed the change.', metadata: { - executionSnapshot: { - status: 'completed', summary: 'done', - outputs: { toolTelemetry: [ - { toolId: 'treeseed.checkpoint', status: 'completed', derivedEvents: [{ type: 'source_checkpoint_committed', commitSha: '1111111111111111', branchRef: 'agent/engineer/work' }] }, - { toolId: 'treeseed.checkpoint', status: 'completed', derivedEvents: [{ type: 'source_checkpoint_committed', commitSha: '2222222222222222', branchRef: 'agent/engineer/work' }] }, - ] }, - artifacts: [{ kind: 'changed_path', name: 'src/result.ts', uri: 'repo://src/result.ts' }], - }, - } }, - }); - expect(manifest.commit).toEqual({ sha: '2222222222222222', ref: 'agent/engineer/work' }); - }); - - it('fails closed instead of dropping a malformed declared citation', () => { - expect(() => buildAgentArtifactManifest({ - assignment: assignment(), - modeRunId: 'mode-run-1', - agentId: 'researcher', - handlerId: 'execution-content', - activityType: 'research', - status: 'completed', - output: { - status: 'completed', - summary: 'Malformed citation.', - metadata: { executionSnapshot: { status: 'completed', summary: 'done', outputs: { citations: [{ sourceUrl: 'not-a-url' }] } } }, - }, - })).toThrow(/research_citation_url_invalid/u); - }); - - it('rejects a TreeDX note receipt without validated subject-link evidence', () => { - const telemetry = { toolId: 'treedx.write', status: 'completed', derivedEvents: [{ type: 'content_created' }] }; - const manifest = buildAgentArtifactManifest({ - assignment: assignment(), - modeRunId: 'mode-run-1', - agentId: 'researcher', - handlerId: 'execution-content', - activityType: 'research', - status: 'completed', - output: { - status: 'completed', - summary: 'Unlinked note.', - metadata: { executionSnapshot: { - status: 'completed', - summary: 'done', - outputs: { toolTelemetry: [telemetry] }, - artifacts: [{ kind: 'treedx_content_receipt', metadata: { toolId: 'treedx.write', telemetry, contentRef: { model: 'note', path: 'notes/unlinked.mdx' } } }], - } }, - }, - }); - expect(validateAgentArtifactManifest(manifest)).toMatchObject({ ok: false, reason: expect.stringContaining('subject link') }); - }); -}); diff --git a/tests/unit/agents/content-artifacts.test.ts b/tests/unit/agents/content-artifacts.test.ts deleted file mode 100644 index 899609cb..00000000 --- a/tests/unit/agents/content-artifacts.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { mkdtempSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; -import { assertRelativeContentPath } from '../../../src/agents/content/content-artifacts.ts'; -import { LocalBranchMutationAdapter } from '../../../src/agents/adapters/tools/mutations.ts'; - -const tempRoots: string[] = []; - -afterEach(() => { - for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true }); -}); - -describe('content artifact ownership', () => { - it('accepts only canonical Knowledge Hub paths for TreeDX-backed artifact references', () => { - const root = mkdtempSync(join(tmpdir(), 'treeseed-agent-content-')); - tempRoots.push(root); - expect(assertRelativeContentPath(root, 'src/content/notes/review.mdx')).toContain('src/content/notes/review.mdx'); - expect(() => assertRelativeContentPath(root, 'README.md')).toThrow(/Knowledge Hub content root/u); - expect(() => assertRelativeContentPath(root, '../outside.mdx')).toThrow(/escapes the repository/u); - }); - - it('forbids local worktree adapters from writing Knowledge Hub content', async () => { - const root = mkdtempSync(join(tmpdir(), 'treeseed-agent-content-')); - tempRoots.push(root); - const adapter = new LocalBranchMutationAdapter(root); - await expect(adapter.writeArtifact({ - runId: 'run-1', - agent: { slug: 'reviewer', execution: { branchPrefix: 'agent/reviewer' } } as never, - relativePath: 'src/content/notes/review.mdx', - content: '# forbidden', - commitMessage: 'forbidden content write', - })).rejects.toThrow(/TreeDX tool receipt/u); - }); -}); diff --git a/tests/unit/agents/context-processor.test.ts b/tests/unit/agents/context-processor.test.ts deleted file mode 100644 index 6933babf..00000000 --- a/tests/unit/agents/context-processor.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { resolveHandlerContextPacks } from '../../../src/agents/context/context-processor.ts'; - -function mockPack(id: string) { - return { - seedIds: [id], - totalTokenEstimate: 10, - includedNodeIds: [`node:${id}`], - nodes: [], - edges: [], - }; -} - -describe('handler context processor', () => { - it('collects context queries and resolves graph context packs', async () => { - const buildContextPack = vi.fn(async (request) => mockPack(String(request.query))); - const result = await resolveHandlerContextPacks({ - sdk: { buildContextPack }, - agent: { - slug: 'researcher', - handler: 'writer', - enabled: true, - systemPrompt: 'Research carefully.', - persona: 'Researcher', - cli: {}, - triggers: [], - permissions: [], - execution: { - maxConcurrency: 1, - timeoutSeconds: 900, - cooldownSeconds: 0, - leaseSeconds: 300, - retryLimit: 0, - branchPrefix: 'agent', - }, - outputs: { messageTypes: [], modelMutations: [] }, - context: { - queries: [{ - id: 'runtime', - purpose: 'research', - query: 'agent runtime', - scope: '/knowledge', - }], - }, - }, - }); - - expect(buildContextPack).toHaveBeenCalledWith(expect.objectContaining({ - query: 'agent runtime', - stage: 'research', - scopePaths: ['/knowledge'], - })); - expect(result.contextPacks.get('runtime')?.pack.includedNodeIds).toEqual(['node:agent runtime']); - expect(result.contextPacks.byPurpose('research')).toHaveLength(1); - }); - - it('merges duplicate ids by source priority', async () => { - const buildContextPack = vi.fn(async (request) => mockPack(String(request.query))); - const result = await resolveHandlerContextPacks({ - sdk: { buildContextPack }, - defaultRoleContext: [{ - id: 'shared', - purpose: 'research', - query: 'default query', - }], - contentRecords: [{ - ref: 'knowledge/runtime', - context: { - queries: [{ - id: 'shared', - purpose: 'research', - query: 'content query', - }], - }, - }], - taskPayload: { - context: { - queries: [{ - id: 'shared', - purpose: 'review', - query: 'task query', - }], - }, - }, - }); - - expect(buildContextPack).toHaveBeenCalledTimes(1); - expect(buildContextPack).toHaveBeenCalledWith(expect.objectContaining({ - query: 'task query', - stage: 'review', - })); - expect(result.contextPacks.get('shared')?.source).toBe('task_payload'); - }); - - it('skips invalid optional queries and propagates warnings', async () => { - const result = await resolveHandlerContextPacks({ - sdk: { buildContextPack: vi.fn(async () => mockPack('unused')) }, - taskPayload: { - context: { - queries: [{ - id: 'bad', - purpose: 'optimize', - query: 'draft', - depth: 9, - }], - }, - }, - }); - - expect(result.contextPacks.all()).toEqual([]); - expect(result.warnings.join('\n')).toContain('Skipped context query "bad"'); - expect(result.warnings.join('\n')).toContain('using "plan"'); - }); - - it('throws when a required query is invalid', async () => { - await expect(resolveHandlerContextPacks({ - sdk: { buildContextPack: vi.fn(async () => mockPack('unused')) }, - taskPayload: { - context: { - queries: [{ - id: 'required-bad', - purpose: 'research', - query: 'runtime', - depth: 9, - required: true, - }], - }, - }, - })).rejects.toThrow('Required context query "required-bad" failed validation'); - }); - - it('skips optional queries when TreeDX context resolution fails', async () => { - const result = await resolveHandlerContextPacks({ - sdk: { buildContextPack: vi.fn(async () => { throw new Error('Graph is not ready.'); }) }, - taskPayload: { - context: { - queries: [{ - id: 'optional-graph', - purpose: 'plan', - query: 'package planning', - }], - }, - }, - }); - - expect(result.contextPacks.all()).toEqual([]); - expect(result.warnings.join('\n')).toContain('Skipped context query "optional-graph" after TreeDX context resolution failed: Graph is not ready.'); - }); - - it('throws when required TreeDX context resolution fails', async () => { - await expect(resolveHandlerContextPacks({ - sdk: { buildContextPack: vi.fn(async () => { throw new Error('Graph is not ready.'); }) }, - taskPayload: { - context: { - queries: [{ - id: 'required-graph', - purpose: 'plan', - query: 'package planning', - required: true, - }], - }, - }, - })).rejects.toThrow('Required context query "required-graph" failed during TreeDX context resolution: Graph is not ready.'); - }); -}); diff --git a/tests/unit/agents/contributions/non-agpl-contribution-policy.test.ts b/tests/unit/agents/contributions/non-agpl-contribution-policy.test.ts deleted file mode 100644 index 32698e22..00000000 --- a/tests/unit/agents/contributions/non-agpl-contribution-policy.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; - -describe('Agent contribution policy', () => { - it('does not impose the AGPL commercial-license approval process', () => { - const root = process.cwd(); - const template = readFileSync(resolve(root, '.github/PULL_REQUEST_TEMPLATE.md'), 'utf8'); - const guidance = readFileSync(resolve(root, 'CONTRIBUTING.md'), 'utf8'); - expect(JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')).license).toBe('Apache-2.0'); - expect(existsSync(resolve(root, '.github/workflows/contributor-license.yml'))).toBe(false); - expect(existsSync(resolve(root, '.github/approved-committers.json'))).toBe(false); - expect(template).not.toMatch(/Contribution grant|contribution-attestation/u); - expect(guidance).toMatch(/does not require a separate contributor-grant checkbox/u); - }); -}); diff --git a/tests/unit/agents/definitions/agent-definition-compatibility.test.ts b/tests/unit/agents/definitions/agent-definition-compatibility.test.ts deleted file mode 100644 index eb7ed14a..00000000 --- a/tests/unit/agents/definitions/agent-definition-compatibility.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { readdirSync,readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { validateAgentDefinitionModel } from '@treeseed/sdk/agent-capacity'; -import { parse as parseYaml } from 'yaml'; -import { describe,expect,it } from 'vitest'; - -describe('Agent shipped agent definitions', () => { - it('remain compatible with the SDK authority and capacity-provider contract', async () => { - const { validateAgentActivityProfileCompatibility } = await import(pathToFileURL(resolve(process.cwd(),'node_modules/@treeseed/sdk/dist/agent-capacity/validation/compatibility/agent-definition-compatibility.js')).href); - const root = resolve(process.cwd(),'docs/src/content/agents'); - const names = readdirSync(root).filter((entry) => entry.endsWith('.mdx')).sort(); - expect(names.length).toBeGreaterThan(0); - for (const name of names) { - const source = readFileSync(resolve(root,name),'utf8'); - const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u); - expect(match,`${name} must contain YAML frontmatter`).not.toBeNull(); - const definition = parseYaml(match![1]) as Record; - expect(validateAgentDefinitionModel(definition),name).toMatchObject({ ok:true,diagnostics:[] }); - for (const [activityType,profile] of Object.entries(definition.activityProfiles ?? {}) as Array<[any,any]>) { - if (profile.enabled) expect(validateAgentActivityProfileCompatibility(activityType,profile),name).toMatchObject({ ok:true,diagnostics:[] }); - } - } - }); -}); diff --git a/tests/unit/agents/execution-retry-propagation.test.ts b/tests/unit/agents/execution-retry-propagation.test.ts deleted file mode 100644 index 2ee90e3a..00000000 --- a/tests/unit/agents/execution-retry-propagation.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe,expect,it } from 'vitest'; -import { codexAgentToolConfig } from '../../../src/agents/adapters/codex/execution-codex-core.ts'; -import { executionCompletionEvidence,executionContentOutputStatus } from '../../../src/agents/handlers/execution-content.ts'; -import { waitingOutputIsTerminal } from '../../../src/agents/kernel/validation/output-validator.ts'; - -describe('execution-provider retry propagation', () => { - it('gives the required production MCP server enough time to initialize under concurrent load', () => { - const config = codexAgentToolConfig({ - repoRoot: '/tmp/agent-lab-repository', sandboxMode: 'workspace_write', - tools: [{ kind: 'agent_tool', id: 'treeseed.content.query', name: 'Query content' }], - } as never) as { mcp_servers: { treeseed_tools: { startup_timeout_sec: number } } }; - expect(config.mcp_servers.treeseed_tools.startup_timeout_sec).toBe(90); - }); - - it('returns retryable provider initialization failures to capacity instead of terminalizing them', () => { - const snapshot = { status: 'failed', retryable: true, code: 'codex_sdk_initialization_failed' } as never; - expect(executionContentOutputStatus(snapshot)).toBe('waiting'); - expect(waitingOutputIsTerminal({ - status: 'waiting', summary: 'Retry provider startup.', - metadata: { executionSnapshot: { ...snapshot, outputs: { executionBlocked: true } } }, - })).toBe(false); - }); - - it('keeps non-retryable provider failures terminal', () => { - const snapshot = { status: 'failed', retryable: false } as never; - expect(executionContentOutputStatus(snapshot)).toBe('failed'); - expect(waitingOutputIsTerminal({ - status: 'waiting', summary: 'Stop.', metadata: { executionSnapshot: { ...snapshot, outputs: { executionBlocked: true } } }, - })).toBe(true); - }); - - it('derives exact communication completion evidence from the durable response receipt',()=>{ - const completion=executionCompletionEvidence({snapshot:{status:'completed',summary:'Answered the exact committed message.'},contentArtifactRefs:[{ - model:'discussion_message',artifactKind:'discussion_response',contentPath:'src/content/discussion-messages/topic/response.mdx',subjectId:'source-message',sourceAssignmentId:'assignment-a',producedByAgent:'guide-steward', - }]} as never); - expect(completion).toMatchObject({disposition:'completed',noUsefulScopedWorkRemaining:true,durableArtifactRefs:['src/content/discussion-messages/topic/response.mdx'],remainingScope:[]}); - expect(completion?.acceptanceChecks).toEqual(expect.arrayContaining([expect.objectContaining({id:'artifact:discussion_message:discussion_response',passed:true})])); - }); -}); diff --git a/tests/unit/agents/execution-time-guidance.test.ts b/tests/unit/agents/execution-time-guidance.test.ts deleted file mode 100644 index 9040401a..00000000 --- a/tests/unit/agents/execution-time-guidance.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { assignmentDeadlineExpired, deadlineBoundExecutionTimeoutMs } from '../../../src/agents/adapters/reconciliation/execution-codex-adapter.ts'; -import { executionCompletionDeadlineMs } from '../../../src/agents/adapters/reconciliation/execution-timeout.ts'; -import { assignmentTimeGuidance, buildExecutionContentInstructions } from '../../../src/agents/handlers/execution-content-prompt.ts'; - -describe('assignment execution timing', () => { - it('derives a deadline from the assignment claim and reserved agent time', () => { - const timing = assignmentTimeGuidance({ capacity: { - assignment: { claimedAt: '2026-08-05T10:00:00.000Z' }, - envelope: { reservedSeconds: 240 }, - } } as never, Date.parse('2026-08-05T10:01:00.000Z')); - expect(timing).toEqual({ - startedAt: '2026-08-05T10:00:00.000Z', - allocatedSeconds: 240, - deadlineAt: '2026-08-05T10:04:00.000Z', - remainingSeconds: 180, - closeoutWarningSeconds: 48, - phase: 'working', - shouldCloseOut: false, - }); - expect(deadlineBoundExecutionTimeoutMs(900_000, timing, Date.parse('2026-08-05T10:01:00.000Z'))).toBe(87_000); - expect(assignmentDeadlineExpired(timing, Date.parse('2026-08-05T10:03:59.999Z'))).toBe(false); - expect(assignmentDeadlineExpired(timing, Date.parse('2026-08-05T10:04:00.000Z'))).toBe(true); - }); - - it('reserves bounded provider lifecycle time before the immutable hard deadline', () => { - const now = Date.parse('2026-08-05T10:00:00.000Z'); - expect(deadlineBoundExecutionTimeoutMs(900_000, { - deadlineAt: '2026-08-05T10:02:00.000Z', closeoutWarningSeconds: 24, - }, now)).toBe(66_000); - expect(deadlineBoundExecutionTimeoutMs(900_000, { - deadlineAt: '2026-08-05T10:15:00.000Z', closeoutWarningSeconds: 180, - }, now)).toBe(600_000); - expect(deadlineBoundExecutionTimeoutMs(900_000, { - deadlineAt: '2026-08-05T10:00:08.000Z', closeoutWarningSeconds: 24, - }, now)).toBe(1); - expect(deadlineBoundExecutionTimeoutMs(60_000, { - deadlineAt: '2026-08-05T10:00:34.000Z', closeoutWarningSeconds: 12, - }, now)).toBe(1); - expect(executionCompletionDeadlineMs(240_000, { - deadlineAt: '2026-08-05T10:05:00.000Z', - }, now)).toBe(Date.parse('2026-08-05T10:04:00.000Z')); - expect(executionCompletionDeadlineMs(900_000, { - deadlineAt: '2026-08-05T10:05:00.000Z', - }, now)).toBe(Date.parse('2026-08-05T10:04:30.000Z')); - }); - - it('tells the agent to preserve useful work before expiration', () => { - const instructions = buildExecutionContentInstructions({ - agent: { systemPrompt: 'Research carefully.' }, - capacity: { - mode: 'planning', assignmentId: 'assignment-a', - assignment: { claimedAt: '2026-08-05T10:00:00.000Z' }, - envelope: { reservedSeconds: 240 }, - }, - } as never, { - payload: {}, subject: { model: 'objective', id: 'core', title: 'Core' }, artifactKind: 'planning_note', - contextPackSummaries: [], assignedObjective: null, contentRoot: 'src/content', - }); - expect(instructions).toContain('Allocated agent time: 240 seconds'); - expect(instructions).toContain('Protected closeout allocation: 48 seconds outside productive execution'); - expect(instructions).toContain('Call treeseed.status at the start'); - expect(instructions).toContain('shouldCloseOut=true'); - expect(instructions).toContain('validate every content artifact with its Zod-backed model'); - expect(instructions).toContain('The proposal does not extend this assignment'); - }); - - it('uses an explicit closeout warning from the governed capacity envelope', () => { - const timing = assignmentTimeGuidance({ capacity: { - assignment: { claimedAt: '2026-08-05T10:00:00.000Z' }, - envelope: { reservedSeconds: 900, budget: { time: { closeoutWarningSeconds: 120 } } }, - } } as never, Date.parse('2026-08-05T10:13:10.000Z')); - expect(timing).toMatchObject({ remainingSeconds: 110, closeoutWarningSeconds: 120, shouldCloseOut: true }); - }); - - it('prevents agents from inventing subject groups for signal routing', () => { - const instructions = buildExecutionContentInstructions({ - agent:{ systemPrompt:'Research carefully.' },capacity:{ mode:'planning',assignmentId:'assignment-a',assignment:{},envelope:{} }, - } as never,{ - payload:{ signalContracts:{ 'evidence-ready':{ id:'evidence-ready' } } }, - subject:{ model:'objective',id:'core',title:'Core' },artifactKind:'planning_note',contextPackSummaries:[],assignedObjective:null,contentRoot:'src/content', - }); - expect(instructions).toContain("otherwise omit subjectGroupIds so the control plane can apply its frozen primary-group fallback"); - expect(instructions).toContain('Never infer group IDs from an artifact path, editorial category, provider, role, or agent class.'); - }); -}); diff --git a/tests/unit/agents/fallback-controller.test.ts b/tests/unit/agents/fallback-controller.test.ts deleted file mode 100644 index a4ccf125..00000000 --- a/tests/unit/agents/fallback-controller.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { AgentKernelFallbackController } from '../../../src/agents/kernel/validation/fallback-controller.ts'; - -describe('AgentKernelFallbackController', () => { - it('derives a replay-stable fallback evidence id from assignment attempt identity', () => { - const controller = new AgentKernelFallbackController(); - const input = { - assignmentId: 'Assignment A', - projectId: 'project-a', - mode: 'planning', - fallback: { code: 'input_missing', reason: 'Input is missing.', retryable: true }, - metadata: { attemptCount: 2 }, - }; - const first = controller.buildOutput(input); - const replay = controller.buildOutput(input); - expect(first.id).toBe('fallback:assignment-a:planning:input_missing:attempt-2'); - expect(replay).toEqual(first); - }); -}); diff --git a/tests/unit/agents/registry.test.ts b/tests/unit/agents/registry.test.ts deleted file mode 100644 index 0d0d0229..00000000 --- a/tests/unit/agents/registry.test.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { afterEach, describe, expect, it } from 'vitest'; -import { - listRegisteredAgentHandlers, - loadTenantAgentHandlerRegistry, - resolveAgentHandler, -} from '../../../src/agents/support/registry.ts'; -import { estimateHandler } from '../../../src/agents/handlers/estimate.ts'; -import { normalizeAgentRuntimeSpec } from '../../../src/agents/support/spec-normalizer.ts'; -import { selectAgentActivityProfile } from '../../../src/agents/kernel/telemetry/activity-profile-resolver.ts'; -import { resolveAssignmentAgentToolPolicy } from '../../../src/provider/capacity/assignments/assignment-tool-policy.ts'; - -const tempRoots: string[] = []; -const agentRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); -const marketRoot = resolve(agentRoot, '..', '..'); -const hasIntegratedMarketAgentContent = existsSync(resolve(marketRoot, 'src/content/agents/architect.mdx')); - -function createTenantRoot() { - const tenantRoot = mkdtempSync(resolve(tmpdir(), 'treeseed-agent-registry-')); - tempRoots.push(tenantRoot); - mkdirSync(resolve(tenantRoot, 'src/agent-handlers'), { recursive: true }); - return tenantRoot; -} - -function profileAgent(overrides: Record = {}) { - return { - slug: 'engineer', - agentClass: 'engineering', - projectAgentClassId: 'engineering', - projectAgentClassSlug: 'engineering', - enabled: true, - identity: { - purpose: 'Implement scoped work.', - responsibilities: ['Plan', 'Estimate', 'Act'], - durableInstructions: 'Stay scoped.', - }, - activityProfiles: { - planning: { - enabled: true, - handler: 'actor', - prompt: { system: 'Plan engineering work.' }, - branchPolicy: { kind: 'read-only', base: 'main' }, - tools: { allowed: ['treeseed.content.query', 'treeseed.content.read', 'treeseed.status'] }, - outputs: { messageTypes: [], modelMutations: ['note:create'] }, - permissions: { - content: { - question: { operations: ['query', 'read'] }, - decision: { operations: ['query', 'read'] }, - note: { operations: ['create'] }, - }, - commit: { allowed: false }, - }, - }, - acting: { - enabled: true, - handler: 'actor', - prompt: { system: 'Implement engineering work.' }, - branchPolicy: { - kind: 'assignment-feature', - base: 'staging', - target: 'staging', - branchNameTemplate: 'agent/{agentSlug}/{assignmentId}', - }, - tools: { allowed: ['treeseed.content.query', 'treeseed.content.read', 'treeseed.verify', 'treeseed.changed_paths'] }, - outputs: { messageTypes: [], modelMutations: ['implementation_report:create'] }, - permissions: { - content: { - question: { operations: ['query', 'read'] }, - decision: { operations: ['query', 'read'] }, - }, - repository: { readPaths: ['src/**'], writePaths: ['src/**'], allowCodeMutation: true }, - commit: { allowed: false }, - }, - execution: { - allowedPaths: ['src/**'], - forbiddenPaths: ['test/**', 'tests/**'], - }, - }, - }, - ...overrides, - }; -} - -describe('agent handler registry', () => { - afterEach(() => { - for (const root of tempRoots.splice(0)) { - rmSync(root, { recursive: true, force: true }); - } - }); - - it('loads tenant TypeScript handlers without requiring a tenant tsconfig', async () => { - const tenantRoot = createTenantRoot(); - writeFileSync( - resolve(tenantRoot, 'src/agent-handlers/security-audit.ts'), - `import type { AgentHandler } from '@treeseed/agent/runtime-types'; - -export const securityAuditHandler: AgentHandler = { -\tkind: 'security-audit', -\tasync resolveInputs() { return {}; }, -\tasync execute() { return { ok: true }; }, -\tasync emitOutputs() { return { status: 'completed', summary: 'ok' }; }, -}; -`, - 'utf8', - ); - - const registry = await loadTenantAgentHandlerRegistry(tenantRoot); - - expect(registry.writer?.kind).toBe('writer'); - expect(registry.actor?.kind).toBe('actor'); - expect(registry.estimate?.kind).toBe('estimate'); - expect(registry.releaser?.kind).toBe('releaser'); - expect(registry.reporter?.kind).toBe('reporter'); - expect(registry['security-audit']?.kind).toBe('security-audit'); - await expect(listRegisteredAgentHandlers({ tenantRoot })).resolves.toEqual(expect.arrayContaining([ - 'writer', - 'actor', - 'estimate', - 'releaser', - 'reporter', - 'security-audit', - ])); - }); - - it('normalizes activity profile specs into runtime specs', () => { - const result = normalizeAgentRuntimeSpec(profileAgent(), { - registeredHandlers: ['actor'], - messageTypes: [], - }); - - expect(result.diagnostics).toEqual([]); - expect(result.spec).toMatchObject({ - slug: 'engineer', - handler: 'actor', - activityType: 'acting', - projectAgentClassId: 'engineering', - tools: { allowed: ['treeseed.content.query', 'treeseed.content.read', 'treeseed.verify', 'treeseed.changed_paths'] }, - branchPolicy: { - kind: 'assignment-feature', - base: 'staging', - target: 'staging', - }, - }); - }); - - it('selects the complete configured activity profile for assignment mode', () => { - const normalized = normalizeAgentRuntimeSpec(profileAgent(), { - registeredHandlers: ['actor'], - messageTypes: [], - }); - expect(normalized.spec).not.toBeNull(); - const planning = selectAgentActivityProfile(normalized.spec!, 'planning'); - expect(planning).toMatchObject({ - activityType: 'planning', - handler: 'actor', - systemPrompt: expect.stringContaining('Plan engineering work.'), - outputs: { modelMutations: ['note:create'] }, - branchPolicy: { kind: 'read-only', base: 'main' }, - }); - expect(planning?.tools.allowed).toEqual(expect.arrayContaining(['treeseed.content.query', 'treeseed.content.read', 'treeseed.status'])); - expect(selectAgentActivityProfile(normalized.spec!, 'acting')).toMatchObject({ - execution: { allowedPaths: ['src/**'], forbiddenPaths: ['test/**', 'tests/**'] }, - }); - expect(selectAgentActivityProfile({ ...normalized.spec!, activityProfiles: {} }, 'planning')).toBeNull(); - expect(selectAgentActivityProfile({ - ...normalized.spec!, - activityProfiles: { - ...normalized.spec!.activityProfiles, - reporting: { - enabled: true, - handler: 'reporter', - prompt: { system: 'Report workday evidence.' }, - branchPolicy: { kind: 'read-only', base: 'main' }, - tools: { allowed: ['treeseed.content.read'] }, - outputs: { messageTypes: [], modelMutations: ['workday_report:create'] }, - }, - }, - }, 'planning', 'reporting')).toMatchObject({ - activityType: 'reporting', - handler: 'reporter', - systemPrompt: expect.stringContaining('Report workday evidence.'), - }); - }); - - it('selects reviewing in either provenance mode without falling back or gaining checkpoint authority', () => { - const normalized = normalizeAgentRuntimeSpec(profileAgent(), { - registeredHandlers: ['actor'], - messageTypes: [], - }); - expect(normalized.spec).not.toBeNull(); - const reviewProfile = { - ...normalized.spec!.activityProfiles.planning!, - activityType: 'reviewing' as const, - authorityPresets: ['review-authority' as const], - prompt: { system: 'Review the exact deliverable.' }, - tools: { allowed: ['treeseed.changed_paths', 'treeseed.verify'] }, - outputs: { messageTypes: [], modelMutations: ['review_finding:create'] }, - }; - const agent = { - ...normalized.spec!, - activityProfiles: { ...normalized.spec!.activityProfiles, reviewing: reviewProfile }, - }; - for (const mode of ['planning', 'acting'] as const) { - const selected = selectAgentActivityProfile(agent, mode, 'reviewing'); - expect(selected).toMatchObject({ - activityType: 'reviewing', - authorityPresetIds: ['review-authority'], - authoritySnapshot: { presetIds: ['review-authority'], branchPolicy: { kind: 'read-only' } }, - }); - expect(selected?.tools.allowed).toContain('treeseed.changed_paths'); - expect(selected?.tools.allowed).not.toContain('treeseed.checkpoint'); - } - expect(selectAgentActivityProfile(agent, 'planning', 'acting')).toBeNull(); - expect(selectAgentActivityProfile(agent, 'acting', 'reporting')).toBeNull(); - }); - - it('derives a specialized chat profile from the common discussion foundation', () => { - const normalized = normalizeAgentRuntimeSpec(profileAgent({ chatProfile: { - foundation: 'discussion-v1', responseStyle: 'implementation-focused', requiredCapabilities: ['agent-execution', 'repository-write'], maxTotalTokens: 24_000, maxCostAmount: 2, - } }), { registeredHandlers: ['actor', 'writer'], messageTypes: ['discussion_response'] }); - const chat = selectAgentActivityProfile(normalized.spec!, 'planning', 'chat'); - expect(chat).toMatchObject({ activityType: 'chat', handler: 'writer', execution: { requiredCapabilities: ['agent-execution', 'repository-write'], maxTotalTokens: 24_000, maxCostAmount: 2 } }); - expect(chat?.systemPrompt).toContain('implementation-focused'); - expect(chat?.systemPrompt).toContain('allocation is a maximum, not a target'); - expect(chat?.systemPrompt).toContain('completed_early'); - expect(chat?.permissionProjection?.write?.models).toEqual(expect.arrayContaining(['proposal', 'question', 'note'])); - expect(chat?.permissionProjection?.write?.models).not.toContain('discussion_message'); - expect(chat?.tools.allowed).toContain('treeseed.discussion.respond'); - }); - - it('derives the provider tool policy from the assigned activity rather than the root profile', () => { - const normalized = normalizeAgentRuntimeSpec(profileAgent(), { - registeredHandlers: ['actor'], - messageTypes: [], - }); - const agent = { - ...normalized.spec!, - activityProfiles: { - ...normalized.spec!.activityProfiles, - estimating: { - enabled: true, - handler: 'estimate', - prompt: { system: 'Estimate the proposal.' }, - branchPolicy: { kind: 'read-only' as const, base: 'main' }, - tools: { allowed: ['treeseed.content.read'] }, - outputs: { messageTypes: [], modelMutations: ['estimate:create'] }, - permissions: { content: { proposal: { operations: ['read'] } }, commit: { allowed: false } }, - }, - }, - }; - const policy = resolveAssignmentAgentToolPolicy(agent, 'planning', 'estimating'); - expect(policy).toMatchObject({ - activityType: 'estimating', - permissionProjection: { commit: { allowed: false } }, - }); - expect(policy?.tools.allowed).toEqual(expect.arrayContaining(['treeseed.content.read', 'treeseed.status', 'treeseed.discussion.respond'])); - expect(policy?.tools.allowed).not.toContain('treeseed.content.create'); - }); - - it('rejects legacy top-level agent configuration', () => { - const result = normalizeAgentRuntimeSpec({ - ...profileAgent(), - handler: 'act', - handlerConfig: { domain: 'legacy' }, - systemPrompt: 'Legacy.', - }, { - registeredHandlers: ['actor'], - messageTypes: [], - }); - - expect(result.spec).toBeNull(); - expect(result.diagnostics).toEqual(expect.arrayContaining([ - expect.objectContaining({ field: 'handler', severity: 'error' }), - expect.objectContaining({ field: 'handlerConfig', severity: 'error' }), - expect.objectContaining({ field: 'systemPrompt', severity: 'error' }), - ])); - }); - - it('does not resolve removed generic handler names', async () => { - const tenantRoot = createTenantRoot(); - - await expect(resolveAgentHandler('plan', { tenantRoot })).rejects.toThrow('No runtime handler is registered'); - await expect(resolveAgentHandler('research', { tenantRoot })).rejects.toThrow('No runtime handler is registered'); - await expect(resolveAgentHandler('act', { tenantRoot })).rejects.toThrow('No runtime handler is registered'); - await expect(resolveAgentHandler('review', { tenantRoot })).rejects.toThrow('No runtime handler is registered'); - await expect(resolveAgentHandler('report', { tenantRoot })).rejects.toThrow('No runtime handler is registered'); - await expect(resolveAgentHandler('actor', { tenantRoot })).resolves.toMatchObject({ kind: 'actor' }); - }); - - it('ships top-level Market engineering agent content with unprefixed slugs', async () => { - if (!hasIntegratedMarketAgentContent) { - expect(existsSync(resolve(agentRoot, 'package.json')), 'package-only verification must still have an agent package root').toBe(true); - return; - } - const slugs = readdirSync(resolve(marketRoot, 'src/content/agents')) - .filter((entry) => entry.endsWith('.mdx')) - .map((entry) => entry.replace(/\.mdx$/u, '')); - expect(slugs).toEqual(expect.arrayContaining([ - 'architect', - 'technical-writer', - 'tester', - 'engineer', - 'researcher', - 'reviewer', - 'releaser', - 'reporter', - ])); - }, 15_000); - - it('emits structured estimates from estimate activity output', async () => { - const output = await estimateHandler.emitOutputs({ - runId: 'run-1', - projectId: 'project-1', - mode: 'planning', - repoRoot: marketRoot, - agent: { - slug: 'engineer', - agentClass: 'engineer', - outputs: { messageTypes: [], modelMutations: [] }, - }, - capacity: { - assignmentId: 'assignment-1', - assignment: { - id: 'assignment-1', - teamId: 'team-1', - projectId: 'project-1', - projectAgentClassId: 'engineer', - }, - envelope: { - teamId: 'team-1', - projectId: 'project-1', - mode: 'planning', - reservedSeconds: 2, - }, - decisionInput: { - input: { decisionId: 'decision-1' }, - }, - }, - sdk: { createMessage: async () => ({}) }, - } as never, { - snapshot: { - runId: 'provider-run-1', - status: 'completed', - summary: 'Estimated work.', - outputs: { - structuredEstimate: { - expectedSeconds: 3, - maxSeconds: 5, - confidence: 'high', - riskLevel: 'medium', - dependencies: [{ - id: 'architecture-spec', - type: 'artifact', - requiredBefore: 'start', - deliverableType: 'architecture_spec', - agentClass: 'architect', - }], - expectedOutputs: [{ outputType: 'implementation_report', required: true }], - }, - }, - }, - contentArtifactRefs: [], - }); - - expect(output.status).toBe('completed'); - expect(output.metadata?.structuredEstimate).toMatchObject({ - schemaVersion: 2, - teamId: 'team-1', - projectId: 'project-1', - decisionId: 'decision-1', - agentClass: 'engineer', - expectedSeconds: 3, - maxSeconds: 5, - workBreakdown: { independentReviewSeconds: 1, finalReviewSeconds: 1, reportingSeconds: 1, expectedRevisionCycles: 1 }, - }); - expect(output.metadata?.estimateValidation).toMatchObject({ ok: true }); - }); -}); diff --git a/tests/unit/agents/timing/assignment-activity-tool.test.ts b/tests/unit/agents/timing/assignment-activity-tool.test.ts deleted file mode 100644 index 6fe49156..00000000 --- a/tests/unit/agents/timing/assignment-activity-tool.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { readAssignmentActivity } from '../../../../src/agents/tools/status/assignment-activity-tool.ts'; - -const event = { - id: 'event-2', sequence: 2, sourceEventId: 'source-2', timestamp: '2026-08-12T14:00:00.000Z', - teamId: 'team-1', projectId: 'project-1', workdayId: 'workday-1', assignmentId: 'assignment-1', - modeRunId: null, executionRunId: null, agentId: 'engineer', agentClassId: 'engineering', activityType: 'acting', - handlerId: 'source', capacityProviderId: 'provider-1', providerManagerId: null, runnerId: 'runner-1', executionProviderId: 'codex', - eventType: 'tool.completed', severity: 'info', summary: 'Verification completed.', transcriptRef: null, - artifactRefs: [], contextPackDigest: null, usageDelta: {}, durationMs: 120, errorCategory: null, recoveryState: null, - redactionStatus: 'sanitized', payloadDigest: 'sha256:event-2', -}; - -describe('assignment activity forensic tool', () => { - it('requests only the current assignment and validates the exact event contract', async () => { - let requested = ''; - const result = await readAssignmentActivity({ - apiBaseUrl: 'http://api.test', providerAccessToken: 'secret', assignmentId: 'assignment-1', - fetchImpl: async (input) => { requested = String(input); return new Response(JSON.stringify({ ok: true, payload: { items: [event], cursor: 2 } }), { status: 200, headers: { 'content-type': 'application/json' } }); }, - }, { after: 1, limit: 10, severity: 'info' }); - expect(requested).toBe('http://api.test/v1/provider/assignments/assignment-1/activity?after=1&limit=10&severity=info'); - expect(result).toMatchObject({ ok: true, payload: { items: [{ assignmentId: 'assignment-1' }], cursor: 2 } }); - }); - - it('rejects malformed forensic evidence returned by the control plane', async () => { - await expect(readAssignmentActivity({ - apiBaseUrl: 'http://api.test', providerAccessToken: 'secret', assignmentId: 'assignment-1', - fetchImpl: async () => new Response(JSON.stringify({ ok: true, payload: { items: [{ id: 'bad' }], cursor: 0 } }), { status: 200 }), - }, {})).rejects.toThrow(); - }); -}); diff --git a/tests/unit/agents/timing/assignment-operational-safety.test.ts b/tests/unit/agents/timing/assignment-operational-safety.test.ts deleted file mode 100644 index f145ebb0..00000000 --- a/tests/unit/agents/timing/assignment-operational-safety.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe,expect,it } from 'vitest'; -import { findAgentToolDefinition } from '@treeseed/sdk/agent-tools'; -import { callAgentTool } from '../../../../src/agents/tools/agent-tool-runtime.ts'; -import type { ExecutionProviderToolDescriptor } from '../../../../src/agents/runtime/runtime-types.ts'; - -function descriptor(id:string,overrides:Partial={}):ExecutionProviderToolDescriptor{ - const definition=findAgentToolDefinition(id)!; - return { kind:'agent_tool',id,name:definition.title,description:definition.description,inputSchema:definition.inputSchema,outputSchema:definition.outputSchema,executionTarget:definition.executionTarget,mutability:definition.mutability,metadata:{ projectId:'project-a',contentRoot:'src/content' },...overrides }; -} -function proxy():ExecutionProviderToolDescriptor{return { ...descriptor('treedx.read_workspace_file'),handleId:'handle-a',repositoryId:'repo-a',workspaceId:'workspace-a',allowedOperations:['files:read','files:write','git:commit'],allowedPaths:['src/content/**'],allowedReadPaths:['src/content/**'],allowedWritePaths:['src/content/**'],routes:{ buildContext:'POST /context',readRepositoryFiles:'POST /files/read',searchWorkspace:'POST /search',readWorkspaceFile:'GET /files?path=:path',applyWorkspaceChangeset:'POST /changesets',commitWorkspace:'POST /commit' },metadata:{ projectId:'project-a',contentRoot:'src/content',baseCommitSha:'abc123',baseRef:'refs/heads/main' } } as ExecutionProviderToolDescriptor;} -function status(stateVersion=3,phase='working') { return { ok:true,payload:{ id:'assignment-a',teamId:'team-a',projectId:'project-a',workdayId:'workday-a',stateVersion,status:'leased',leaseState:'leased',assignedAt:'2026-08-13T10:00:00.000Z',updatedAt:'2026-08-13T10:01:00.000Z',capacityEnvelope:{ budget:{ time:{ hardDeadlineAt:phase==='closeout'?'2026-08-13T10:03:00.000Z':'2099-08-13T10:00:00.000Z',closeoutWarningSeconds:180 } } },metadata:{} } }; } -function runtime(fetchImpl:typeof fetch,descriptors:ExecutionProviderToolDescriptor[]){ return { apiBaseUrl:'https://api.example.test',providerAccessToken:'provider-token',assignmentId:'assignment-a',leaseToken:'lease-a',descriptors,fetchImpl }; } - -describe('assignment operational safety',()=>{ - it('starts the full productive window only after initial plan read-back',async()=>{ - const calls:string[]=[]; let created=false; let executionBody:Record={}; - const plan='---\ntitle: assignment plan for assignment-a\nid: assignment-a\nstatus: ready\nrevision: 1\nobjective: Do bounded work.\ncompleted: []\nremaining: []\nrisks: []\nteamId: team-a\nprojectId: project-a\nworkdayId: workday-a\nassignmentId: assignment-a\ncreatedAt: 2026-08-13T10:00:00.000Z\nupdatedAt: 2026-08-13T10:00:00.000Z\n---\n'; - const fetchImpl=async(input:RequestInfo|URL,init?:RequestInit)=>{ - const url=String(input); calls.push(url); - if(url.endsWith('/execution-start')) { executionBody=JSON.parse(String(init?.body)); return Response.json({ok:true,payload:{ id:'assignment-a',stateVersion:4,metadata:{ operationalState:'executing' } }}); } - if(url.includes('/files?path=')) return created?Response.json({ok:true,payload:{content:plan}}):new Response('missing',{status:404}); - if(url.includes('/changesets')) { created=true; return Response.json({ok:true,payload:{changedPaths:['src/content/assignment-plans/assignment-a.mdx']}}); } - return Response.json({ok:true,payload:{...status(3,'preparation').payload,capacityEnvelope:{reservedSeconds:600,budget:{time:{requestedSeconds:600,executionSeconds:600,preparationSeconds:180,closeoutSeconds:120,preparationDeadlineAt:'2099-08-13T10:03:00.000Z',executionStartedAt:null,executionDeadlineAt:null,closeoutDeadlineAt:'2099-08-13T10:05:00.000Z'}}}}}); - }; - const restrictedProxy={ ...proxy(),metadata:{ ...proxy().metadata,permissionSummary:{ readActions:['read'],readModels:['objective'],writeActions:['create'],writeModels:['note'],commitAllowed:true } } }; - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor('treeseed.assignment_plan'),restrictedProxy]),'treeseed.assignment_plan',{ action:'write',expectedStateVersion:3,idempotencyKey:'initial-plan-a',objective:'Do bounded work.',status:'ready',completed:[],remaining:[],risks:[] }); - expect(result).toMatchObject({ok:true,assignmentStateVersion:4,executionTransition:{id:'assignment-a',stateVersion:4}}); - expect(calls.at(-1)).toContain('/execution-start'); - expect(executionBody).toMatchObject({leaseToken:'lease-a',expectedStateVersion:3,idempotencyKey:'initial-plan-a:execution-start',planRef:{id:'assignment-a',path:'src/content/assignment-plans/assignment-a.mdx',revision:1}}); - }); - - it('starts protected closeout before writing the terminal summary',async()=>{ - const calls:string[]=[]; let created=false; let fileReads=0; - const plan='---\ntitle: assignment plan for assignment-a\nid: assignment-a\nstatus: active\nrevision: 1\nobjective: Do bounded work.\ncompleted: []\nremaining: []\nrisks: []\nteamId: team-a\nprojectId: project-a\nworkdayId: workday-a\nassignmentId: assignment-a\ncreatedAt: 2026-08-13T10:00:00.000Z\nupdatedAt: 2026-08-13T10:00:00.000Z\n---\n'; - const summary='---\ntitle: assignment summary for assignment-a\nid: assignment-a\nstatus: completed\nsummary: Finished bounded work.\nartifactRefs: []\nverificationRefs: []\nlessons: []\nblockers: []\nperformance:\n outcome: completed\nteamId: team-a\nprojectId: project-a\nworkdayId: workday-a\nassignmentId: assignment-a\ncreatedAt: 2026-08-13T10:00:00.000Z\nupdatedAt: 2026-08-13T10:00:00.000Z\n---\n'; - const fetchImpl=async(input:RequestInfo|URL)=>{ - const url=String(input); calls.push(url); - if(url.endsWith('/closeout-start')) return Response.json({ok:true,payload:{id:'assignment-a',stateVersion:4,workDayId:'workday-a',metadata:{operationalState:'closeout'}}}); - if(url.includes('/files?path=')) { fileReads+=1; return fileReads===1?Response.json({ok:true,payload:{content:plan}}):created?Response.json({ok:true,payload:{content:summary}}):new Response('missing',{status:404}); } - if(url.includes('/changesets')) { created=true; return Response.json({ok:true,payload:{changedPaths:['src/content/assignment-summaries/assignment-a.mdx']}}); } - return Response.json(status(3)); - }; - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor('treeseed.assignment_summary'),descriptor('treeseed.assignment_plan'),proxy()]),'treeseed.assignment_summary',{action:'write',expectedStateVersion:3,idempotencyKey:'summary-a',status:'completed',summary:'Finished bounded work.',artifactRefs:[],verificationRefs:[],lessons:[],blockers:[],performance:{outcome:'completed'}}); - expect(result).toMatchObject({ok:true,assignmentStateVersion:4,closeoutTransition:{id:'assignment-a',stateVersion:4}}); - expect(calls.findIndex((url)=>url.endsWith('/closeout-start'))).toBeLessThan(calls.findIndex((url)=>url.includes('/changesets'))); - }); - - it('requires the initial TreeDX plan before any other mutation',async()=>{ - const fetchImpl=async(input:RequestInfo|URL)=>String(input).includes('/v1/provider/assignments/')&&!String(input).includes('/files')?Response.json(status()):new Response('missing',{ status:404 }); - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor('treeseed.publish_signal'),descriptor('treeseed.assignment_plan'),proxy()]),'treeseed.publish_signal',{ contractId:'handoff',subjectKind:'objective',subjectId:'objective-a',message:'Durable handoff evidence.' }); - expect(result).toMatchObject({ ok:false,code:'assignment_initial_plan_required' }); - }); - - it('rejects stale operational writes before TreeDX mutation',async()=>{ - const fetchImpl=async()=>Response.json(status(3)); - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor('treeseed.assignment_status_update'),descriptor('treeseed.assignment_plan'),proxy()]),'treeseed.assignment_status_update',{ expectedStateVersion:2,sequence:0,phase:'implementation',status:'running',idempotencyKey:'status-update-a' }); - expect(result).toMatchObject({ ok:false,code:'assignment_status_stale',metadata:{ stateVersion:3 } }); - }); - - it('requires exact append-only lineage after the initial status entry',async()=>{ - const result=await callAgentTool(runtime((async()=>{ throw new Error('no side effects expected'); }) as typeof fetch,[descriptor('treeseed.assignment_status_update')]),'treeseed.assignment_status_update',{ expectedStateVersion:3,sequence:1,phase:'implementation',status:'running',idempotencyKey:'status-update-b' }); - expect(result).toMatchObject({ ok:false,code:'invalid_tool_input' }); - }); - - it('blocks exploration once authoritative closeout begins',async()=>{ - const now=Date.now(); - const fetchImpl=async()=>Response.json({ ...status(),payload:{ ...status().payload,capacityEnvelope:{ budget:{ time:{ hardDeadlineAt:new Date(now+60_000).toISOString(),closeoutWarningSeconds:180 } } } } }); - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor('treeseed.repository.search')]),'treeseed.repository.search',{ query:'new scope' }); - expect(result).toMatchObject({ ok:false,code:'assignment_closeout_tool_restricted',metadata:{ phase:'closeout' } }); - }); - - it('keeps bounded discussion reads and follows available during protected closeout',async()=>{ - const now=Date.now(); - const fetchImpl=async(input:RequestInfo|URL)=>{ - if(String(input).includes('/discussions?')) return Response.json({ok:true,payload:{discussionId:'discussion-a',messages:[],events:[],cursor:'cursor-a'}}); - return Response.json({ ...status(),payload:{ ...status().payload,capacityEnvelope:{ budget:{ time:{ hardDeadlineAt:new Date(now+60_000).toISOString(),closeoutWarningSeconds:180 } } } } }); - }; - for(const toolId of ['treeseed.discussion.read','treeseed.discussion.follow']){ - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor(toolId)]),toolId,{discussionId:'discussion-a',limit:20}); - expect(result).toMatchObject({ok:true,payload:{discussionId:'discussion-a',cursor:'cursor-a'}}); - } - }); - - it('rejects every mutation after protected closeout expires',async()=>{ - const fetchImpl=async()=>Response.json({ ...status(),payload:{ ...status().payload,capacityEnvelope:{ budget:{ time:{ hardDeadlineAt:new Date(Date.now()-1_000).toISOString(),closeoutWarningSeconds:180 } } } } }); - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor('treeseed.assignment_status_update')]),'treeseed.assignment_status_update',{expectedStateVersion:3,sequence:0,phase:'closeout',status:'failed',idempotencyKey:'expired-status-a'}); - expect(result).toMatchObject({ok:false,code:'assignment_closeout_deadline_exhausted'}); - }); - - it('allows a required signal publication during closeout after the initial plan exists',async()=>{ - const now=Date.now(); - const fetchImpl=async(input:RequestInfo|URL)=>{ - if(String(input).includes('/files?path=')) return Response.json({ ok:true,result:{ content:'---\nschemaVersion: treeseed.assignment-plan/v1\n---\n' } }); - return Response.json({ ...status(),payload:{ ...status().payload,capacityEnvelope:{ budget:{ time:{ hardDeadlineAt:new Date(now+60_000).toISOString(),closeoutWarningSeconds:180 } } } } }); - }; - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor('treeseed.publish_signal'),descriptor('treeseed.assignment_plan'),proxy()]),'treeseed.publish_signal',{ contractId:'evidence-ready',subjectKind:'objective',subjectId:'core',message:'Committed evidence dossier is ready for downstream review.' }); - expect(result).toMatchObject({ ok:true,payload:{ contractId:'evidence-ready',requested:true } }); - }); - - it('allows the operational plan to be checkpointed during closeout',async()=>{ - const now=Date.now(); - const existingPlan='---\ntitle: assignment plan for assignment-a\nid: assignment-a\nstatus: active\nrevision: 1\nobjective: Finish safely.\ncompleted: []\nremaining: []\nrisks: []\nteamId: team-a\nprojectId: project-a\nworkdayId: workday-a\nassignmentId: assignment-a\ncreatedAt: 2026-08-13T10:00:00.000Z\nupdatedAt: 2026-08-13T10:00:00.000Z\n---\n'; - const fetchImpl=async(input:RequestInfo|URL)=>{ - if(String(input).includes('/files?path=')) return Response.json({ ok:true,payload:{ content:existingPlan } }); - if(String(input).includes('/changesets')) return Response.json({ ok:true,payload:{ changedPaths:['src/content/assignment-plans/assignment-a.mdx'] } }); - return Response.json({ ...status(),payload:{ ...status().payload,capacityEnvelope:{ budget:{ time:{ hardDeadlineAt:new Date(now+60_000).toISOString(),closeoutWarningSeconds:180 } } } } }); - }; - const result=await callAgentTool(runtime(fetchImpl as typeof fetch,[descriptor('treeseed.assignment_plan'),proxy()]),'treeseed.assignment_plan',{ - action:'write',expectedStateVersion:3,idempotencyKey:'closeout-plan-a',objective:'Finish safely.',status:'completed', - completed:[{ id:'work',title:'Work',description:'The bounded work is complete.' }],remaining:[],risks:[], - }); - expect(result).toMatchObject({ ok:true }); - }); -}); diff --git a/tests/unit/agents/timing/assignment-process-timeout.test.ts b/tests/unit/agents/timing/assignment-process-timeout.test.ts deleted file mode 100644 index 4f24731c..00000000 --- a/tests/unit/agents/timing/assignment-process-timeout.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {describe,expect,it} from 'vitest'; -import {assignmentProcessTimeoutSeconds} from '../../../../src/agents/kernel/execution/context-loader.ts'; - -describe('assignment provider process timeout',()=>{ - it('adds bounded preparation and closeout around the full productive allocation',()=>{ - expect(assignmentProcessTimeoutSeconds(900,{reservedSeconds:600,budget:{time:{preparationSeconds:180,closeoutSeconds:120}}} as never)).toBe(900); - }); - it('keeps legacy envelopes bounded by productive reservation',()=>{ - expect(assignmentProcessTimeoutSeconds(900,{reservedSeconds:600} as never)).toBe(600); - }); -}); diff --git a/tests/unit/agents/timing/assignment-status-tool.test.ts b/tests/unit/agents/timing/assignment-status-tool.test.ts deleted file mode 100644 index 8ebf610c..00000000 --- a/tests/unit/agents/timing/assignment-status-tool.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe,expect,it } from 'vitest'; -import { readAssignmentStatus } from '../../../../src/agents/tools/status/assignment-status-tool.ts'; - -function response(remainingSeconds: number) { - const now = new Date('2026-08-12T14:00:00.000Z'); - const deadline = new Date(now.getTime() + remainingSeconds * 1_000).toISOString(); - return { - options: { - apiBaseUrl: 'http://api.test', providerAccessToken: 'secret', assignmentId: 'assignment-1', now: () => now, - fetchImpl: async () => new Response(JSON.stringify({ ok:true,payload:{ - id:'assignment-1',workDayId:'workday-1',projectId:'project-1',agentId:'engineer',projectAgentClassId:'engineering', - status:'running',leaseState:'leased',assignedAt:'2026-08-12T13:55:00.000Z',reservationId:'reservation-1', - capacityEnvelope:{ reservedSeconds:900,budget:{ deadline,time:{ hardDeadlineAt:deadline,closeoutWarningSeconds:120 } } }, - decisionInput:{ input:{ activityType:'acting' } },metadata:{ workdayRunId:'run-1' }, - } }), { status:200,headers:{ 'content-type':'application/json' } }), - }, - deadline, - }; -} - -describe('assignment status time contract', () => { - it('separates preparation, productive execution, and protected closeout',async()=>{ - const now=new Date('2026-08-12T14:00:00.000Z'); - const options={ apiBaseUrl:'http://api.test',providerAccessToken:'secret',assignmentId:'assignment-1',now:()=>now,fetchImpl:async()=>Response.json({ok:true,payload:{ id:'assignment-1',projectId:'project-1',status:'leased',leaseState:'leased',capacityEnvelope:{reservedSeconds:600,budget:{time:{requestedSeconds:600,executionSeconds:600,preparationSeconds:180,closeoutSeconds:120,preparationDeadlineAt:'2026-08-12T14:03:00.000Z',executionStartedAt:null,executionDeadlineAt:null,closeoutDeadlineAt:'2026-08-12T14:05:00.000Z'}}},decisionInput:{},metadata:{} }}) }; - await expect(readAssignmentStatus(options)).resolves.toMatchObject({payload:{time:{phase:'preparation',preparationRemainingSeconds:180,executionRemainingSeconds:600,remainingSeconds:180}}}); - }); - it('returns authoritative wall-clock assignment timing while work continues', async () => { - const input = response(121); - await expect(readAssignmentStatus(input.options)).resolves.toMatchObject({ ok:true,payload:{ time:{ - now:'2026-08-12T14:00:00.000Z',startedAt:'2026-08-12T13:55:00.000Z',deadlineAt:input.deadline, - allocatedSeconds:900,remainingSeconds:121,closeoutWarningSeconds:120,phase:'working',shouldCloseOut:false, - } } }); - }); - - it('requires closeout at the configured threshold and reports expiration', async () => { - await expect(readAssignmentStatus(response(120).options)).resolves.toMatchObject({ payload:{ time:{ phase:'closeout',shouldCloseOut:true,remainingSeconds:120 } } }); - await expect(readAssignmentStatus(response(0).options)).resolves.toMatchObject({ payload:{ time:{ phase:'expired',shouldCloseOut:true,remainingSeconds:0 } } }); - }); -}); diff --git a/tests/unit/agents/tools/operational-content-response.test.ts b/tests/unit/agents/tools/operational-content-response.test.ts deleted file mode 100644 index f35ebbf0..00000000 --- a/tests/unit/agents/tools/operational-content-response.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe,expect,it } from 'vitest'; -import { assignmentOperationalPath,operationalResponseContent } from '../../../../src/agents/tools/assignment/operational-content-tool.ts'; -import { modelRelativePlacement } from '../../../../src/agents/tools/content-tool-runtime.ts'; -import { contentModelForArtifactKind,missingOperationalCloseoutReceipts,missingPrecommitContentReceipts } from '../../../../src/agents/tools/agent-tool-completion.ts'; -import { matchesScopedPath } from '../../../../src/agents/tools/path-scope/path-matcher.ts'; -import { assignmentContextRef,assignmentRuntimeContextRef } from '../../../../src/agents/handlers/context/shared-context-queries.ts'; - -describe('assignment operational content response extraction', () => { - it('terminates on absent nested envelopes', () => { - expect(operationalResponseContent({ ok: false })).toBe(''); - }); - - it('extracts nested TreeDX content without following cycles', () => { - const response: Record = {}; - response.payload = response; - response.data = { file: { content: '---\nid: assignment-1\n---\nPlan' } }; - expect(operationalResponseContent(response)).toContain('id: assignment-1'); - }); - - it('preserves the exact case-sensitive assignment identity in owned paths', () => { - expect(assignmentOperationalPath('treeseed.assignment_plan',{}, { assignmentId:'assignment_AbC-123' })) - .toBe('src/content/assignment-plans/assignment_AbC-123.mdx'); - expect(modelRelativePlacement({ placement:{ path:'src/content/assignment-plans/assignment_AbC-123.mdx' } },'assignment_plan','src/content')) - .toEqual({ path:'src/content/assignment-plans/assignment_AbC-123' }); - }); - - it('matches embedded wildcards in assignment-owned status paths', () => { - expect(matchesScopedPath( - 'src/content/assignment-statuses/assignment_AbC-123-status-0.mdx', - 'src/content/assignment-statuses/assignment_AbC-123-status-*', - )).toBe(true); - }); - - it('separates immutable query definitions from current runtime content', () => { - const context={ capacity:{ treedxProxyHandle:{ baseCommitSha:'workspace-ref' } } } as never; - const payload={ contextDefinitionRef:'definition-ref',contextRuntimeRef:'runtime-ref',contentBaseRef:'workspace-ref' }; - expect(assignmentContextRef(context,payload)).toBe('definition-ref'); - expect(assignmentRuntimeContextRef(context,payload)).toBe('runtime-ref'); - }); - - it('requires terminal operational records before a content workspace can commit', () => { - const artifact = { status:'completed', derivedEvents:[{ type:'content_created', contentRef:{ model:'knowledge', path:'src/content/knowledge/guide.mdx' } }] }; - expect(missingPrecommitContentReceipts([artifact], 'knowledge_update')).toEqual([ - 'assignment_terminal_status', 'assignment_summary', - ]); - expect(missingPrecommitContentReceipts([artifact, - { toolId:'treeseed.assignment_status_update', status:'completed', inputSummary:{ status:'completed' } }, - { toolId:'treeseed.assignment_summary', status:'completed', inputSummary:{ action:'write', status:'completed' } }, - ], 'knowledge_update')).toEqual([]); - expect(missingOperationalCloseoutReceipts([])).toEqual(['assignment_terminal_status','assignment_summary']); - expect(missingOperationalCloseoutReceipts([ - { toolId:'treeseed.assignment_status_update',status:'completed',inputSummary:{status:'completed'} }, - { toolId:'treeseed.assignment_summary',status:'completed',inputSummary:{action:'write',status:'completed'} }, - ])).toEqual([]); - }); - - it('binds the Chat output contract to the dedicated Discussion message model',()=>{ - expect(contentModelForArtifactKind('discussion_response')).toBe('discussion_message'); - }); -}); diff --git a/tests/unit/agents/validation/early-completion.test.ts b/tests/unit/agents/validation/early-completion.test.ts deleted file mode 100644 index 7237eb7c..00000000 --- a/tests/unit/agents/validation/early-completion.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { AgentKernelOutputValidator } from '../../../../src/agents/kernel/validation/output-validator.ts'; - -describe('early completion evidence', () => { - it('rejects false efficiency and accepts fully evidenced early completion even without other output restrictions', () => { - const validator = new AgentKernelOutputValidator(); - expect(validator.validate({ mode: 'planning', outputs: { metadata: { completion: { disposition: 'completed_early', noUsefulScopedWorkRemaining: true } } } }).ok).toBe(false); - expect(validator.validate({ mode: 'planning', outputs: { metadata: { completion: { - disposition: 'completed_early', noUsefulScopedWorkRemaining: true, completionReason: 'All scoped checks passed.', - acceptanceChecks: [{ id: 'tests', passed: true, evidenceRefs: ['artifact:test-report'] }], - durableArtifactRefs: ['artifact:test-report'], remainingBudget: { time: { remainingSeconds: 240 }, tokens: { hardLimitTokens: 10_000 } }, - } } } }).ok).toBe(true); - }); -}); diff --git a/tests/unit/agents/validation/failure-classifier.test.ts b/tests/unit/agents/validation/failure-classifier.test.ts deleted file mode 100644 index 5c925723..00000000 --- a/tests/unit/agents/validation/failure-classifier.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - classifyAgentExecutionFailure, - isRetryableAgentExecutionFailure, -} from '../../../../src/agents/kernel/validation/failure-classifier.ts'; - -describe('agent execution failure classification', () => { - it('keeps transient transport failures resumable', () => { - expect(isRetryableAgentExecutionFailure(new TypeError('fetch failed'))).toBe(true); - expect(isRetryableAgentExecutionFailure(new Error('socket temporarily unavailable'))).toBe(true); - expect(classifyAgentExecutionFailure(new TypeError('fetch failed'))).toBe('sdk_error'); - }); - - it('does not make semantic or permission failures retryable', () => { - expect(isRetryableAgentExecutionFailure(new Error('Artifact kind is invalid.'))).toBe(false); - expect(isRetryableAgentExecutionFailure(new Error('Path is not allowed.'))).toBe(false); - }); -}); diff --git a/tests/unit/agents/workflow-execution-provider.test.ts b/tests/unit/agents/workflow-execution-provider.test.ts deleted file mode 100644 index 7c325565..00000000 --- a/tests/unit/agents/workflow-execution-provider.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { WorkflowExecutionProviderAdapter } from '../../../src/agents/adapters/operations/execution-workflow.ts'; - -function invocation() { - return { - assignment: { id: 'assignment-a', capabilityHandles: { workflowOperations: [{ - id: 'handle-a', kind: 'workflow_operation', operationId: 'verify-project', status: 'active', - assignmentId: 'assignment-a', operations: ['dispatch_workflow'], repository: 'example/repo', - workflowFile: '.github/workflows/verify.yml', ref: 'refs/heads/main', - }] } }, - leaseToken: 'lease-a', workPackage: { kind: 'verify-project', title: 'Verify', summary: 'Verify.', - instructions: 'Run verification.', context: {}, expectedOutputs: [], metadata: {} }, - decisionInput: { input: {} }, metadata: {}, - } as any; -} - -describe('assignment workflow execution provider', () => { - it('preserves the TreeSeed run id and polls through the assignment-scoped API', async () => { - const getWorkflowOperationRun = vi.fn().mockResolvedValue({ ok: true, payload: { - id: 'run-a', status: 'completed', providerRunId: '881', providerRunUrl: 'https://github.test/runs/881', - } }); - const adapter = new WorkflowExecutionProviderAdapter({ - dispatchWorkflowOperation: vi.fn().mockResolvedValue({ ok: true, payload: { - run: { id: 'run-a', status: 'authorizing' }, operation: { id: 'platform-operation-a' }, - } }), getWorkflowOperationRun, - }); - const started = await adapter.start(invocation()); - expect(started).toMatchObject({ runId: 'run-a', status: 'waiting' }); - const observed = await adapter.poll({ assignmentId: 'assignment-a', runId: started.runId, - metadata: started.metadata, externalRef: started.externalRef, externalUrl: started.externalUrl }); - expect(getWorkflowOperationRun).toHaveBeenCalledWith('assignment-a', 'run-a'); - expect(observed).toMatchObject({ runId: 'run-a', status: 'completed', externalUrl: 'https://github.test/runs/881' }); - }); -}); diff --git a/tests/unit/provider/assignment-performance-summary.test.ts b/tests/unit/provider/assignment-performance-summary.test.ts deleted file mode 100644 index 6bdf7137..00000000 --- a/tests/unit/provider/assignment-performance-summary.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe,expect,it } from 'vitest'; -import type { ProviderAssignment } from '@treeseed/sdk/agent-capacity'; -import { buildAssignmentPerformanceSummary } from '../../../src/provider/capacity/assignments/performance-summary.ts'; - -describe('assignment performance attribution', () => { - it('records configuration revisions and receipt-linked downstream outcomes', () => { - const assignment = { id: 'assignment-1', teamId: 'team-1', projectId: 'project-1', projectAgentClassId: 'engineer', - capacityProviderId: 'provider-1', mode: 'acting', attemptCount: 0, metadata: { planningGraphRevision: 'graph-4', agentDefinitionRevision: 'agent-8' } } as ProviderAssignment; - const downstreamOutcomes = [{ kind: 'integration' as const, status: 'integrated', evidenceRefs: ['checkpoint:abc'], - artifactMutationReceiptIds: ['receipt-1'], proposalId: 'proposal-1', proposalVersion: 2, occurredAt: '2026-08-13T12:00:00.000Z' }]; - const summary = buildAssignmentPerformanceSummary({ assignment, disposition: 'completed', reason: 'done', capacityEnvelope: {}, usage: {}, activeSeconds: 3, - elapsedSeconds: 10,preparationSeconds:4,executionSeconds:3,closeoutSeconds:3, completion: { downstreamOutcomes } }); - expect(summary.schemaVersion).toBe('treeseed.assignment-performance/v2'); - expect(summary.configuration).toMatchObject({ planningGraphRevision: 'graph-4', agentDefinitionRevision: 'agent-8' }); - expect(summary.downstreamOutcomes).toEqual(downstreamOutcomes); - expect(summary.actual).toMatchObject({preparationSeconds:4,executionSeconds:3,closeoutSeconds:3,custodySeconds:10,activeSeconds:3}); - }); -}); diff --git a/tests/unit/provider/assignment-result-reporter.test.ts b/tests/unit/provider/assignment-result-reporter.test.ts deleted file mode 100644 index 4e641d18..00000000 --- a/tests/unit/provider/assignment-result-reporter.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe,expect,it,vi } from 'vitest'; -import { closeTerminalWorkspaceWithRetry,completeProviderAssignmentWithRetry } from '../../../src/provider/capacity/assignments/assignment-result-reporter.ts'; - -describe('provider assignment completion reporting', () => { - it('retries transient control-plane failures without returning completed work', async () => { - const transient = Object.assign(new Error('TreeDX projection timed out.'), { status: 500 }); - const completeAssignment = vi.fn() - .mockRejectedValueOnce(transient) - .mockRejectedValueOnce(transient) - .mockResolvedValue({ status: 'completed' }); - const wait = vi.fn(async () => undefined); - - await expect(completeProviderAssignmentWithRetry( - { completeAssignment } as never, - 'assignment-a', - { leaseToken: 'lease-a' }, - wait, - )).resolves.toEqual({ status: 'completed' }); - expect(completeAssignment).toHaveBeenCalledTimes(3); - expect(wait).toHaveBeenNthCalledWith(1, 250); - expect(wait).toHaveBeenNthCalledWith(2, 500); - }); - - it('does not retry a governance conflict', async () => { - const conflict = Object.assign(new Error('Proposal is invalid.'), { status: 409 }); - const completeAssignment = vi.fn().mockRejectedValue(conflict); - const wait = vi.fn(async () => undefined); - await expect(completeProviderAssignmentWithRetry( - { completeAssignment } as never, 'assignment-a', {}, wait, - )).rejects.toBe(conflict); - expect(completeAssignment).toHaveBeenCalledOnce(); - expect(wait).not.toHaveBeenCalled(); - }); - - it('requires terminal TreeDX workspace cleanup and retries transient failures', async () => { - const closeWorkspace = vi.fn() - .mockRejectedValueOnce(new TypeError('fetch failed')) - .mockResolvedValue({ status: 'closed' }); - const wait = vi.fn(async () => undefined); - await expect(closeTerminalWorkspaceWithRetry(closeWorkspace, wait)).resolves.toBe('completed'); - expect(closeWorkspace).toHaveBeenCalledTimes(2); - expect(wait).toHaveBeenCalledWith(250); - }); - - it('does not report cleanup when no TreeDX workspace was assigned', async () => { - await expect(closeTerminalWorkspaceWithRetry(null)).resolves.toBe('not_required'); - }); -}); diff --git a/tests/unit/provider/budgets.test.ts b/tests/unit/provider/budgets.test.ts deleted file mode 100644 index 094550e3..00000000 --- a/tests/unit/provider/budgets.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { mkdtempSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { discoverProviderBudgets } from '../../../src/provider/configuration/budgets.ts'; -import type { ProviderHostRuntimeConfig } from '../../../src/provider/configuration/config.ts'; - -function baseConfig(patch: Partial = {}): ProviderHostRuntimeConfig { - return { - dataDir: '/tmp/treeseed-provider', - environment: 'local', - capabilitiesFile: null, - budgetFile: null, - maxConcurrentWorkdays: 1, - maxConcurrentRunners: 4, - dailyAgentSecondsLimit: null, - monthlyAgentSecondsLimit: null, - codexAuthFile: null, - codexAuthJsonB64: null, - codexAuthOverwrite: false, - jira: null, - githubIssues: null, - discord: null, - env: {}, - redactedEnv: {}, - ...patch, - }; -} - -describe('provider budget discovery', () => { - it('loads native capacity facts from the provider budget file', () => { - const root = mkdtempSync(resolve(tmpdir(), 'treeseed-provider-budget-')); - const budgetFile = resolve(root, 'budget.yaml'); - writeFileSync(budgetFile, [ - 'nativeCapacity:', - ' executionProviders:', - ' - id: codex-seat', - ' name: Codex Pro Seat', - ' kind: codex', - ' nativeUnit: wall_minute', - ' quotaVisibility: opaque', - ' maxConcurrentWorkers: 1', - ' nativeLimits:', - ' - scope: daily', - ' nativeUnit: wall_minute', - ' limitAmount: 240', - ' reserveBufferPercent: 25', - '', - ].join('\n')); - - const budgets = discoverProviderBudgets(baseConfig({ budgetFile })); - expect(budgets.nativeCapacity).toMatchObject({ - executionProviders: [{ - id: 'codex-seat', - nativeUnit: 'wall_minute', - nativeLimits: [expect.objectContaining({ - limitAmount: 240, - reserveBufferPercent: 25, - })], - }], - }); - expect(budgets.dailyAgentSecondsLimit).toBeUndefined(); - }); -}); diff --git a/tests/unit/provider/capabilities.test.ts b/tests/unit/provider/capabilities.test.ts deleted file mode 100644 index 3f6b5046..00000000 --- a/tests/unit/provider/capabilities.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { discoverProviderCapabilities } from '../../../src/provider/configuration/capabilities.ts'; - -describe('provider capability discovery', () => { - it('advertises the canonical capability required by agent definitions', () => { - const capabilities = discoverProviderCapabilities({ env: {} } as never); - expect(capabilities).toEqual(expect.arrayContaining([ - expect.objectContaining({ id: 'agent-execution' }), - ])); - }); -}); diff --git a/tests/unit/provider/capacity/assignments/workday-assignment-authority.test.ts b/tests/unit/provider/capacity/assignments/workday-assignment-authority.test.ts deleted file mode 100644 index 91cbb676..00000000 --- a/tests/unit/provider/capacity/assignments/workday-assignment-authority.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { validateWorkdayAssignmentAuthority, workdayAssignmentAuthorityProjection } from '../../../../../src/provider/capacity/assignments/workday-assignment-authority.ts'; -import { runProviderAssignment } from '../../../../../src/provider/operations/runner.ts'; - -function assignment(overrides: Record = {}) { - return { - id: 'assignment-1', - membershipId: 'membership-1', - stateVersion: 1, - teamId: 'team-1', - projectId: 'project-1', - capacityProviderId: 'provider-1', - projectAgentClassId: 'class-1', - allocationSetId: 'allocation-1', - reservationId: 'reservation-1', - workDayId: 'workday-1', - mode: 'planning', - synthesizedFrom: 'workday_demand', - metadata: { workdayRunId: 'run-1', demandId: 'demand-1' }, - capacityEnvelope: { - teamId: 'team-1', - projectId: 'project-1', - capacityProviderId: 'provider-1', - projectAgentClassId: 'class-1', - workDayId: 'workday-1', - mode: 'planning', - }, - decisionInput: { metadata: { demandId: 'demand-1' } }, - ...overrides, - }; -} - -describe('API-issued workday assignment authority', () => { - it('accepts an exact planning assignment without deriving policy', () => { - const value = assignment(); - expect(validateWorkdayAssignmentAuthority(value)).toEqual([]); - expect(workdayAssignmentAuthorityProjection(value)).toEqual({ - workdayRunId: 'run-1', - workdayId: 'workday-1', - demandId: 'demand-1', - allocationSetId: 'allocation-1', - projectAgentClassId: 'class-1', - reservationId: 'reservation-1', - capacityProviderId: 'provider-1', - decisionId: null, - capacityPlanId: null, - stateVersion: 1, - }); - }); - - it('rejects missing authority and cross-record identity conflicts', () => { - const diagnostics = validateWorkdayAssignmentAuthority(assignment({ - allocationSetId: null, - reservationId: null, - projectAgentClassId: '', - capacityEnvelope: { teamId: 'another-team', mode: 'acting' }, - })); - expect(diagnostics.map((entry) => entry.code)).toEqual(expect.arrayContaining([ - 'workday_assignment_allocation_missing', - 'workday_assignment_reservation_missing', - 'workday_assignment_class_missing', - 'workday_assignment_authority_conflict', - ])); - }); - - it('requires API-owned decision and capacity-plan evidence for acting', () => { - const value = assignment({ - mode: 'acting', - decisionId: null, - capacityEnvelope: { ...assignment().capacityEnvelope as Record, mode: 'acting' }, - }); - const diagnostics = validateWorkdayAssignmentAuthority(value); - expect(diagnostics.map((entry) => entry.code)).toEqual(expect.arrayContaining([ - 'workday_acting_decision_missing', - 'workday_acting_capacity_plan_missing', - ])); - }); - - it('does not impose the new workday contract on mixed-version legacy assignments', () => { - expect(validateWorkdayAssignmentAuthority({ synthesizedFrom: 'approved_decision' })).toEqual([]); - }); - - it('fails before kernel or workspace preparation when authority is incomplete', async () => { - let kernelCalls = 0; - const failures: Record[] = []; - const result = await runProviderAssignment({ - config: { environment: 'local', env: {} } as never, - client: { - failAssignment: async (_id: string, request: Record) => { - failures.push(request); - return { ok: true, payload: { status: 'failed' } }; - }, - } as never, - assignment: assignment({ reservationId: null }), - leaseToken: 'lease-1', - runnerId: 'runner-1', - leaseSeconds: 60, - renewLease: async () => undefined, - kernel: { runAssignment: async () => { kernelCalls += 1; return {} as never; } }, - }); - expect(result).toEqual({ ok: true, payload: { status: 'failed' } }); - expect(kernelCalls).toBe(0); - expect(failures).toHaveLength(1); - expect(failures[0]).toMatchObject({ code: 'workday_assignment_authority_invalid', retryable: false }); - }); -}); diff --git a/tests/unit/provider/configuration/runtime-version.test.ts b/tests/unit/provider/configuration/runtime-version.test.ts deleted file mode 100644 index 8e523d98..00000000 --- a/tests/unit/provider/configuration/runtime-version.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { providerRuntimeVersion } from '../../../../src/provider/configuration/config.ts'; - -describe('capacity-provider runtime version', () => { - it('defaults to the exact package version and permits an explicit image override', () => { - const packageVersion = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')).version; - expect(providerRuntimeVersion({})).toBe(packageVersion); - expect(providerRuntimeVersion({ TREESEED_PROVIDER_RUNTIME_VERSION: 'image-build-1' })).toBe('image-build-1'); - }); -}); diff --git a/tests/unit/provider/disk-capacity.test.ts b/tests/unit/provider/disk-capacity.test.ts deleted file mode 100644 index e3cb76e5..00000000 --- a/tests/unit/provider/disk-capacity.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { evaluateProviderDiskCapacity } from '../../../src/provider/runtime/disk-capacity.ts'; - -describe('provider disk capacity admission', () => { - it('keeps a reserve and assignment headroom before accepting work', () => { - const observation = evaluateProviderDiskCapacity({ - path: '/data', - totalBytes: 100 * 1024 ** 3, - availableBytes: 20 * 1024 ** 3, - assignmentHeadroomBytes: 2 * 1024 ** 3, - }); - - expect(observation).toMatchObject({ ok: true, reserveBytes: 10 * 1024 ** 3 }); - expect(observation.requiredAvailableBytes).toBe(12 * 1024 ** 3); - }); - - it('fails closed before a lease when the protected reserve would be crossed', () => { - const observation = evaluateProviderDiskCapacity({ - path: '/data', - totalBytes: 100 * 1024 ** 3, - availableBytes: 11 * 1024 ** 3, - assignmentHeadroomBytes: 2 * 1024 ** 3, - }); - - expect(observation.ok).toBe(false); - expect(observation.deficitBytes).toBe(1024 ** 3); - expect(observation.reason).toContain('provider-disk-capacity-insufficient'); - }); -}); diff --git a/tests/unit/provider/error-diagnostics.test.ts b/tests/unit/provider/error-diagnostics.test.ts deleted file mode 100644 index ae6d91c9..00000000 --- a/tests/unit/provider/error-diagnostics.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe,expect,it } from 'vitest'; -import { providerErrorDiagnostic,providerErrorIsRetryable } from '../../../src/provider/reporting/error-diagnostics.ts'; - -describe('provider error diagnostics', () => { - it('preserves actionable proposal requirements and treats contract conflicts as terminal', () => { - const error = Object.assign(new Error('Proposal is incomplete.'), { - status: 409, - code: 'assignment_proposal_plan_incomplete', - details: { - contentPath: 'src/content/proposals/guide.mdx', - missingRequirements: ['research evidence'], - }, - }); - expect(providerErrorDiagnostic(error, 'assignment_processing')).toMatchObject({ - code: 'assignment_proposal_plan_incomplete', - path: 'src/content/proposals/guide.mdx', - missingRequirements: ['research evidence'], - }); - expect(providerErrorIsRetryable(error)).toBe(false); - }); - - it('keeps transport and server failures retryable', () => { - expect(providerErrorIsRetryable(new Error('Connection reset.'))).toBe(true); - expect(providerErrorIsRetryable(Object.assign(new Error('Unavailable.'), { status: 503 }))).toBe(true); - }); -}); diff --git a/tests/unit/provider/runtime-event-reporter.test.ts b/tests/unit/provider/runtime-event-reporter.test.ts deleted file mode 100644 index c19785db..00000000 --- a/tests/unit/provider/runtime-event-reporter.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe,expect,it,vi } from 'vitest'; -import { reportProviderRuntimeEvent } from '../../../src/provider/reporting/runtime-event-reporter.ts'; - -const event = { - id: 'event-1', eventType: 'provider.execution.started' as const, status: 'active' as const, - component: 'execution-provider' as const, message: 'Codex invocation started.', -}; - -describe('provider runtime event reporting', () => { - it('delivers a typed runtime event once', async () => { - const createAssignmentEvent = vi.fn().mockResolvedValue({ ok: true }); - const delivered = await reportProviderRuntimeEvent({ client: { createAssignmentEvent } as never, assignmentId: 'assignment-1', event }); - expect(delivered).toBe(true); - expect(createAssignmentEvent).toHaveBeenCalledOnce(); - }); - - it('retries interrupted delivery without failing assignment execution', async () => { - const createAssignmentEvent = vi.fn().mockRejectedValue(new Error('offline')); - const error = vi.spyOn(console, 'error').mockImplementation(() => {}); - const delivered = await reportProviderRuntimeEvent({ client: { createAssignmentEvent } as never, assignmentId: 'assignment-1', event }); - expect(delivered).toBe(false); - expect(createAssignmentEvent).toHaveBeenCalledTimes(3); - expect(error).toHaveBeenCalledWith(expect.stringContaining('provider.runtime_event_delivery_failed')); - error.mockRestore(); - }); -}); diff --git a/tests/unit/provider/treedx-context-adapter.test.ts b/tests/unit/provider/treedx-context-adapter.test.ts deleted file mode 100644 index 0423563c..00000000 --- a/tests/unit/provider/treedx-context-adapter.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { createAssignmentTreeDxAdapter } from '../../../src/provider/treedx/graph/treedx-context-adapter.ts'; -import type { ProviderConnectionRuntimeContext } from '../../../src/provider/configuration/config.ts'; - -afterEach(() => vi.unstubAllGlobals()); - -describe('assignment TreeDX context adapter', () => { - it('retries a transient transport failure against the same authoritative path', async () => { - const fetchMock = vi.fn() - .mockRejectedValueOnce(new TypeError('fetch failed')) - .mockResolvedValueOnce(new Response(JSON.stringify({ payload: { file: { path: 'src/content/agents/editorial/guide-steward.mdx', content: 'agent' } } }), { - status: 200, - headers: { 'content-type': 'application/json' }, - })); - vi.stubGlobal('fetch', fetchMock); - const adapter = createAssignmentTreeDxAdapter({ - config: { marketUrl: 'http://api.test', accessToken: 'secret' } as ProviderConnectionRuntimeContext, - projectId: 'project-1', - assignmentId: 'assignment-1', - treedxProxyHandle: { - id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', - repositoryId: 'repo-1', status: 'issued', allowedOperations: ['files:read'], allowedPaths: ['**'], - }, - }); - const result = await adapter?.readRepositoryFiles({ repoId: 'repo-1', paths: ['src/content/agents/editorial/guide-steward.mdx'] }); - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(fetchMock.mock.calls.map((call) => JSON.parse(String(call[1]?.body)).path)).toEqual([ - 'src/content/agents/editorial/guide-steward.mdx', - 'src/content/agents/editorial/guide-steward.mdx', - ]); - expect(result?.files).toHaveLength(1); - }); - - it('does not retry a missing path', async () => { - const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ error: { message: 'File not found.' } }), { - status: 404, - headers: { 'content-type': 'application/json' }, - })); - vi.stubGlobal('fetch', fetchMock); - const adapter = createAssignmentTreeDxAdapter({ - config: { marketUrl: 'http://api.test', accessToken: 'secret' } as ProviderConnectionRuntimeContext, - projectId: 'project-1', assignmentId: 'assignment-1', - treedxProxyHandle: { id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', repositoryId: 'repo-1', status: 'issued', allowedOperations: ['files:read'], allowedPaths: ['**'] }, - }); - await expect(adapter?.readRepositoryFiles({ repoId: 'repo-1', paths: ['missing.mdx'] })).rejects.toThrow(/404/u); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - it('bounds internally assembled context packs before proxy transport', async () => { - const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ payload: { nodes: [] } }), { status: 200, headers: { 'content-type': 'application/json' } })); - vi.stubGlobal('fetch', fetchMock); - const adapter = createAssignmentTreeDxAdapter({ - config: { marketUrl: 'http://api.test', accessToken: 'secret' } as ProviderConnectionRuntimeContext, - projectId: 'project-1', assignmentId: 'assignment-1', - treedxProxyHandle: { id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', repositoryId: 'repo-1', baseCommitSha: 'a'.repeat(40), status: 'issued', allowedOperations: ['files:read'], allowedPaths: ['**'] }, - }); - await adapter?.buildContext({ repoId: 'repo-1', query: 'guide', body: { limit: 24, format: 'full' } }); - const sent = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)); - expect(sent).toMatchObject({ query: 'guide', ref:'a'.repeat(40), limit: 24, format: 'full', budget: { maxNodes: 8, maxTokens: 1800 } }); - }); - - it('defaults repository reads and listings to immutable assignment custody while preserving an explicit ref', async () => { - const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ payload: { files: [] } }), { status: 200, headers: { 'content-type': 'application/json' } })); - vi.stubGlobal('fetch', fetchMock); - const adapter = createAssignmentTreeDxAdapter({ - config:{marketUrl:'http://api.test',accessToken:'secret'} as ProviderConnectionRuntimeContext,projectId:'project-1',assignmentId:'assignment-1', - treedxProxyHandle:{id:'handle-1',teamId:'team-1',projectId:'project-1',assignmentId:'assignment-1',repositoryId:'repo-1',baseCommitSha:'b'.repeat(40),status:'issued',allowedOperations:['files:read'],allowedPaths:['**']}, - }); - await adapter?.readRepositoryFiles({repoId:'repo-1',paths:['template.mdx']}); - await adapter?.listRepositoryPaths?.({repoId:'repo-1',path:'src/content',ref:'c'.repeat(40)}); - expect(fetchMock.mock.calls.map((call)=>JSON.parse(String(call[1]?.body)).ref)).toEqual(['b'.repeat(40),'c'.repeat(40)]); - }); - - it('resolves a current provider access token for every TreeDX request', async () => { - const accessTokenProvider = vi.fn() - .mockResolvedValueOnce('token-before-refresh') - .mockResolvedValueOnce('token-after-refresh'); - const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ payload: {} }), { - status: 200, - headers: { 'content-type': 'application/json' }, - })); - vi.stubGlobal('fetch', fetchMock); - const adapter = createAssignmentTreeDxAdapter({ - config: { marketUrl: 'http://api.test', accessToken: 'expired', accessTokenProvider } as ProviderConnectionRuntimeContext, - projectId: 'project-1', assignmentId: 'assignment-1', - treedxProxyHandle: { - id: 'handle-1', teamId: 'team-1', projectId: 'project-1', assignmentId: 'assignment-1', - repositoryId: 'repo-1', workspaceId: 'workspace-1', status: 'issued', - allowedOperations: ['files:read', 'workspace:write'], allowedPaths: ['**'], - }, - }); - await adapter?.readRepositoryFiles({ repoId: 'repo-1', paths: ['src/content/objectives/core.mdx'] }); - await adapter?.closeWorkspace({ workspaceId: 'workspace-1' }); - expect(accessTokenProvider).toHaveBeenCalledTimes(2); - expect(fetchMock.mock.calls.map((call) => new Headers(call[1]?.headers).get('authorization'))).toEqual([ - 'Bearer token-before-refresh', - 'Bearer token-after-refresh', - ]); - }); -}); diff --git a/treeseed.package.yaml b/treeseed.package.yaml index 86becef4..32e4d18b 100644 --- a/treeseed.package.yaml +++ b/treeseed.package.yaml @@ -61,10 +61,6 @@ capacityProvider: projectName: treeseed-capacity-provider sharedVolume: true dataDir: .treeseed/local-capacity-providers/agent-standalone - railway: - runnerCount: 1 - runnerVolumePolicy: per-service - volumeMountPath: /data capabilities: [] executionProviders: [] envBindings: [] diff --git a/vitest.config.ts b/vitest.config.ts index ca617ef0..28e96562 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,37 +1,7 @@ -import { existsSync } from 'node:fs'; -import { resolve } from 'node:path'; import { defineConfig } from 'vitest/config'; -const workspaceSdkRoot = resolve(process.cwd(), '../sdk'); -const useWorkspaceSdk = existsSync(resolve(workspaceSdkRoot, 'src/index.ts')); - -function resolveWorkspaceSdkSource(source: string) { - if (source === '@treeseed/sdk') { - return resolve(workspaceSdkRoot, 'src/index.ts'); - } - if (!source.startsWith('@treeseed/sdk/')) { - return null; - } - const subpath = source.slice('@treeseed/sdk/'.length); - const candidates = [ - resolve(workspaceSdkRoot, 'src', `${subpath}.ts`), - resolve(workspaceSdkRoot, 'src', subpath, 'index.ts'), - ]; - return candidates.find((candidate) => existsSync(candidate)) ?? null; -} - export default defineConfig({ - plugins: useWorkspaceSdk - ? [{ - name: 'treeseed-sdk-typescript-source', - enforce: 'pre', - resolveId(source) { - return resolveWorkspaceSdkSource(source); - }, - }] - : undefined, test: { - include: ['tests/{unit,integration,contract,acceptance}/**/*.test.ts'], - setupFiles: ['tests/support/setup-runtime.ts'], + include: ['tests/modern/**/*.test.ts'], }, }); From a87b6c91d34c019b2d4f52da76c021705691a214 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Sat, 22 Aug 2026 06:48:04 -0400 Subject: [PATCH 2/3] ci: remove obsolete SDK content publisher workflow --- .github/workflows/content.yml | 81 ----------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 .github/workflows/content.yml diff --git a/.github/workflows/content.yml b/.github/workflows/content.yml deleted file mode 100644 index 4f959bc3..00000000 --- a/.github/workflows/content.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Content - -on: - push: - paths: - - 'docs/src/content/**' - - '.github/workflows/content.yml' - pull_request: - paths: - - 'docs/src/content/**' - - '.github/workflows/content.yml' - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: content-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - publish: - runs-on: ubuntu-latest - environment: ${{ github.event_name == 'pull_request' && 'preview' || (github.ref_name == 'main' && 'production' || (github.ref_name == 'staging' && 'staging' || 'preview')) }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha || github.sha }} - - - name: Checkout canonical content publisher - uses: actions/checkout@v4 - with: - repository: treeseed-ai/sdk - ref: ${{ github.event_name == 'pull_request' && github.base_ref || (github.ref_name == 'main' && 'main' || 'staging') }} - path: .treeseed/content-publisher - persist-credentials: false - - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: npm - cache-dependency-path: .treeseed/content-publisher/package-lock.json - - - name: Build canonical content publisher - working-directory: .treeseed/content-publisher - run: npm ci --ignore-scripts && npm run build:dist - - - name: Validate and publish exact content commit - env: - TREESEED_TEAM_ID: treeseed - TREESEED_PROJECT_ID: agent - TREESEED_CLOUDFLARE_ACCOUNT_ID: ${{ vars.TREESEED_CLOUDFLARE_ACCOUNT_ID }} - TREESEED_CONTENT_BUCKET_NAME: ${{ vars.TREESEED_CONTENT_BUCKET_NAME }} - TREESEED_R2_ACCESS_KEY_ID: ${{ secrets.TREESEED_R2_ACCESS_KEY_ID }} - TREESEED_R2_SECRET_ACCESS_KEY: ${{ secrets.TREESEED_R2_SECRET_ACCESS_KEY }} - run: | - set -euo pipefail - SOURCE_COMMIT="$(git rev-parse HEAD)" - test "${SOURCE_COMMIT}" = "${{ github.event.pull_request.head.sha || github.sha }}" - CHANNEL=preview - if test "${GITHUB_REF_NAME}" = main; then CHANNEL=production; fi - if test "${GITHUB_REF_NAME}" = staging; then CHANNEL=staging; fi - VALIDATE=(--validate-only) - if test "${GITHUB_REF_NAME}" = main && test "${{ github.event_name }}" != pull_request; then - VALIDATE=() - node ./.treeseed/content-publisher/dist/scripts/content/publish-content.js --acceptance --team-id "${TREESEED_TEAM_ID}" > r2-acceptance-receipt.json - fi - node ./.treeseed/content-publisher/dist/scripts/content/publish-content.js \ - --root . \ - --content-path docs/src/content \ - --source-commit "${SOURCE_COMMIT}" \ - --ref "${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" \ - --channel "${CHANNEL}" \ - "${VALIDATE[@]}" > content-publication-receipt.json - - - name: Upload publication receipt - uses: actions/upload-artifact@v4 - with: - name: content-publication-${{ github.sha }} - path: content-publication-receipt.json From 6bef0d8d21768c1ec79915d24fe194b1a480170e Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Sat, 22 Aug 2026 06:51:16 -0400 Subject: [PATCH 3/3] fix: renew assignment authority during execution --- src/provider/lifecycle/lifecycle.ts | 2 +- src/provider/operations/runner.ts | 44 +++++++++++++++++++++++- src/provider/teams/multi-team-runtime.ts | 5 +++ tests/modern/provider-runner.test.ts | 26 ++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/provider/lifecycle/lifecycle.ts b/src/provider/lifecycle/lifecycle.ts index 612e3422..c9db8d0d 100644 --- a/src/provider/lifecycle/lifecycle.ts +++ b/src/provider/lifecycle/lifecycle.ts @@ -30,7 +30,7 @@ export async function buildProviderPlan(config: ProviderHostRuntimeConfig) { return okPayload('plan', { mode: 'plan', dataDir: config.dataDir, - capabilities: discoverProviderCapabilities(config), + capabilities: discoverProviderCapabilities(config).map((capability) => capability.id), budgets: discoverProviderBudgets(config), executorConfigured: Boolean(config.executorModule), redactedEnv: config.redactedEnv, diff --git a/src/provider/operations/runner.ts b/src/provider/operations/runner.ts index b4a2f922..77adcdb3 100644 --- a/src/provider/operations/runner.ts +++ b/src/provider/operations/runner.ts @@ -10,11 +10,14 @@ function text(...values: unknown[]) { } export interface ProviderAssignmentRunInput { - client: Pick; + client: Pick; executor: AgentExecutor; assignment: Record; leaseToken: string; runnerId: string; + leaseSeconds?: number; + renewalIntervalMs?: number; + onLeaseRenewed?: (leaseExpiresAt: string) => Promise; signal?: AbortSignal; } @@ -29,10 +32,49 @@ export async function runProviderAssignment(input: ProviderAssignmentRunInput) { if (!assignmentId) throw new Error('Catalogued assignment lease omitted its stable id.'); await input.client.startAssignmentExecution(assignmentId, { leaseToken: input.leaseToken, runnerId: input.runnerId, executorId: input.executor.id }); let result: AgentExecutionResult; + let stopped = false; + let renewalFailure: unknown = null; + let timer: ReturnType | null = null; + let renewalInFlight: Promise | null = null; + const scheduleRenewal = () => { + timer = setTimeout(() => { + renewalInFlight = renew(); + }, input.renewalIntervalMs ?? Math.max(30_000, (input.leaseSeconds ?? 300) * 500)); + }; + const renew = async (): Promise => { + if (stopped) return; + try { + const renewed = await input.client.renewAssignment(assignmentId, { + leaseToken: input.leaseToken, + runnerId: input.runnerId, + leaseSeconds: input.leaseSeconds ?? 300, + }); + const assignment = record(renewed.assignment ?? renewed.payload); + const leaseExpiresAt = text(assignment.leaseExpiresAt); + if (leaseExpiresAt) await input.onLeaseRenewed?.(leaseExpiresAt); + } catch (error) { + renewalFailure = error; + return; + } + scheduleRenewal(); + }; + scheduleRenewal(); try { result = await input.executor.execute({ assignment: input.assignment, assignmentId, leaseToken: input.leaseToken, runnerId: input.runnerId, signal: input.signal }); } catch (error) { result = { status: 'failed', code: 'agent_executor_failed', summary: error instanceof Error ? error.message : String(error), retryable: true }; + } finally { + stopped = true; + if (timer) clearTimeout(timer); + await renewalInFlight; + } + if (renewalFailure) { + result = { + status: 'returned', + code: 'assignment_lease_renewal_failed', + summary: renewalFailure instanceof Error ? renewalFailure.message : String(renewalFailure), + retryable: true, + }; } await reportUsage(input, assignmentId, result); if (result.status === 'returned') { diff --git a/src/provider/teams/multi-team-runtime.ts b/src/provider/teams/multi-team-runtime.ts index e0ca8746..4acda922 100644 --- a/src/provider/teams/multi-team-runtime.ts +++ b/src/provider/teams/multi-team-runtime.ts @@ -159,6 +159,11 @@ export async function runMultiTeamProviderRunners( assignment, leaseToken, runnerId: claim.runnerId, + leaseSeconds: 300, + onLeaseRenewed: (renewedLeaseExpiresAt) => localState.renewLease(claim.id, { + assignmentId, + leaseExpiresAt: renewedLeaseExpiresAt, + }).then(() => undefined), }); await localState.finalize(claim.id, 'terminal-receipt-confirmed'); results.push({ connectionId: connection.connection.id, assignmentId, status: 'settled', terminal }); diff --git a/tests/modern/provider-runner.test.ts b/tests/modern/provider-runner.test.ts index a7608d54..6bd14905 100644 --- a/tests/modern/provider-runner.test.ts +++ b/tests/modern/provider-runner.test.ts @@ -5,6 +5,7 @@ import type { AgentExecutor } from '../../src/provider/execution/contracts.ts'; function client() { return { startAssignmentExecution: vi.fn().mockResolvedValue({ ok: true }), + renewAssignment: vi.fn().mockResolvedValue({ ok: true, payload: { leaseExpiresAt: new Date(Date.now() + 60_000).toISOString() } }), startAssignmentCloseout: vi.fn().mockResolvedValue({ ok: true }), preflightAssignmentCompletion: vi.fn().mockResolvedValue({ ok: true }), completeAssignment: vi.fn().mockResolvedValue({ ok: true, payload: { status: 'completed' } }), @@ -44,6 +45,31 @@ describe('catalog-driven provider assignment runner', () => { })); }); + it('renews long-running assignment leases and persists the new expiry', async () => { + const api = client(); + const renewed: string[] = []; + const executor: AgentExecutor = { + id: 'fake', + observe: async () => ({ available: true }), + execute: async () => new Promise((resolve) => setTimeout(() => resolve({ + status: 'returned', + summary: 'paused', + }), 20)), + }; + await runProviderAssignment({ + client: api, + executor, + assignment: { id: 'assignment-renew' }, + leaseToken: 'lease', + runnerId: 'runner', + renewalIntervalMs: 5, + onLeaseRenewed: async (value) => { renewed.push(value); }, + }); + expect(api.renewAssignment).toHaveBeenCalled(); + expect(renewed.length).toBeGreaterThan(0); + expect(api.returnAssignment).toHaveBeenCalledOnce(); + }); + it('turns executor exceptions into an exact retryable failure receipt', async () => { const api = client(); const executor: AgentExecutor = {