@moretea-labs/forge 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +4 -0
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/SKILL.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/hooks/hook-input.sh +11 -8
- package/assets/hooks/lib/session-state.sh +10 -7
- package/assets/hooks/lib/workflow-state.sh +13 -177
- package/assets/hooks/post-tool-observer.sh +8 -2
- package/assets/hooks/prompt-guard.sh +5 -81
- package/assets/hooks/session-start-context.sh +56 -1
- package/assets/partials/05-workflow.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/partials-agents/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/contract.template.md +2 -7
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/check-task-workflow.sh +4 -4
- package/assets/templates/helpers/contract-worktree.sh +1 -21
- package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-project-template.sh +22 -45
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +12 -37
- package/assets/templates/helpers/ship-worktrees.sh +2 -29
- package/assets/templates/helpers/verify-contract.sh +1 -111
- package/assets/templates/helpers/verify-sprint.sh +3 -180
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/assets/templates/plan.template.md +1 -1
- package/assets/templates/review.template.md +9 -34
- package/assets/workflow-contract.v1.json +10 -19
- package/docs/README.md +10 -10
- package/docs/architecture/CURRENT.md +178 -0
- package/docs/architecture/EVOLUTION.md +42 -0
- package/docs/architecture/current/README.md +4 -156
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
- package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
- package/docs/architecture/history/README.md +7 -0
- package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
- package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
- package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
- package/docs/architecture/history.md +3 -34
- package/docs/architecture/index.md +15 -98
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
- package/docs/architecture/versions/1.6.md +38 -0
- package/docs/architecture/versions/1.7.md +41 -0
- package/docs/forge-chatgpt-browser-engine.md +2 -2
- package/docs/forge-chatgpt-mcp-setup.md +4 -28
- package/docs/forge-chatgpt-supervised-automation.md +3 -3
- package/docs/forge-github-issue-launcher.md +1 -1
- package/docs/forge-plugin-management.md +1 -1
- package/docs/forge-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +26 -6
- package/docs/operations/controller-performance-and-502.md +143 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +2 -2
- package/docs/operations/github-repository.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +5 -3
- package/docs/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Integrations.md +2 -2
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/package.json +33 -11
- package/scripts/activate-source-baseline.ts +17 -13
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-background-check-overlap.ts +228 -0
- package/scripts/benchmark-mcp-host.sh +117 -0
- package/scripts/benchmark-mcp-transport.ts +170 -0
- package/scripts/benchmark-quality-harness.ts +146 -0
- package/scripts/benchmark-route-session-concurrency.ts +207 -6
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
- package/scripts/benchmark-typescript-navigation.ts +186 -0
- package/scripts/benchmark-work-efficiency.ts +334 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1558
- package/scripts/check-architecture-sync.sh +2 -371
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-runtime-architecture.mjs +349 -79
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -722
- package/scripts/ensure-task-workflow.sh +1 -1225
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +22 -46
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -1189
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -796
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +5 -1
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -558
- package/scripts/smoke-ios-physical-device.ts +4 -2
- package/scripts/smoke-runtime-recovery.ts +1 -35
- package/scripts/smoke-schedule-engine.ts +1 -20
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -703
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +1 -677
- package/scripts/workflow-contract.ts +4 -205
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/commands/recovery.ts +25 -3
- package/src/cli/commands/runtime.ts +55 -3
- package/src/cli/commands/security.ts +12 -1
- package/src/cli/commands/setup-profile.ts +31 -21
- package/src/cli/controller/check-runner.ts +113 -5
- package/src/cli/controller/composite-result.ts +1 -1
- package/src/cli/controller/context/focus.ts +51 -0
- package/src/cli/controller/context/instruction-context.ts +114 -0
- package/src/cli/controller/context/known-paths.ts +76 -0
- package/src/cli/controller/context/query-planning.ts +110 -0
- package/src/cli/controller/context/rule-resolution.ts +532 -0
- package/src/cli/controller/context/source-materializer.ts +278 -0
- package/src/cli/controller/context/structural.ts +16 -0
- package/src/cli/controller/context/types.ts +286 -0
- package/src/cli/controller/context-pack.ts +828 -487
- package/src/cli/controller/launch-agents.ts +77 -2
- package/src/cli/controller/operational-plan.ts +2 -2
- package/src/cli/controller/work-mode.ts +6 -4
- package/src/cli/editing/edit-session.ts +60 -7
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +135 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +54 -78
- package/src/cli/local-bridge/console-view-models.ts +1 -35
- package/src/cli/local-bridge/facade-api.ts +37 -264
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
- package/src/cli/local-bridge/frontend/src/styles.css +1 -1
- package/src/cli/local-bridge/frontend/src/types.ts +5 -8
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +2 -515
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
- package/src/cli/mcp/auth.ts +5 -5
- package/src/cli/mcp/instructions.ts +3 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -222
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +282 -146
- package/src/cli/mcp/server.ts +586 -116
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +44 -7
- package/src/cli/mcp/transports/http.ts +124 -14
- package/src/cli/mcp/transports/session-registry.ts +43 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +103 -21
- package/src/cli/repositories/command-executor.ts +78 -526
- package/src/cli/repositories/command-process.ts +108 -0
- package/src/cli/repositories/command-scope.ts +99 -41
- package/src/cli/repositories/controller-home.ts +7 -8
- package/src/cli/repositories/git-command-executor.ts +2 -0
- package/src/cli/repositories/locks.ts +53 -16
- package/src/cli/repositories/repository-snapshot.ts +124 -0
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +118 -6
- package/src/cli/repository/inspector.ts +563 -65
- package/src/cli/repository/session-cache.ts +58 -17
- package/src/cli/v81-entry.ts +1 -4
- package/src/effects/fs-transaction.ts +85 -17
- package/src/runtime/context/codegraph-read-provider.ts +233 -2
- package/src/runtime/context/codegraph-sidecar.cjs +74 -40
- package/src/runtime/context/generic-lsp-provider.ts +355 -0
- package/src/runtime/context/lsp-client.ts +283 -0
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +223 -0
- package/src/runtime/context/swift-navigation.ts +565 -0
- package/src/runtime/context/typescript-navigation.ts +226 -0
- package/src/runtime/control-plane/dispatch-priority.ts +28 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
- package/src/runtime/control-plane/facade/index.ts +3 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
- package/src/runtime/control-plane/facade/types.ts +90 -10
- package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
- package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
- package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
- package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
- package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
- package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
- package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
- package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
- package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
- package/src/runtime/control-plane/release-retention.ts +62 -2
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +24 -21
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/control-plane/runtime-status-client.ts +0 -2
- package/src/runtime/diagnostics/mcp-timing.ts +13 -0
- package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
- package/src/runtime/diagnostics/performance.ts +24 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/jobs/store.ts +58 -4
- package/src/runtime/execution/jobs/types.ts +1 -1
- package/src/runtime/execution/managed-workspace.ts +203 -1
- package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
- package/src/runtime/execution/process-runtime/check-result.ts +88 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
- package/src/runtime/execution/process-runtime/gc.ts +38 -6
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
- package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
- package/src/runtime/execution/process-runtime/runtime.ts +53 -3
- package/src/runtime/execution/process-runtime/store.ts +1 -0
- package/src/runtime/execution/process-runtime/types.ts +4 -12
- package/src/runtime/execution/thin-harness/async-process.ts +12 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -61
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +186 -84
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
- package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
- package/src/runtime/plugins/browser-adapter.ts +2662 -829
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +110 -5
- package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-node-bridge.ts +6 -34
- package/src/runtime/plugins/browser-provider-registry.ts +109 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
- package/src/runtime/plugins/browser-runtime.ts +193 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
- package/src/runtime/plugins/external-adapter.ts +358 -3
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +2 -0
- package/src/runtime/plugins/github-adapter.ts +125 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
- package/src/runtime/plugins/ios-adapter.ts +106 -16
- package/src/runtime/plugins/ios-agent-device.ts +234 -64
- package/src/runtime/plugins/ios-physical-device.ts +1143 -392
- package/src/runtime/plugins/lightweight-action.ts +153 -0
- package/src/runtime/plugins/local-system-adapter.ts +123 -6
- package/src/runtime/plugins/macos-capability-broker.ts +47 -9
- package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
- package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +330 -33
- package/src/runtime/plugins/types.ts +38 -3
- package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
- package/src/runtime/projections/git-status-sampler.ts +16 -16
- package/src/runtime/projections/materialized-view.ts +28 -10
- package/src/runtime/recovery/maintenance-executor.ts +426 -25
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +230 -10
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +461 -22
- package/src/runtime/root/release-manifest.ts +144 -18
- package/src/runtime/root/release-materialize.ts +125 -32
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -29
- package/src/runtime/root/types.ts +12 -0
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- package/src/runtime/shared/trusted-executable.ts +62 -0
- package/src/runtime/shared/trusted-node-executable.ts +48 -0
- package/src/runtime/standalone-recovery/core.ts +1079 -240
- package/src/runtime/standalone-recovery/entry.ts +100 -24
- package/src/runtime/standalone-recovery/installer.ts +20 -4
- package/src/runtime/standalone-recovery/service-labels.ts +2 -0
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
- package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
- package/src/runtime/workflow/schedules/engine.ts +364 -202
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +64 -17
- package/src/runtime/workflow/schedules/store.ts +209 -19
- package/src/runtime/workflow/schedules/types.ts +2 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
- package/src/testing/test-governance.ts +8 -40
- package/assets/reference-configs/heartbeat-triage.md +0 -57
- package/assets/templates/helpers/harness-trace-grade.sh +0 -150
- package/assets/templates/helpers/heartbeat-triage.sh +0 -361
- package/assets/templates/helpers/maintenance-triage.sh +0 -167
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
- package/docs/architecture/current/architecture-invariants.md +0 -331
- package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
- package/docs/architecture/current/codegraph-context-provider.md +0 -238
- package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
- package/docs/architecture/current/controller-repository-decoupling.md +0 -148
- package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
- package/docs/architecture/current/entity-model.md +0 -612
- package/docs/architecture/current/external-plugin-governance.md +0 -396
- package/docs/architecture/current/failure-recovery.md +0 -447
- package/docs/architecture/current/governance.md +0 -213
- package/docs/architecture/current/human-interaction-plane.md +0 -87
- package/docs/architecture/current/implementation-status.md +0 -128
- package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
- package/docs/architecture/current/legacy-governance.md +0 -58
- package/docs/architecture/current/local-workspace-target-grants.md +0 -70
- package/docs/architecture/current/migration-roadmap.md +0 -105
- package/docs/architecture/current/multi-repository-execution.md +0 -371
- package/docs/architecture/current/operations-runbook.md +0 -45
- package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
- package/docs/architecture/current/repository-access-modes.md +0 -102
- package/docs/architecture/current/route-policy.md +0 -15
- package/docs/architecture/current/route-session-concurrency.md +0 -85
- package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
- package/docs/architecture/current/runtime-directory-map.md +0 -49
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
- package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
- package/docs/architecture/current/system-overview.md +0 -169
- package/docs/architecture/current/target-requirements-traceability.md +0 -202
- package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
- package/docs/architecture/current/thin-harness-v1.md +0 -321
- package/docs/architecture/current/verification-and-release-gates.md +0 -391
- package/docs/forge-autonomous-goal-loop.md +0 -9
- package/docs/forge-provider-configuration.md +0 -7
- package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
- package/docs/operations/gmail-assistant-routines.md +0 -186
- package/docs/operations/personal-assistant-operator-guide.md +0 -61
- package/docs/operations/provider-configuration.md +0 -31
- package/scripts/harness-trace-grade.sh +0 -150
- package/scripts/heartbeat-triage.sh +0 -361
- package/scripts/maintenance-triage.sh +0 -167
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
- package/src/cli/repositories/goal-registry.ts +0 -239
- package/src/runtime/assistant/action-proposals.ts +0 -279
- package/src/runtime/assistant/intent.ts +0 -395
- package/src/runtime/assistant/model-provider.ts +0 -460
- package/src/runtime/assistant/openapi.ts +0 -223
- package/src/runtime/assistant/policy.ts +0 -89
- package/src/runtime/assistant/readiness.ts +0 -258
- package/src/runtime/assistant/routine-runtime.ts +0 -477
- package/src/runtime/assistant/schedule-binding.ts +0 -171
- package/src/runtime/assistant/standing-grants.ts +0 -375
- package/src/runtime/assistant/store.ts +0 -243
- package/src/runtime/assistant/types.ts +0 -135
- package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
- package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
- package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
- package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
- package/src/runtime/control-plane/goal-loop/index.ts +0 -13
- package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
- package/src/runtime/control-plane/goal-loop/types.ts +0 -378
- package/src/runtime/control-plane/startup-recovery.ts +0 -198
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
- package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
- package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
- package/src/runtime/workflow/findings/store.ts +0 -204
- package/src/runtime/workflow/findings/types.ts +0 -58
- package/src/runtime/workflow/portfolio/engine.ts +0 -20
- package/src/runtime/workflow/portfolio/store.ts +0 -185
- package/src/runtime/workflow/portfolio/types.ts +0 -29
|
@@ -5,6 +5,7 @@ export * from './work-task-projection';
|
|
|
5
5
|
export * from './work-continuation';
|
|
6
6
|
export * from './controller-session-store';
|
|
7
7
|
export * from './plan-contract-store';
|
|
8
|
+
export * from './semantic-admission';
|
|
8
9
|
export * from './facade-result';
|
|
9
10
|
export * from './suggested-actions';
|
|
10
11
|
export * from './capability-registry';
|
|
@@ -14,3 +15,5 @@ export * from './goal-workloop-access';
|
|
|
14
15
|
export * from './codex-delegation';
|
|
15
16
|
export * from './self-healing-loop';
|
|
16
17
|
export * from './operation-digest';
|
|
18
|
+
export * from './requirement-authority';
|
|
19
|
+
export * from './repository-work-admission';
|
|
@@ -3,12 +3,21 @@ import { join } from 'path';
|
|
|
3
3
|
import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
|
|
4
4
|
import { withControllerLock } from '../../../cli/repositories/locks';
|
|
5
5
|
import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
|
|
6
|
+
import {
|
|
7
|
+
normalizePlanScopeKey,
|
|
8
|
+
resolvePlanAdmission,
|
|
9
|
+
withPlanAdmissionLock,
|
|
10
|
+
withPlanAdmissionLockAsync,
|
|
11
|
+
type PlanAdmissionRelation,
|
|
12
|
+
type PlanAdmissionResolution,
|
|
13
|
+
} from './semantic-admission';
|
|
6
14
|
import {
|
|
7
15
|
listControlPlaneRecords,
|
|
8
16
|
readControlPlaneRecordWithinTransaction,
|
|
9
17
|
withControlPlaneTransaction,
|
|
10
18
|
writeControlPlaneRecordWithinTransaction,
|
|
11
19
|
} from '../persistence/sqlite-store';
|
|
20
|
+
import { readRequirement } from '../persistence/requirement-store';
|
|
12
21
|
import {
|
|
13
22
|
isTerminalPlanContractStatus,
|
|
14
23
|
type EvidenceRef,
|
|
@@ -46,6 +55,18 @@ export interface CreatePlanContractInput {
|
|
|
46
55
|
evidenceRefs?: EvidenceRef[];
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
export interface AdmitPlanContractInput extends CreatePlanContractInput {
|
|
59
|
+
planRelation?: PlanAdmissionRelation;
|
|
60
|
+
relatedPlanId?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RepairDraftPlanContractInput extends Omit<CreatePlanContractInput, 'planId' | 'repoId' | 'requirementId' | 'evidenceRefs'> {
|
|
64
|
+
/** Source revision observed before entering the admission lock; rejects stale draft writers. */
|
|
65
|
+
expectedSourceRevision?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type AdmitPlanContractResult = PlanAdmissionResolution;
|
|
69
|
+
|
|
49
70
|
export interface PlanContractSummary {
|
|
50
71
|
planId: string;
|
|
51
72
|
repoId: string;
|
|
@@ -68,7 +89,7 @@ function bounded(values: readonly string[] | undefined, limit: number, maxLength
|
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
function normalizeScopeKey(value: string): string {
|
|
71
|
-
return
|
|
92
|
+
return normalizePlanScopeKey(value) || 'unknown';
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
function normalizeStep(step: CreatePlanContractInput['steps'][number]): PlanStep {
|
|
@@ -115,6 +136,14 @@ function updatePlanContract(
|
|
|
115
136
|
);
|
|
116
137
|
}
|
|
117
138
|
|
|
139
|
+
function assertRequirementReference(options: PlanContractStoreOptions, requirementId: string | undefined): void {
|
|
140
|
+
const normalized = requirementId?.trim();
|
|
141
|
+
if (!normalized || !options.controllerHome) return;
|
|
142
|
+
if (!readRequirement({ controllerHome: options.controllerHome, now: options.now }, normalized)) {
|
|
143
|
+
throw new Error(`PLAN_REQUIREMENT_NOT_FOUND: ${normalized}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
118
147
|
export function planContractRoot(location: PlanContractStoreLocation): string {
|
|
119
148
|
if (location.root) {
|
|
120
149
|
mkdirSync(location.root, { recursive: true });
|
|
@@ -205,11 +234,10 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
|
|
|
205
234
|
return store;
|
|
206
235
|
}
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
const at = nowIso(options);
|
|
237
|
+
function buildPlanContract(input: CreatePlanContractInput, at: string): PlanContract {
|
|
210
238
|
const planId = sanitizeFileComponent(input.planId);
|
|
211
239
|
if (!String(input.planId ?? '').trim() || planId === 'unknown') throw new Error('plan_id is required');
|
|
212
|
-
|
|
240
|
+
return {
|
|
213
241
|
schemaVersion: 1,
|
|
214
242
|
planId,
|
|
215
243
|
repoId: input.repoId,
|
|
@@ -229,6 +257,38 @@ export function createPlanContract(options: PlanContractStoreOptions, input: Cre
|
|
|
229
257
|
createdAt: at,
|
|
230
258
|
updatedAt: at,
|
|
231
259
|
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function draftContentErrors(plan: PlanContract): string[] {
|
|
263
|
+
const errors: string[] = [];
|
|
264
|
+
if (!plan.sourceRevision) errors.push('source_revision is required');
|
|
265
|
+
if (!plan.scopeKey || plan.scopeKey === 'unknown') errors.push('scope_key is required');
|
|
266
|
+
if (!plan.goal) errors.push('goal is required');
|
|
267
|
+
if (plan.steps.length === 0) errors.push('at least one plan step is required');
|
|
268
|
+
const ids = new Set<string>();
|
|
269
|
+
for (const step of plan.steps) {
|
|
270
|
+
if (!step.id || step.id === 'unknown') errors.push('every plan step needs an id');
|
|
271
|
+
else if (ids.has(step.id)) errors.push(`duplicate plan step id: ${step.id}`);
|
|
272
|
+
else ids.add(step.id);
|
|
273
|
+
if (!step.objective) errors.push(`step ${step.id || 'unknown'} needs an objective`);
|
|
274
|
+
if (step.checks.length === 0) errors.push(`step ${step.id || 'unknown'} needs at least one machine-checkable check`);
|
|
275
|
+
if (step.acceptanceCriteria.length === 0) errors.push(`step ${step.id || 'unknown'} needs acceptance criteria`);
|
|
276
|
+
if (step.status !== 'pending') errors.push(`step ${step.id || 'unknown'} must be pending before approval`);
|
|
277
|
+
}
|
|
278
|
+
for (const step of plan.steps) {
|
|
279
|
+
for (const dependency of step.dependencies) {
|
|
280
|
+
const dependencyStep = plan.steps.find((candidate) => candidate.id === dependency);
|
|
281
|
+
if (!dependencyStep) errors.push(`step ${step.id} references unknown dependency ${dependency}`);
|
|
282
|
+
else if (dependencyStep.status !== 'pending') errors.push(`step ${step.id} dependency ${dependency} must be pending before approval`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return [...new Set(errors)];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function createPlanContractUnlocked(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
|
|
289
|
+
assertRequirementReference(options, input.requirementId);
|
|
290
|
+
const at = nowIso(options);
|
|
291
|
+
const plan = buildPlanContract(input, at);
|
|
232
292
|
const store = readPlanContractStore(options);
|
|
233
293
|
if (store.contracts.some((existing) => existing.planId === plan.planId)) {
|
|
234
294
|
throw new Error(`plan contract already exists: ${plan.planId}`);
|
|
@@ -237,6 +297,135 @@ export function createPlanContract(options: PlanContractStoreOptions, input: Cre
|
|
|
237
297
|
return plan;
|
|
238
298
|
}
|
|
239
299
|
|
|
300
|
+
function replacePlanContractUnlocked(
|
|
301
|
+
options: PlanContractStoreOptions,
|
|
302
|
+
predecessorId: string,
|
|
303
|
+
input: CreatePlanContractInput,
|
|
304
|
+
): PlanContract {
|
|
305
|
+
assertRequirementReference(options, input.requirementId);
|
|
306
|
+
const at = nowIso(options);
|
|
307
|
+
const store = readPlanContractStore(options);
|
|
308
|
+
const predecessorKey = sanitizeFileComponent(predecessorId);
|
|
309
|
+
const predecessorIndex = store.contracts.findIndex((contract) => contract.planId === predecessorKey);
|
|
310
|
+
if (predecessorIndex < 0) throw new Error(`plan contract not found: ${predecessorKey}`);
|
|
311
|
+
const predecessor = store.contracts[predecessorIndex]!;
|
|
312
|
+
if (isTerminalPlanContractStatus(predecessor.status)) throw new Error(`plan contract ${predecessor.planId} is terminal (${predecessor.status})`);
|
|
313
|
+
const successor = buildPlanContract(input, at);
|
|
314
|
+
if (successor.planId === predecessor.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
|
|
315
|
+
if (successor.repoId !== predecessor.repoId) throw new Error('PLAN_SUCCESSOR_REPOSITORY_MISMATCH');
|
|
316
|
+
if (store.contracts.some((existing) => existing.planId === successor.planId)) {
|
|
317
|
+
throw new Error(`plan contract already exists: ${successor.planId}`);
|
|
318
|
+
}
|
|
319
|
+
const conflictingScope = store.contracts.find((existing, index) => index !== predecessorIndex
|
|
320
|
+
&& !isTerminalPlanContractStatus(existing.status)
|
|
321
|
+
&& existing.scopeKey === successor.scopeKey);
|
|
322
|
+
if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${successor.scopeKey}:${conflictingScope.planId}`);
|
|
323
|
+
const contracts = [...store.contracts];
|
|
324
|
+
contracts[predecessorIndex] = { ...predecessor, status: 'superseded', supersededBy: successor.planId, updatedAt: at };
|
|
325
|
+
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts: [successor, ...contracts] });
|
|
326
|
+
return successor;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function repairDraftPlanContractUnlocked(
|
|
330
|
+
options: PlanContractStoreOptions,
|
|
331
|
+
planId: string,
|
|
332
|
+
input: RepairDraftPlanContractInput,
|
|
333
|
+
): PlanContract {
|
|
334
|
+
const store = readPlanContractStore(options);
|
|
335
|
+
const key = sanitizeFileComponent(planId);
|
|
336
|
+
const index = store.contracts.findIndex((contract) => contract.planId === key);
|
|
337
|
+
if (index < 0) throw new Error(`plan contract not found: ${key}`);
|
|
338
|
+
const current = store.contracts[index]!;
|
|
339
|
+
if (current.status !== 'draft') throw new Error(`PLAN_DRAFT_REPAIR_STATUS_INVALID: ${current.planId}:${current.status}`);
|
|
340
|
+
if (input.expectedSourceRevision !== undefined && current.sourceRevision !== input.expectedSourceRevision) {
|
|
341
|
+
throw new Error(`PLAN_DRAFT_REPAIR_STALE_SOURCE: ${current.planId}:expected=${input.expectedSourceRevision}:actual=${current.sourceRevision}`);
|
|
342
|
+
}
|
|
343
|
+
assertRequirementReference(options, current.requirementId);
|
|
344
|
+
const at = nowIso(options);
|
|
345
|
+
const candidate = buildPlanContract({
|
|
346
|
+
...input,
|
|
347
|
+
planId: current.planId,
|
|
348
|
+
repoId: current.repoId,
|
|
349
|
+
requirementId: current.requirementId,
|
|
350
|
+
evidenceRefs: current.evidenceRefs,
|
|
351
|
+
}, at);
|
|
352
|
+
const contentErrors = draftContentErrors(candidate);
|
|
353
|
+
if (contentErrors.length > 0) {
|
|
354
|
+
throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${contentErrors.join('; ')}`);
|
|
355
|
+
}
|
|
356
|
+
const conflictingScope = store.contracts.find((existing, candidateIndex) => candidateIndex !== index
|
|
357
|
+
&& !isTerminalPlanContractStatus(existing.status)
|
|
358
|
+
&& existing.scopeKey === candidate.scopeKey);
|
|
359
|
+
if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${candidate.scopeKey}:${conflictingScope.planId}`);
|
|
360
|
+
const repaired: PlanContract = {
|
|
361
|
+
...candidate,
|
|
362
|
+
createdAt: current.createdAt,
|
|
363
|
+
updatedAt: at,
|
|
364
|
+
};
|
|
365
|
+
const contracts = [...store.contracts];
|
|
366
|
+
contracts[index] = repaired;
|
|
367
|
+
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
368
|
+
return repaired;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function repairDraftPlanContract(
|
|
372
|
+
options: PlanContractStoreOptions,
|
|
373
|
+
planId: string,
|
|
374
|
+
input: RepairDraftPlanContractInput,
|
|
375
|
+
): PlanContract {
|
|
376
|
+
return withPlanAdmissionLock(options, () => repairDraftPlanContractUnlocked(options, planId, input));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function repairDraftPlanContractAsync(
|
|
380
|
+
options: PlanContractStoreOptions,
|
|
381
|
+
planId: string,
|
|
382
|
+
input: RepairDraftPlanContractInput,
|
|
383
|
+
): Promise<PlanContract> {
|
|
384
|
+
return await withPlanAdmissionLockAsync(options, () => repairDraftPlanContractUnlocked(options, planId, input));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function createPlanContract(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
|
|
388
|
+
return withPlanAdmissionLock(options, () => {
|
|
389
|
+
const store = readPlanContractStore(options);
|
|
390
|
+
const planId = sanitizeFileComponent(input.planId);
|
|
391
|
+
if (store.contracts.some((existing) => existing.planId === planId)) {
|
|
392
|
+
throw new Error(`plan contract already exists: ${planId}`);
|
|
393
|
+
}
|
|
394
|
+
const scopeKey = normalizeScopeKey(input.scopeKey);
|
|
395
|
+
const existingScopeAuthority = scopeKey === 'unknown' ? undefined : store.contracts.find((existing) =>
|
|
396
|
+
!isTerminalPlanContractStatus(existing.status) && existing.scopeKey === scopeKey);
|
|
397
|
+
if (existingScopeAuthority) {
|
|
398
|
+
throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${scopeKey}:${existingScopeAuthority.planId}`);
|
|
399
|
+
}
|
|
400
|
+
return createPlanContractUnlocked(options, input);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function admitPlanContractUnlocked(options: PlanContractStoreOptions, input: AdmitPlanContractInput): AdmitPlanContractResult {
|
|
405
|
+
assertRequirementReference(options, input.requirementId);
|
|
406
|
+
const activePlans = listPlanContracts({ ...options, status: 'active', limit: 100 });
|
|
407
|
+
const resolution = resolvePlanAdmission(activePlans, {
|
|
408
|
+
requirementId: input.requirementId,
|
|
409
|
+
scopeKey: input.scopeKey,
|
|
410
|
+
planRelation: input.planRelation,
|
|
411
|
+
relatedPlanId: input.relatedPlanId,
|
|
412
|
+
});
|
|
413
|
+
if (resolution.admissionDecision === 'extend_existing' && resolution.plan) {
|
|
414
|
+
const successor = replacePlanContractUnlocked(options, resolution.plan.planId, input);
|
|
415
|
+
return { ...resolution, admissionDecision: 'create_new', plan: successor };
|
|
416
|
+
}
|
|
417
|
+
if (resolution.admissionDecision !== 'create_new') return resolution;
|
|
418
|
+
return { ...resolution, plan: createPlanContractUnlocked(options, input) };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function admitPlanContract(options: PlanContractStoreOptions, input: AdmitPlanContractInput): AdmitPlanContractResult {
|
|
422
|
+
return withPlanAdmissionLock(options, () => admitPlanContractUnlocked(options, input));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function admitPlanContractAsync(options: PlanContractStoreOptions, input: AdmitPlanContractInput): Promise<AdmitPlanContractResult> {
|
|
426
|
+
return await withPlanAdmissionLockAsync(options, () => admitPlanContractUnlocked(options, input));
|
|
427
|
+
}
|
|
428
|
+
|
|
240
429
|
export function getPlanContract(options: PlanContractStoreOptions, planId: string): PlanContract | undefined {
|
|
241
430
|
return readPlanContractStore(options).contracts.find((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
242
431
|
}
|
|
@@ -266,28 +455,7 @@ export function summarizePlanContract(plan: PlanContract): PlanContractSummary {
|
|
|
266
455
|
}
|
|
267
456
|
|
|
268
457
|
function approvalErrors(plan: PlanContract, allPlans: readonly PlanContract[]): string[] {
|
|
269
|
-
const errors
|
|
270
|
-
if (!plan.sourceRevision) errors.push('source_revision is required before approval');
|
|
271
|
-
if (!plan.scopeKey || plan.scopeKey === 'unknown') errors.push('scope_key is required before approval');
|
|
272
|
-
if (!plan.goal) errors.push('goal is required before approval');
|
|
273
|
-
if (plan.steps.length === 0) errors.push('at least one plan step is required before approval');
|
|
274
|
-
const ids = new Set<string>();
|
|
275
|
-
for (const step of plan.steps) {
|
|
276
|
-
if (!step.id || step.id === 'unknown') errors.push('every plan step needs an id');
|
|
277
|
-
else if (ids.has(step.id)) errors.push(`duplicate plan step id: ${step.id}`);
|
|
278
|
-
else ids.add(step.id);
|
|
279
|
-
if (!step.objective) errors.push(`step ${step.id || 'unknown'} needs an objective`);
|
|
280
|
-
if (step.checks.length === 0) errors.push(`step ${step.id || 'unknown'} needs at least one machine-checkable check`);
|
|
281
|
-
if (step.acceptanceCriteria.length === 0) errors.push(`step ${step.id || 'unknown'} needs acceptance criteria`);
|
|
282
|
-
if (step.status !== 'pending') errors.push(`step ${step.id || 'unknown'} must be pending before approval`);
|
|
283
|
-
}
|
|
284
|
-
for (const step of plan.steps) {
|
|
285
|
-
for (const dependency of step.dependencies) {
|
|
286
|
-
const dependencyStep = plan.steps.find((candidate) => candidate.id === dependency);
|
|
287
|
-
if (!dependencyStep) errors.push(`step ${step.id} references unknown dependency ${dependency}`);
|
|
288
|
-
else if (dependencyStep.status !== 'pending') errors.push(`step ${step.id} dependency ${dependency} must be pending before approval`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
458
|
+
const errors = draftContentErrors(plan).map((error) => `${error} before approval`);
|
|
291
459
|
const scopeIsCommitted = (status: PlanContractStatus): boolean => [
|
|
292
460
|
'approved',
|
|
293
461
|
'executing',
|
|
@@ -301,11 +469,12 @@ function approvalErrors(plan: PlanContract, allPlans: readonly PlanContract[]):
|
|
|
301
469
|
return [...new Set(errors)];
|
|
302
470
|
}
|
|
303
471
|
|
|
304
|
-
|
|
472
|
+
function approvePlanContractUnlocked(options: PlanContractStoreOptions, planId: string): PlanContract {
|
|
305
473
|
const store = readPlanContractStore(options);
|
|
306
474
|
const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
307
475
|
if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
|
|
308
476
|
const current = store.contracts[index];
|
|
477
|
+
assertRequirementReference(options, current.requirementId);
|
|
309
478
|
if (current.status !== 'draft' && current.status !== 'reviewing') throw new Error(`plan contract ${current.planId} cannot be approved from ${current.status}`);
|
|
310
479
|
const errors = approvalErrors(current, store.contracts);
|
|
311
480
|
if (errors.length > 0) throw new Error(`plan contract ${current.planId} cannot be approved: ${errors.join('; ')}`);
|
|
@@ -317,20 +486,33 @@ export function approvePlanContract(options: PlanContractStoreOptions, planId: s
|
|
|
317
486
|
return next;
|
|
318
487
|
}
|
|
319
488
|
|
|
489
|
+
export function approvePlanContract(options: PlanContractStoreOptions, planId: string): PlanContract {
|
|
490
|
+
return withPlanAdmissionLock(options, () => approvePlanContractUnlocked(options, planId));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function approvePlanContractAsync(options: PlanContractStoreOptions, planId: string): Promise<PlanContract> {
|
|
494
|
+
return await withPlanAdmissionLockAsync(options, () => approvePlanContractUnlocked(options, planId));
|
|
495
|
+
}
|
|
496
|
+
|
|
320
497
|
export function supersedePlanContract(options: PlanContractStoreOptions, planId: string, supersededBy: string): PlanContract {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
498
|
+
return withPlanAdmissionLock(options, () => {
|
|
499
|
+
const store = readPlanContractStore(options);
|
|
500
|
+
const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
501
|
+
if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
|
|
502
|
+
const current = store.contracts[index]!;
|
|
503
|
+
if (isTerminalPlanContractStatus(current.status)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
|
|
504
|
+
const replacement = sanitizeFileComponent(supersededBy);
|
|
505
|
+
if (!replacement || replacement === 'unknown') throw new Error('superseded_by is required');
|
|
506
|
+
if (replacement === current.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
|
|
507
|
+
const successor = store.contracts.find((contract) => contract.planId === replacement);
|
|
508
|
+
if (!successor) throw new Error(`PLAN_SUCCESSOR_NOT_FOUND: ${replacement}`);
|
|
509
|
+
const at = nowIso(options);
|
|
510
|
+
const next = { ...current, status: 'superseded' as const, supersededBy: successor.planId, updatedAt: at };
|
|
511
|
+
const contracts = [...store.contracts];
|
|
512
|
+
contracts[index] = next;
|
|
513
|
+
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
514
|
+
return next;
|
|
515
|
+
});
|
|
334
516
|
}
|
|
335
517
|
|
|
336
518
|
/**
|
|
@@ -343,6 +525,7 @@ export function claimPlanStepForWork(
|
|
|
343
525
|
input: { planId: string; stepId: string; workId: string; sourceRevision: string },
|
|
344
526
|
): PlanContract {
|
|
345
527
|
return updatePlanContract(options, input.planId, (current) => {
|
|
528
|
+
assertRequirementReference(options, current.requirementId);
|
|
346
529
|
if (current.status !== 'approved' && current.status !== 'executing') {
|
|
347
530
|
throw new Error(`PLAN_NOT_EXECUTABLE: ${current.planId} is ${current.status}`);
|
|
348
531
|
}
|
|
@@ -363,6 +546,45 @@ export function claimPlanStepForWork(
|
|
|
363
546
|
});
|
|
364
547
|
}
|
|
365
548
|
|
|
549
|
+
export function repairDanglingPlanStepWorkBinding(
|
|
550
|
+
options: PlanContractStoreOptions,
|
|
551
|
+
input: { planId: string; stepId: string; expectedWorkId: string; reason: string },
|
|
552
|
+
): PlanContract {
|
|
553
|
+
const expectedWorkId = input.expectedWorkId.trim();
|
|
554
|
+
const reason = input.reason.trim();
|
|
555
|
+
if (!expectedWorkId) throw new Error('PLAN_STEP_REPAIR_EXPECTED_WORK_REQUIRED');
|
|
556
|
+
if (!reason) throw new Error('PLAN_STEP_REPAIR_REASON_REQUIRED');
|
|
557
|
+
return updatePlanContract(options, input.planId, (current) => {
|
|
558
|
+
if (isTerminalPlanContractStatus(current.status)) throw new Error(`PLAN_STEP_REPAIR_PLAN_TERMINAL: ${current.planId} is ${current.status}`);
|
|
559
|
+
const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
|
|
560
|
+
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
|
|
561
|
+
const step = current.steps[stepIndex];
|
|
562
|
+
if (step.workId !== expectedWorkId) {
|
|
563
|
+
throw new Error(`PLAN_STEP_REPAIR_BINDING_CHANGED: ${step.id} is bound to ${step.workId ?? 'no Work'}, expected ${expectedWorkId}`);
|
|
564
|
+
}
|
|
565
|
+
if (step.status !== 'executing') {
|
|
566
|
+
throw new Error(`PLAN_STEP_REPAIR_STATUS_INVALID: ${step.id} is ${step.status}`);
|
|
567
|
+
}
|
|
568
|
+
const at = nowIso(options);
|
|
569
|
+
const steps = [...current.steps];
|
|
570
|
+
steps[stepIndex] = {
|
|
571
|
+
...step,
|
|
572
|
+
status: 'ready',
|
|
573
|
+
workId: undefined,
|
|
574
|
+
evidenceRefs: [{
|
|
575
|
+
title: 'dangling Work binding repaired',
|
|
576
|
+
summary: `${expectedWorkId}: ${reason}`.slice(0, 2_000),
|
|
577
|
+
detailLevel: 'summary' as const,
|
|
578
|
+
}, ...step.evidenceRefs].slice(0, 20),
|
|
579
|
+
};
|
|
580
|
+
// This is admission-state repair, not semantic replanning: the Plan and its
|
|
581
|
+
// acceptance criteria remain authoritative. Return the exact step to ready
|
|
582
|
+
// so a later start can atomically admit one replacement for the missing
|
|
583
|
+
// record without forcing a second Plan authority.
|
|
584
|
+
return { ...current, status: 'executing', steps, updatedAt: at };
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
366
588
|
export function completePlanStepForWork(
|
|
367
589
|
options: PlanContractStoreOptions,
|
|
368
590
|
input: {
|
|
@@ -392,6 +614,10 @@ export function completePlanStepForWork(
|
|
|
392
614
|
// semantic validation until an explicit Controller acceptance records it.
|
|
393
615
|
// Failed/cancelled Work returns the slice to ready for replanning.
|
|
394
616
|
status: delivered ? 'validating' : 'ready',
|
|
617
|
+
// Successful delivery keeps the exact Work identity through semantic
|
|
618
|
+
// validation. Failed/cancelled Work releases the step authority so the
|
|
619
|
+
// replanning path cannot retain a ghost binding to a terminal Work.
|
|
620
|
+
workId: delivered ? step.workId : undefined,
|
|
395
621
|
evidenceRefs: input.work.evidenceRefs.length > 0 ? input.work.evidenceRefs.slice(0, 20) : step.evidenceRefs,
|
|
396
622
|
};
|
|
397
623
|
return { ...current, status: delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
|
|
@@ -400,7 +626,7 @@ export function completePlanStepForWork(
|
|
|
400
626
|
|
|
401
627
|
export function acceptPlanStepEvidence(
|
|
402
628
|
options: PlanContractStoreOptions,
|
|
403
|
-
input: { planId: string; stepId: string; reviewer: string; rationale: string },
|
|
629
|
+
input: { planId: string; stepId: string; reviewer: string; rationale: string; acceptedSourceRevision?: string },
|
|
404
630
|
): PlanContract {
|
|
405
631
|
const reviewer = input.reviewer.trim();
|
|
406
632
|
const rationale = input.rationale.trim();
|
|
@@ -418,6 +644,13 @@ export function acceptPlanStepEvidence(
|
|
|
418
644
|
evidenceRefs: [{ title: 'semantic acceptance', summary: `${reviewer}: ${rationale}`, detailLevel: 'summary' as const }, ...step.evidenceRefs].slice(0, 20),
|
|
419
645
|
};
|
|
420
646
|
const allCompleted = steps.every((candidate) => candidate.status === 'completed');
|
|
421
|
-
|
|
647
|
+
const acceptedSourceRevision = input.acceptedSourceRevision?.trim();
|
|
648
|
+
return {
|
|
649
|
+
...current,
|
|
650
|
+
...(acceptedSourceRevision ? { sourceRevision: acceptedSourceRevision } : {}),
|
|
651
|
+
status: allCompleted ? 'finalized' : 'executing',
|
|
652
|
+
steps,
|
|
653
|
+
updatedAt: nowIso(options),
|
|
654
|
+
};
|
|
422
655
|
});
|
|
423
656
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { AccessMode } from '../governance/access-policy';
|
|
2
|
+
import type { RouteDecision } from '../routing/route-policy';
|
|
3
|
+
import { createWorkContract, getWorkContract, updateWorkContract, type WorkContractStoreOptions } from './work-contract-store';
|
|
4
|
+
import type { WorkContract } from './types';
|
|
5
|
+
|
|
6
|
+
export interface PreparedRepositoryWorkAdmissionInput {
|
|
7
|
+
workId: string;
|
|
8
|
+
repoId: string;
|
|
9
|
+
objective: string;
|
|
10
|
+
acceptanceCriteria: string[];
|
|
11
|
+
allowedPaths: string[];
|
|
12
|
+
checks: string[];
|
|
13
|
+
accessMode: AccessMode;
|
|
14
|
+
isolated: boolean;
|
|
15
|
+
requestedBy: WorkContract['requestedBy'];
|
|
16
|
+
requestId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Canonical WorkContract admission for the compatibility work_prepare surface.
|
|
21
|
+
* Workspace materialization remains an execution concern; this function owns
|
|
22
|
+
* the semantic contract shape so transports cannot invent a parallel policy.
|
|
23
|
+
*/
|
|
24
|
+
export function admitPreparedRepositoryWorkContract(
|
|
25
|
+
store: WorkContractStoreOptions,
|
|
26
|
+
input: PreparedRepositoryWorkAdmissionInput,
|
|
27
|
+
): WorkContract {
|
|
28
|
+
return createWorkContract(store, {
|
|
29
|
+
workId: input.workId,
|
|
30
|
+
repoId: input.repoId,
|
|
31
|
+
mode: input.isolated ? 'goal_workloop' : 'direct_control',
|
|
32
|
+
objective: input.objective,
|
|
33
|
+
acceptanceCriteria: input.acceptanceCriteria,
|
|
34
|
+
allowedPaths: input.allowedPaths,
|
|
35
|
+
forbiddenPaths: [],
|
|
36
|
+
checks: input.checks,
|
|
37
|
+
constraints: {
|
|
38
|
+
accessMode: input.accessMode,
|
|
39
|
+
workspaceMode: input.isolated ? 'isolated' : 'current',
|
|
40
|
+
requireWorktree: input.isolated,
|
|
41
|
+
directMainProhibited: input.isolated,
|
|
42
|
+
allowCommit: true,
|
|
43
|
+
allowMerge: true,
|
|
44
|
+
allowCleanup: true,
|
|
45
|
+
},
|
|
46
|
+
worktreePolicy: {
|
|
47
|
+
required: input.isolated,
|
|
48
|
+
reason: input.isolated ? 'work_prepare selected isolated worktree execution' : 'explicitly reused a registered checkout',
|
|
49
|
+
},
|
|
50
|
+
requestedBy: input.requestedBy,
|
|
51
|
+
requestId: input.requestId,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DirectEditWorkAdmissionInput {
|
|
56
|
+
workId: string;
|
|
57
|
+
repoId: string;
|
|
58
|
+
checkoutId?: string;
|
|
59
|
+
principalId?: string;
|
|
60
|
+
controllerInstanceId?: string;
|
|
61
|
+
baseRevision?: string;
|
|
62
|
+
workspaceFingerprint: string;
|
|
63
|
+
routeDecision: RouteDecision;
|
|
64
|
+
objective: string;
|
|
65
|
+
issueId?: string;
|
|
66
|
+
taskId?: string;
|
|
67
|
+
allowedPaths: string[];
|
|
68
|
+
checks: string[];
|
|
69
|
+
requestedBy: WorkContract['requestedBy'];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Canonical compatibility admission for an Edit Session bound to Direct Work.
|
|
74
|
+
* The caller supplies the canonical Route Policy decision as evidence; this
|
|
75
|
+
* boundary rejects any decision that requires isolation or a non-direct lane.
|
|
76
|
+
*/
|
|
77
|
+
export function admitDirectEditWorkContract(
|
|
78
|
+
store: WorkContractStoreOptions,
|
|
79
|
+
input: DirectEditWorkAdmissionInput,
|
|
80
|
+
): WorkContract {
|
|
81
|
+
if (input.routeDecision.executionMode !== 'direct_control' || input.routeDecision.requiresIsolation) {
|
|
82
|
+
throw new Error('DIRECT_EDIT_WORK_ROUTE_CONFLICT: canonical Route Policy does not permit Direct Control');
|
|
83
|
+
}
|
|
84
|
+
return createWorkContract(store, {
|
|
85
|
+
workId: input.workId,
|
|
86
|
+
repoId: input.repoId,
|
|
87
|
+
checkoutId: input.checkoutId,
|
|
88
|
+
principalId: input.principalId,
|
|
89
|
+
controllerInstanceId: input.controllerInstanceId,
|
|
90
|
+
baseRevision: input.baseRevision,
|
|
91
|
+
workspaceFingerprint: input.workspaceFingerprint,
|
|
92
|
+
routeDecisionFingerprint: input.routeDecision.inputFingerprint,
|
|
93
|
+
routeDecision: input.routeDecision,
|
|
94
|
+
mode: 'direct_control',
|
|
95
|
+
objective: input.objective,
|
|
96
|
+
acceptanceCriteria: [],
|
|
97
|
+
constraints: { workspaceMode: 'current', requireWorktree: false, requireHandoffOnAmbiguity: true },
|
|
98
|
+
risk: 'low',
|
|
99
|
+
status: 'running',
|
|
100
|
+
phase: 'implementation',
|
|
101
|
+
issueId: input.issueId,
|
|
102
|
+
taskId: input.taskId,
|
|
103
|
+
scopeSummary: input.objective,
|
|
104
|
+
allowedPaths: input.allowedPaths,
|
|
105
|
+
forbiddenPaths: [],
|
|
106
|
+
checks: input.checks,
|
|
107
|
+
requestedBy: input.requestedBy,
|
|
108
|
+
evidenceRefs: [],
|
|
109
|
+
handoffRefs: [],
|
|
110
|
+
suggestedNextActions: [],
|
|
111
|
+
policyDecisions: [],
|
|
112
|
+
checkRefs: [],
|
|
113
|
+
reconciliations: [],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
export interface MaterializedRepositoryWorkspace {
|
|
119
|
+
managed: boolean;
|
|
120
|
+
checkoutId?: string;
|
|
121
|
+
root?: string;
|
|
122
|
+
baseRevision?: string | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Canonical placement materialization transition for an admitted isolated
|
|
127
|
+
* WorkContract. The concrete worktree creator is injected by the execution
|
|
128
|
+
* boundary; only this authority decides how its evidence updates the Work.
|
|
129
|
+
*/
|
|
130
|
+
export function materializeRepositoryWorkPlacement(
|
|
131
|
+
store: WorkContractStoreOptions,
|
|
132
|
+
workId: string,
|
|
133
|
+
materialize: (contract: WorkContract) => MaterializedRepositoryWorkspace,
|
|
134
|
+
): WorkContract | undefined {
|
|
135
|
+
const contract = getWorkContract(store, workId);
|
|
136
|
+
if (!contract || contract.worktreePolicy.required !== true || contract.worktreeRef) return contract;
|
|
137
|
+
const workspace = materialize(contract);
|
|
138
|
+
if (!workspace.managed || !workspace.checkoutId || !workspace.root) {
|
|
139
|
+
throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
|
|
140
|
+
}
|
|
141
|
+
return updateWorkContract(store, workId, {
|
|
142
|
+
checkoutId: workspace.checkoutId,
|
|
143
|
+
baseRevision: workspace.baseRevision ?? contract.baseRevision,
|
|
144
|
+
worktreeRef: workspace.root,
|
|
145
|
+
driver: { ...contract.driver, preferred: 'isolated_worktree', allowDirectEdit: false },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRequirement,
|
|
3
|
+
readRequirement,
|
|
4
|
+
type CreateRequirementInput,
|
|
5
|
+
type Requirement,
|
|
6
|
+
type RequirementStoreOptions,
|
|
7
|
+
} from '../persistence/requirement-store';
|
|
8
|
+
|
|
9
|
+
export type RequirementAdmissionDecision = 'created' | 'reuse_existing' | 'existing_conflict';
|
|
10
|
+
|
|
11
|
+
export interface RequirementAdmissionResult {
|
|
12
|
+
decision: RequirementAdmissionDecision;
|
|
13
|
+
requirement: Requirement;
|
|
14
|
+
created: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function bounded(values: readonly string[] | undefined, limit: number, maxLength = 500): string[] {
|
|
18
|
+
return (values ?? [])
|
|
19
|
+
.map((value) => String(value).trim())
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.slice(0, limit)
|
|
22
|
+
.map((value) => value.slice(0, maxLength));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Canonical Requirement bootstrap request. Persistence performs the same
|
|
27
|
+
* defensive bounds before writing, but create/reuse/conflict admission lives
|
|
28
|
+
* only here so MCP/CLI adapters cannot grow their own Requirement lifecycle.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeRequirementAdmissionInput(input: CreateRequirementInput): CreateRequirementInput {
|
|
31
|
+
const requirementId = String(input.requirementId ?? '').trim();
|
|
32
|
+
if (!requirementId || requirementId.includes('/') || requirementId.includes('\\')) throw new Error('REQUIREMENT_ID_INVALID');
|
|
33
|
+
const normalized: CreateRequirementInput = {
|
|
34
|
+
requirementId: requirementId.slice(0, 160),
|
|
35
|
+
title: String(input.title ?? '').trim().slice(0, 500),
|
|
36
|
+
outcomeStatement: String(input.outcomeStatement ?? '').trim().slice(0, 2_000),
|
|
37
|
+
acceptanceCriteria: bounded(input.acceptanceCriteria, 50),
|
|
38
|
+
requiredDeliveryReferences: bounded(input.requiredDeliveryReferences, 50),
|
|
39
|
+
legacyAliases: bounded(input.legacyAliases, 20, 160),
|
|
40
|
+
};
|
|
41
|
+
if (!normalized.title || !normalized.outcomeStatement) throw new Error('REQUIREMENT_CONTENT_REQUIRED');
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sameBootstrapIdentity(existing: Requirement, requested: CreateRequirementInput): boolean {
|
|
46
|
+
return existing.requirementId === requested.requirementId
|
|
47
|
+
&& existing.title === requested.title
|
|
48
|
+
&& existing.outcomeStatement === requested.outcomeStatement
|
|
49
|
+
&& JSON.stringify(existing.acceptanceCriteria) === JSON.stringify(requested.acceptanceCriteria ?? [])
|
|
50
|
+
&& JSON.stringify(existing.requiredDeliveryReferences) === JSON.stringify(requested.requiredDeliveryReferences ?? [])
|
|
51
|
+
&& JSON.stringify(existing.legacyAliases) === JSON.stringify(requested.legacyAliases ?? []);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function existingDecision(existing: Requirement, requested: CreateRequirementInput): RequirementAdmissionResult {
|
|
55
|
+
return {
|
|
56
|
+
decision: sameBootstrapIdentity(existing, requested) ? 'reuse_existing' : 'existing_conflict',
|
|
57
|
+
requirement: existing,
|
|
58
|
+
created: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sole bootstrap admission authority for Requirement identity. A create race is
|
|
64
|
+
* closed by rereading the winner and applying the same identity comparison;
|
|
65
|
+
* adapters therefore never need read/compare/create policy of their own.
|
|
66
|
+
*/
|
|
67
|
+
export function admitRequirement(
|
|
68
|
+
options: RequirementStoreOptions,
|
|
69
|
+
input: CreateRequirementInput,
|
|
70
|
+
): RequirementAdmissionResult {
|
|
71
|
+
const requested = normalizeRequirementAdmissionInput(input);
|
|
72
|
+
const existing = readRequirement(options, requested.requirementId)?.value;
|
|
73
|
+
if (existing) return existingDecision(existing, requested);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return { decision: 'created', requirement: createRequirement(options, requested), created: true };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!(error instanceof Error) || !error.message.startsWith('REQUIREMENT_ALREADY_EXISTS:')) throw error;
|
|
79
|
+
const raced = readRequirement(options, requested.requirementId)?.value;
|
|
80
|
+
if (!raced) throw error;
|
|
81
|
+
return existingDecision(raced, requested);
|
|
82
|
+
}
|
|
83
|
+
}
|