@moretea-labs/forge 1.5.0-rc.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 +120 -0
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +120 -0
- package/CODE_OF_CONDUCT.md +23 -0
- package/CONTRIBUTING.md +77 -0
- package/LICENSE +22 -0
- package/NOTICE +9 -0
- package/README.en.md +5 -0
- package/README.es.md +12 -0
- package/README.fr.md +12 -0
- package/README.ja.md +12 -0
- package/README.md +132 -0
- package/README.zh-CN.md +84 -0
- package/SECURITY.md +50 -0
- package/SKILL.md +431 -0
- package/SUPPORT.md +31 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/agents/openai.yaml +4 -0
- package/assets/AGENTS.md +68 -0
- package/assets/CLAUDE.md +68 -0
- package/assets/branding/README.md +10 -0
- package/assets/branding/forge-controller-flow.mmd +8 -0
- package/assets/hooks/AGENTS.md +75 -0
- package/assets/hooks/CLAUDE.md +75 -0
- package/assets/hooks/anti-simplification.sh +12 -0
- package/assets/hooks/changelog-guard.sh +80 -0
- package/assets/hooks/codex.hooks.template.json +60 -0
- package/assets/hooks/first-principles-guard.sh +72 -0
- package/assets/hooks/hook-input.sh +618 -0
- package/assets/hooks/lib/session-state.sh +106 -0
- package/assets/hooks/lib/workflow-state.sh +1730 -0
- package/assets/hooks/post-bash.sh +215 -0
- package/assets/hooks/post-edit-guard.sh +256 -0
- package/assets/hooks/post-tool-observer.sh +99 -0
- package/assets/hooks/pre-edit-guard.sh +240 -0
- package/assets/hooks/prompt-guard.sh +1077 -0
- package/assets/hooks/run-hook.sh +67 -0
- package/assets/hooks/security-sentinel.sh +115 -0
- package/assets/hooks/session-start-context.sh +593 -0
- package/assets/hooks/settings.template.json +60 -0
- package/assets/hooks/stop-orchestrator.sh +171 -0
- package/assets/hooks/subagent-return-channel-guard.sh +107 -0
- package/assets/hooks/worktree-guard.sh +39 -0
- package/assets/initializer-question-pack.v1.json +132 -0
- package/assets/initializer-question-pack.v1.schema.json +46 -0
- package/assets/initializer-question-pack.v2.json +174 -0
- package/assets/initializer-question-pack.v3.json +206 -0
- package/assets/initializer-question-pack.v3.schema.json +92 -0
- package/assets/initializer-question-pack.v4.json +557 -0
- package/assets/initializer-question-pack.v4.schema.json +169 -0
- package/assets/partials/01-header.partial.md +10 -0
- package/assets/partials/02-iron-rules.partial.md +30 -0
- package/assets/partials/03-philosophy.partial.md +32 -0
- package/assets/partials/04-project-structure.partial.md +56 -0
- package/assets/partials/05-workflow.partial.md +79 -0
- package/assets/partials/06-cloudflare.partial.md +20 -0
- package/assets/partials/07-footer.partial.md +38 -0
- package/assets/partials/08-orchestration.partial.md +49 -0
- package/assets/partials/09-compact-instructions.partial.md +11 -0
- package/assets/partials/_assembly-order.md +40 -0
- package/assets/partials-agents/01-header.partial.md +8 -0
- package/assets/partials-agents/02-operating-mode.partial.md +44 -0
- package/assets/partials-agents/03-orchestration.partial.md +45 -0
- package/assets/partials-agents/04-task-protocol.partial.md +69 -0
- package/assets/partials-agents/05-coding-constraints.partial.md +20 -0
- package/assets/partials-agents/06-quality-safety.partial.md +23 -0
- package/assets/partials-agents/07-cloudflare.partial.md +9 -0
- package/assets/partials-agents/08-deep-docs.partial.md +32 -0
- package/assets/plan-map.json +301 -0
- package/assets/plugin-registry.v1.json +8 -0
- package/assets/project-structures/ai-native-collaborative-editor.txt +25 -0
- package/assets/project-structures/ai-native-product-copilot.txt +21 -0
- package/assets/project-structures/ai-native-runtime-console.txt +25 -0
- package/assets/project-structures/ai-native-sidecar-kernel.txt +19 -0
- package/assets/project-structures/astro-ssr.txt +25 -0
- package/assets/project-structures/expo-nativewind.txt +52 -0
- package/assets/project-structures/monorepo.txt +63 -0
- package/assets/project-structures/remix.txt +50 -0
- package/assets/project-structures/tanstack-start-workers.txt +51 -0
- package/assets/project-structures/vite-tanstack.txt +58 -0
- package/assets/reference-configs/AGENTS.md +13 -0
- package/assets/reference-configs/CLAUDE.md +13 -0
- package/assets/reference-configs/agentic-development-flow.md +94 -0
- package/assets/reference-configs/ai-workflows.md +14 -0
- package/assets/reference-configs/changelog-versioning.md +13 -0
- package/assets/reference-configs/coding-standards.md +14 -0
- package/assets/reference-configs/development-protocol.md +21 -0
- package/assets/reference-configs/document-generation.md +38 -0
- package/assets/reference-configs/evaluator-rubric.md +19 -0
- package/assets/reference-configs/external-tooling.md +374 -0
- package/assets/reference-configs/git-strategy.md +14 -0
- package/assets/reference-configs/global-working-rules.md +64 -0
- package/assets/reference-configs/handoff-protocol.md +38 -0
- package/assets/reference-configs/harness-overview.md +124 -0
- package/assets/reference-configs/heartbeat-triage.md +57 -0
- package/assets/reference-configs/hook-operations.md +96 -0
- package/assets/reference-configs/release-deploy.md +40 -0
- package/assets/reference-configs/spa-day-protocol.md +14 -0
- package/assets/reference-configs/sprint-contracts.md +89 -0
- package/assets/reference-configs/workflow-orchestration.md +14 -0
- package/assets/skill-hooks.json +41 -0
- package/assets/skill-version.json +189 -0
- package/assets/skills/claude-review/SKILL.md +231 -0
- package/assets/skills/codex-review/SKILL.md +103 -0
- package/assets/templates/AGENTS.md +13 -0
- package/assets/templates/CLAUDE.md +13 -0
- package/assets/templates/architecture.template.md +80 -0
- package/assets/templates/brief.template.md +38 -0
- package/assets/templates/contract.template.md +104 -0
- package/assets/templates/decisions.template.md +85 -0
- package/assets/templates/factor-factory/factor-backtest-report.template.md +36 -0
- package/assets/templates/factor-factory/factor-hypothesis.template.md +30 -0
- package/assets/templates/factor-factory/factor-registry.template.json +5 -0
- package/assets/templates/gitignore.template +53 -0
- package/assets/templates/guides/metro-esm-gotchas.template.md +46 -0
- package/assets/templates/helpers/architecture-event.ts +822 -0
- package/assets/templates/helpers/architecture-queue.sh +647 -0
- package/assets/templates/helpers/archive-architecture-request.sh +244 -0
- package/assets/templates/helpers/archive-workflow.sh +295 -0
- package/assets/templates/helpers/capability-config.ts +394 -0
- package/assets/templates/helpers/capability-resolver.ts +505 -0
- package/assets/templates/helpers/capture-plan.sh +335 -0
- package/assets/templates/helpers/check-agent-tooling.sh +1533 -0
- package/assets/templates/helpers/check-architecture-sync.sh +287 -0
- package/assets/templates/helpers/check-brain-manifest.sh +255 -0
- package/assets/templates/helpers/check-context-files.sh +49 -0
- package/assets/templates/helpers/check-deploy-sql-order.sh +117 -0
- package/assets/templates/helpers/check-skill-version.ts +193 -0
- package/assets/templates/helpers/check-task-sync.sh +65 -0
- package/assets/templates/helpers/check-task-workflow.sh +1002 -0
- package/assets/templates/helpers/codex-handoff-resume.sh +269 -0
- package/assets/templates/helpers/context-contract-sync.sh +516 -0
- package/assets/templates/helpers/contract-run.ts +465 -0
- package/assets/templates/helpers/contract-worktree.sh +725 -0
- package/assets/templates/helpers/ensure-task-workflow.sh +1218 -0
- package/assets/templates/helpers/harness-trace-grade.sh +150 -0
- package/assets/templates/helpers/heartbeat-triage.sh +361 -0
- package/assets/templates/helpers/inspect-project-state.ts +280 -0
- package/assets/templates/helpers/maintenance-triage.sh +167 -0
- package/assets/templates/helpers/migrate-project-template.sh +54 -0
- package/assets/templates/helpers/migrate-workflow-docs.ts +413 -0
- package/assets/templates/helpers/new-plan.sh +183 -0
- package/assets/templates/helpers/new-spec.sh +50 -0
- package/assets/templates/helpers/new-sprint.sh +49 -0
- package/assets/templates/helpers/plan-to-todo.sh +799 -0
- package/assets/templates/helpers/prepare-codex-handoff.sh +203 -0
- package/assets/templates/helpers/prepare-handoff.sh +81 -0
- package/assets/templates/helpers/refresh-current-status.sh +469 -0
- package/assets/templates/helpers/select-agent-context-blocks.sh +66 -0
- package/assets/templates/helpers/ship-worktrees.sh +561 -0
- package/assets/templates/helpers/sprint-backlog.sh +837 -0
- package/assets/templates/helpers/summarize-failures.sh +125 -0
- package/assets/templates/helpers/switch-plan.sh +153 -0
- package/assets/templates/helpers/sync-brain-docs.sh +381 -0
- package/assets/templates/helpers/verify-contract.sh +705 -0
- package/assets/templates/helpers/verify-sprint.sh +680 -0
- package/assets/templates/helpers/workflow-contract.ts +202 -0
- package/assets/templates/helpers/workstream-sync.sh +263 -0
- package/assets/templates/implementation-notes.template.md +37 -0
- package/assets/templates/packages.template.md +45 -0
- package/assets/templates/plan.template.md +77 -0
- package/assets/templates/prd.template.md +165 -0
- package/assets/templates/research.template.md +25 -0
- package/assets/templates/review.template.md +78 -0
- package/assets/templates/spec.template.md +31 -0
- package/assets/templates/sprint.template.md +69 -0
- package/assets/templates/tech-stack.template.md +57 -0
- package/assets/versions.json +77 -0
- package/assets/workflow-contract.v1.json +671 -0
- package/bin/forge-desktop-helper.mjs +204 -0
- package/bin/forge-hook.mjs +32 -0
- package/bin/forge-runtime-service.mjs +34 -0
- package/bin/forge-runtime.mjs +30 -0
- package/bin/forge.mjs +32 -0
- package/docs/CHANGELOG.md +978 -0
- package/docs/README.md +72 -0
- package/docs/README.zh-CN.md +58 -0
- package/docs/architecture/chatgpt-handoff-facade.md +393 -0
- package/docs/architecture/current/README.md +158 -0
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +1501 -0
- package/docs/architecture/current/architecture-invariants.md +331 -0
- package/docs/architecture/current/automation-and-schedule-engine.md +453 -0
- package/docs/architecture/current/codegraph-context-provider.md +226 -0
- package/docs/architecture/current/control-plane-authority-inventory.md +82 -0
- package/docs/architecture/current/controller-repository-decoupling.md +148 -0
- package/docs/architecture/current/dispatch-and-agent-strategy.md +354 -0
- package/docs/architecture/current/entity-model.md +619 -0
- package/docs/architecture/current/external-plugin-governance.md +396 -0
- package/docs/architecture/current/failure-recovery.md +471 -0
- package/docs/architecture/current/governance.md +213 -0
- package/docs/architecture/current/human-interaction-plane.md +87 -0
- package/docs/architecture/current/implementation-status.md +128 -0
- package/docs/architecture/current/job-and-run-lifecycle.md +413 -0
- package/docs/architecture/current/local-workspace-target-grants.md +70 -0
- package/docs/architecture/current/migration-roadmap.md +105 -0
- package/docs/architecture/current/multi-repository-execution.md +371 -0
- package/docs/architecture/current/operations-runbook.md +45 -0
- package/docs/architecture/current/peer-supercontroller-runtime.md +32 -0
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +207 -0
- package/docs/architecture/current/repository-access-modes.md +102 -0
- package/docs/architecture/current/route-policy.md +15 -0
- package/docs/architecture/current/route-session-concurrency.md +85 -0
- package/docs/architecture/current/runtime-architecture-simplification.md +214 -0
- package/docs/architecture/current/runtime-directory-map.md +49 -0
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +85 -0
- package/docs/architecture/current/scheduler-and-resource-claims.md +432 -0
- package/docs/architecture/current/session-aware-execution-and-authorization.md +77 -0
- package/docs/architecture/current/system-overview.md +144 -0
- package/docs/architecture/current/target-requirements-traceability.md +202 -0
- package/docs/architecture/current/thin-harness-governance-policy.md +32 -0
- package/docs/architecture/current/thin-harness-v1.md +313 -0
- package/docs/architecture/current/verification-and-release-gates.md +391 -0
- package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +100 -0
- package/docs/architecture/decisions/20260803-canonical-skill-surface.md +40 -0
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +53 -0
- package/docs/architecture/diagrams/.gitkeep +1 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +186 -0
- package/docs/architecture/domains/.gitkeep +0 -0
- package/docs/architecture/domains/public-surface.md +30 -0
- package/docs/architecture/domains/runtime-harness.md +33 -0
- package/docs/architecture/domains/verification.md +33 -0
- package/docs/architecture/domains/workflow-engine.md +30 -0
- package/docs/architecture/global-hook-runtime.md +162 -0
- package/docs/architecture/history.md +38 -0
- package/docs/architecture/index.md +114 -0
- package/docs/architecture/ios-semantic-automation-provider-v2.md +166 -0
- package/docs/architecture/modules/.gitkeep +0 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +24 -0
- package/docs/architecture/requests/.gitkeep +1 -0
- package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +63 -0
- package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +68 -0
- package/docs/architecture/requests/archive/2026/root.md +68 -0
- package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +76 -0
- package/docs/architecture/requests/archive/2026/verification-evals-checks.md +68 -0
- package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +69 -0
- package/docs/architecture/reviews/2026-07-11-access-surface-review.md +99 -0
- package/docs/architecture/snapshots/.gitkeep +1 -0
- package/docs/forge-autonomous-goal-loop.md +7 -0
- package/docs/forge-chatgpt-browser-engine.md +11 -0
- package/docs/forge-chatgpt-mcp-setup.md +239 -0
- package/docs/forge-chatgpt-supervised-automation.md +11 -0
- package/docs/forge-github-issue-launcher.md +7 -0
- package/docs/forge-plugin-management.md +95 -0
- package/docs/forge-provider-configuration.md +7 -0
- package/docs/forge-runtime-self-healing-loop.md +7 -0
- package/docs/images/forge-banner-cn.svg +1 -0
- package/docs/images/forge-banner.svg +1 -0
- package/docs/images/forge-controller-flow.svg +1 -0
- package/docs/images/image.png +0 -0
- package/docs/images/repo-harness-gptpro-cn.png +0 -0
- package/docs/images/repo-harness-gptpro.png +0 -0
- package/docs/images/repo-harness-hook-carrot.png +0 -0
- package/docs/operations/chatgpt-personal-assistant-runtime.md +83 -0
- package/docs/operations/checkout-route-integrity.md +22 -0
- package/docs/operations/controller-browser-plugin.md +299 -0
- package/docs/operations/controller-desktop-plugin.md +112 -0
- package/docs/operations/controller-performance-and-502.md +126 -0
- package/docs/operations/controller-reliability-runbook.md +134 -0
- package/docs/operations/environment-hardening.md +50 -0
- package/docs/operations/features.md +45 -0
- package/docs/operations/features.zh-CN.md +45 -0
- package/docs/operations/github-repository.md +60 -0
- package/docs/operations/gmail-assistant-routines.md +186 -0
- package/docs/operations/homebrew.md +26 -0
- package/docs/operations/local-system-assistant.md +46 -0
- package/docs/operations/local-system-assistant.zh-CN.md +46 -0
- package/docs/operations/mcp-tool-exposure.md +58 -0
- package/docs/operations/mobile-intents-shortcuts.md +117 -0
- package/docs/operations/open-source-release-hygiene.md +31 -0
- package/docs/operations/personal-assistant-operator-guide.md +61 -0
- package/docs/operations/platform-support.md +62 -0
- package/docs/operations/platform-support.zh-CN.md +61 -0
- package/docs/operations/provider-configuration.md +31 -0
- package/docs/operations/releasing.md +86 -0
- package/docs/operations/releasing.zh-CN.md +86 -0
- package/docs/operations/route-session-concurrency-baseline.json +893 -0
- package/docs/operations/route-session-concurrency-baseline.md +94 -0
- package/docs/operations/runtime-performance-diagnostics.md +14 -0
- package/docs/operations/sqlite-control-plane-recovery.md +65 -0
- package/docs/operations/standalone-disaster-recovery.md +136 -0
- package/docs/operations/troubleshooting.md +72 -0
- package/docs/operations/troubleshooting.zh-CN.md +70 -0
- package/docs/public-usage-guide.md +33 -0
- package/docs/public-usage-guide.zh-CN.md +33 -0
- package/docs/tutorials/01-install-and-start.md +93 -0
- package/docs/tutorials/01-install-and-start.zh-CN.md +93 -0
- package/docs/tutorials/02-connect-chatgpt.md +82 -0
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +80 -0
- package/docs/tutorials/03-first-repository-task.md +57 -0
- package/docs/tutorials/03-first-repository-task.zh-CN.md +57 -0
- package/docs/tutorials/README.md +13 -0
- package/docs/tutorials/README.zh-CN.md +13 -0
- package/docs/wiki/Architecture.md +85 -0
- package/docs/wiki/Core-Concepts.md +27 -0
- package/docs/wiki/Home.md +36 -0
- package/docs/wiki/Implementation.md +172 -0
- package/docs/wiki/Installation.md +50 -0
- package/docs/wiki/Integrations.md +29 -0
- package/docs/wiki/Operations.md +26 -0
- package/docs/wiki/Quick-Start.md +46 -0
- package/docs/wiki/Releases-and-Upgrades.md +34 -0
- package/docs/wiki/Runtime-Architecture.md +50 -0
- package/docs/wiki/Security-Model.md +31 -0
- package/docs/wiki/Troubleshooting.md +38 -0
- package/docs/wiki/Work-Lifecycle.md +31 -0
- package/docs/wiki/_Sidebar.md +21 -0
- package/install.ps1 +48 -0
- package/install.sh +66 -0
- package/package.json +156 -0
- package/scripts/activate-source-baseline.ts +335 -0
- package/scripts/architecture-event.ts +822 -0
- package/scripts/architecture-queue.sh +647 -0
- package/scripts/archive-architecture-request.sh +244 -0
- package/scripts/archive-workflow.sh +295 -0
- package/scripts/assemble-template.ts +1172 -0
- package/scripts/benchmark-controller-hotpath.ts +212 -0
- package/scripts/benchmark-controller-round2.ts +139 -0
- package/scripts/benchmark-ios-agent-device.ts +514 -0
- package/scripts/benchmark-route-session-concurrency.ts +714 -0
- package/scripts/benchmark-thin-harness-gateway-ab.ts +632 -0
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +479 -0
- package/scripts/benchmark-thin-harness.ts +251 -0
- package/scripts/bootstrap-runtime-maintenance-recovery.sh +183 -0
- package/scripts/canary-global-hook.sh +263 -0
- package/scripts/capability-config.ts +394 -0
- package/scripts/capability-resolver.ts +505 -0
- package/scripts/capture-plan.sh +335 -0
- package/scripts/check-agent-tooling.sh +1549 -0
- package/scripts/check-architecture-sync.sh +373 -0
- package/scripts/check-brain-manifest.sh +255 -0
- package/scripts/check-ci.sh +16 -0
- package/scripts/check-context-files.sh +49 -0
- package/scripts/check-deploy-sql-order.sh +117 -0
- package/scripts/check-mcp-compatibility.ts +93 -0
- package/scripts/check-npm-release.sh +32 -0
- package/scripts/check-open-source-tracked-surface.sh +146 -0
- package/scripts/check-package-identity.mjs +117 -0
- package/scripts/check-platform-support.mjs +79 -0
- package/scripts/check-public-docs.sh +105 -0
- package/scripts/check-public-release-surface.sh +19 -0
- package/scripts/check-release-published.sh +57 -0
- package/scripts/check-release-readiness.sh +43 -0
- package/scripts/check-release-version.mjs +74 -0
- package/scripts/check-runtime-architecture.mjs +593 -0
- package/scripts/check-runtime-env.mjs +89 -0
- package/scripts/check-skill-version.ts +193 -0
- package/scripts/check-tarball-install-smoke.sh +66 -0
- package/scripts/check-task-sync.sh +65 -0
- package/scripts/check-task-workflow.sh +1002 -0
- package/scripts/check-third-party-notices.mjs +36 -0
- package/scripts/codex-handoff-resume.sh +269 -0
- package/scripts/complete-control-plane-cutover.ts +126 -0
- package/scripts/context-contract-sync.sh +516 -0
- package/scripts/contract-run.ts +465 -0
- package/scripts/contract-worktree.sh +725 -0
- package/scripts/controller-ngrok-rotation.sh +678 -0
- package/scripts/create-project-dirs.sh +282 -0
- package/scripts/ensure-codegraph.sh +16 -0
- package/scripts/ensure-task-workflow.sh +1218 -0
- package/scripts/export-requirement-portfolio.ts +28 -0
- package/scripts/factor-lab-check.sh +73 -0
- package/scripts/factor-lab-new.sh +96 -0
- package/scripts/factor-lab-promote.sh +86 -0
- package/scripts/factor-lab-reject.sh +68 -0
- package/scripts/harness-trace-grade.sh +150 -0
- package/scripts/heartbeat-triage.sh +361 -0
- package/scripts/hook-dispatch-diet-report.ts +234 -0
- package/scripts/hook-shim.sh +93 -0
- package/scripts/init-project.sh +505 -0
- package/scripts/initializer-question-pack.ts +200 -0
- package/scripts/inspect-project-state.ts +280 -0
- package/scripts/install-desktop-operator-registration.ts +45 -0
- package/scripts/install-standalone-recovery.ts +74 -0
- package/scripts/lib/AGENTS.md +13 -0
- package/scripts/lib/CLAUDE.md +13 -0
- package/scripts/lib/controller-home-env.sh +69 -0
- package/scripts/lib/project-init-lib.sh +2759 -0
- package/scripts/load-standalone-recovery.sh +17 -0
- package/scripts/loop-engine-cutover-gate.ts +324 -0
- package/scripts/maintenance-triage.sh +167 -0
- package/scripts/migrate-project-template.sh +1189 -0
- package/scripts/migrate-requirement-portfolio.ts +86 -0
- package/scripts/migrate-workflow-docs.ts +413 -0
- package/scripts/new-plan.sh +183 -0
- package/scripts/new-spec.sh +50 -0
- package/scripts/new-sprint.sh +49 -0
- package/scripts/open-source-audit-allowlist.txt +46 -0
- package/scripts/package-source-archive.sh +147 -0
- package/scripts/plan-to-todo.sh +799 -0
- package/scripts/prepare-codex-handoff.sh +203 -0
- package/scripts/prepare-handoff.sh +81 -0
- package/scripts/public-release-files.txt +39 -0
- package/scripts/public-release.ts +174 -0
- package/scripts/publish-release-tarball.sh +27 -0
- package/scripts/refresh-current-status.sh +469 -0
- package/scripts/refresh-ios-physical-runtime.ts +196 -0
- package/scripts/repair-executable-modes.ts +8 -0
- package/scripts/rollout-all-registered-repos.sh +7 -0
- package/scripts/route-nl-vs-ts-eval.ts +679 -0
- package/scripts/run-bun-test-file.ts +182 -0
- package/scripts/run-governed-gate.ts +140 -0
- package/scripts/run-mobile-monitor-bridge.ts +71 -0
- package/scripts/run-skill-evals.ts +1022 -0
- package/scripts/run-skill-hook.ts +244 -0
- package/scripts/run-supervised-command.ts +24 -0
- package/scripts/run-tests-portable.sh +20 -0
- package/scripts/select-agent-context-blocks.sh +66 -0
- package/scripts/setup-plugins.sh +43 -0
- package/scripts/ship-worktrees.sh +561 -0
- package/scripts/smoke-browser-cdp-bridge.ts +93 -0
- package/scripts/smoke-chatgpt-supervised-automation.ts +208 -0
- package/scripts/smoke-ios-physical-device.ts +114 -0
- package/scripts/smoke-mcp-http-runtime.ts +216 -0
- package/scripts/smoke-mcp-tool-surface.ts +117 -0
- package/scripts/smoke-runtime-recovery.ts +220 -0
- package/scripts/smoke-schedule-engine.ts +283 -0
- package/scripts/sprint-backlog.sh +837 -0
- package/scripts/stage-runtime-release.ts +38 -0
- package/scripts/summarize-failures.sh +125 -0
- package/scripts/switch-plan.sh +153 -0
- package/scripts/sync-brain-docs.sh +381 -0
- package/scripts/sync-codex-installed-copies.sh +183 -0
- package/scripts/test-governance.ts +75 -0
- package/scripts/verify-contract.sh +705 -0
- package/scripts/verify-forge-runtime.sh +31 -0
- package/scripts/verify-sprint.sh +680 -0
- package/scripts/windows-paths.d.mts +7 -0
- package/scripts/windows-paths.mjs +28 -0
- package/scripts/workflow-contract.ts +206 -0
- package/scripts/workstream-sync.sh +263 -0
- package/src/cli/agent-jobs/executable-resolver.ts +289 -0
- package/src/cli/agent-jobs/executor-health.ts +252 -0
- package/src/cli/agent-jobs/integration.ts +963 -0
- package/src/cli/agent-jobs/job-manager.ts +2030 -0
- package/src/cli/agent-jobs/types.ts +219 -0
- package/src/cli/agent-jobs/worker-lifecycle.ts +288 -0
- package/src/cli/chatgpt-browser/bind-server.ts +504 -0
- package/src/cli/chatgpt-browser/binding.ts +140 -0
- package/src/cli/chatgpt-browser/bridge-extension.ts +221 -0
- package/src/cli/chatgpt-browser/bridge-provider.ts +268 -0
- package/src/cli/chatgpt-browser/engine.ts +569 -0
- package/src/cli/chatgpt-browser/file-policy.ts +158 -0
- package/src/cli/chatgpt-browser/native-provider.ts +553 -0
- package/src/cli/chatgpt-browser/oracle-provider.ts +437 -0
- package/src/cli/chatgpt-browser/prompt-assembler.ts +29 -0
- package/src/cli/chatgpt-browser/session-store.ts +301 -0
- package/src/cli/chatgpt-browser/types.ts +172 -0
- package/src/cli/commands/adopt-plan.ts +186 -0
- package/src/cli/commands/brain-root.ts +132 -0
- package/src/cli/commands/brain.ts +609 -0
- package/src/cli/commands/capability-context.ts +613 -0
- package/src/cli/commands/chatgpt.ts +424 -0
- package/src/cli/commands/controller.ts +680 -0
- package/src/cli/commands/docs.ts +103 -0
- package/src/cli/commands/doctor.ts +476 -0
- package/src/cli/commands/global-runtime.ts +323 -0
- package/src/cli/commands/hook.ts +34 -0
- package/src/cli/commands/init-hook.ts +870 -0
- package/src/cli/commands/init.ts +829 -0
- package/src/cli/commands/install.ts +86 -0
- package/src/cli/commands/mcp.ts +315 -0
- package/src/cli/commands/migrate.ts +154 -0
- package/src/cli/commands/plugin.ts +74 -0
- package/src/cli/commands/prompt-guard-decision.ts +238 -0
- package/src/cli/commands/recovery.ts +667 -0
- package/src/cli/commands/repository.ts +224 -0
- package/src/cli/commands/run.ts +23 -0
- package/src/cli/commands/runtime.ts +191 -0
- package/src/cli/commands/security.ts +440 -0
- package/src/cli/commands/status.ts +137 -0
- package/src/cli/commands/tools.ts +167 -0
- package/src/cli/controller/check-runner.ts +1022 -0
- package/src/cli/controller/codex-continuation.ts +153 -0
- package/src/cli/controller/completion-backlog.ts +380 -0
- package/src/cli/controller/completion-orchestrator.ts +1807 -0
- package/src/cli/controller/completion-target.ts +96 -0
- package/src/cli/controller/composite-operations.ts +196 -0
- package/src/cli/controller/composite-result.ts +130 -0
- package/src/cli/controller/context-pack.ts +652 -0
- package/src/cli/controller/execution-completion.ts +175 -0
- package/src/cli/controller/execution-policy.ts +392 -0
- package/src/cli/controller/governance.ts +594 -0
- package/src/cli/controller/issue-store.ts +1421 -0
- package/src/cli/controller/launch-agents.ts +410 -0
- package/src/cli/controller/legacy-issue-cutover.ts +415 -0
- package/src/cli/controller/lifecycle-authority.ts +16 -0
- package/src/cli/controller/migrated-issue-decision.ts +237 -0
- package/src/cli/controller/operational-plan.ts +226 -0
- package/src/cli/controller/postcondition.ts +134 -0
- package/src/cli/controller/progress.ts +656 -0
- package/src/cli/controller/project-state.ts +147 -0
- package/src/cli/controller/requirement-portfolio-export.ts +146 -0
- package/src/cli/controller/requirement-portfolio-migration.ts +613 -0
- package/src/cli/controller/run-evidence.ts +43 -0
- package/src/cli/controller/runtime-config.ts +92 -0
- package/src/cli/controller/stuck-state-migration.ts +396 -0
- package/src/cli/controller/task-ledger.ts +632 -0
- package/src/cli/controller/task-status-resolver.ts +403 -0
- package/src/cli/controller/types.ts +273 -0
- package/src/cli/controller/work-mode.ts +186 -0
- package/src/cli/controller/worklog.ts +201 -0
- package/src/cli/diagnostic-entry.ts +39 -0
- package/src/cli/editing/edit-session.ts +1606 -0
- package/src/cli/editing/executable-modes.ts +70 -0
- package/src/cli/github/github.ts +274 -0
- package/src/cli/github/plugin.ts +345 -0
- package/src/cli/github/session.ts +104 -0
- package/src/cli/hook/prompt-guard-decision.ts +238 -0
- package/src/cli/hook/prompt-intents.ts +534 -0
- package/src/cli/hook/route-registry.ts +107 -0
- package/src/cli/hook/runtime.ts +303 -0
- package/src/cli/hook/state-snapshot.ts +388 -0
- package/src/cli/hook-entry.ts +51 -0
- package/src/cli/index.ts +574 -0
- package/src/cli/installer/managed-entries.ts +78 -0
- package/src/cli/installer/shared.ts +65 -0
- package/src/cli/installer/targets/claude.ts +136 -0
- package/src/cli/installer/targets/codex.ts +174 -0
- package/src/cli/installer/targets/registry.ts +28 -0
- package/src/cli/installer/types.ts +72 -0
- package/src/cli/local-bridge/connector-freshness.ts +529 -0
- package/src/cli/local-bridge/console-view-models.ts +324 -0
- package/src/cli/local-bridge/dashboard.ts +1312 -0
- package/src/cli/local-bridge/facade-api.ts +1877 -0
- package/src/cli/local-bridge/job-store.ts +1451 -0
- package/src/cli/local-bridge/mobile-intents.ts +344 -0
- package/src/cli/local-bridge/mobile-monitor.ts +209 -0
- package/src/cli/local-bridge/server.ts +3183 -0
- package/src/cli/local-bridge/types.ts +176 -0
- package/src/cli/mcp/access-mode.ts +132 -0
- package/src/cli/mcp/access-tools.ts +387 -0
- package/src/cli/mcp/audit.ts +32 -0
- package/src/cli/mcp/auth.ts +402 -0
- package/src/cli/mcp/instructions.ts +33 -0
- package/src/cli/mcp/legacy-context.ts +16 -0
- package/src/cli/mcp/legacy-tool-service.ts +5516 -0
- package/src/cli/mcp/multi-repository.ts +423 -0
- package/src/cli/mcp/oauth.ts +226 -0
- package/src/cli/mcp/paths.ts +134 -0
- package/src/cli/mcp/policy.ts +230 -0
- package/src/cli/mcp/proxy-env.ts +62 -0
- package/src/cli/mcp/readiness-projection.ts +24 -0
- package/src/cli/mcp/redaction.ts +21 -0
- package/src/cli/mcp/repo.ts +24 -0
- package/src/cli/mcp/repository-tools.ts +1295 -0
- package/src/cli/mcp/server.ts +341 -0
- package/src/cli/mcp/setup.ts +793 -0
- package/src/cli/mcp/tool-capability-cache.ts +87 -0
- package/src/cli/mcp/tools.ts +9 -0
- package/src/cli/mcp/toolset-names.ts +70 -0
- package/src/cli/mcp/toolset-selection.ts +41 -0
- package/src/cli/mcp/toolset.ts +300 -0
- package/src/cli/mcp/transports/AGENTS.md +39 -0
- package/src/cli/mcp/transports/CLAUDE.md +39 -0
- package/src/cli/mcp/transports/http.ts +1234 -0
- package/src/cli/mcp/transports/session-registry.ts +428 -0
- package/src/cli/mcp/transports/stdio.ts +8 -0
- package/src/cli/mcp/types.ts +41 -0
- package/src/cli/mcp/umbrella-tools.ts +117 -0
- package/src/cli/repo-adoption/reclaim-runtime.ts +681 -0
- package/src/cli/repositories/branch-name-policy.ts +57 -0
- package/src/cli/repositories/command-classifier.ts +600 -0
- package/src/cli/repositories/command-executor.ts +1066 -0
- package/src/cli/repositories/command-normalization.ts +53 -0
- package/src/cli/repositories/command-scope.ts +264 -0
- package/src/cli/repositories/controller-home.ts +88 -0
- package/src/cli/repositories/entity-migration.ts +199 -0
- package/src/cli/repositories/ephemeral-workspace.ts +62 -0
- package/src/cli/repositories/git-command-executor.ts +301 -0
- package/src/cli/repositories/goal-registry.ts +239 -0
- package/src/cli/repositories/identity.ts +64 -0
- package/src/cli/repositories/local-project-onboarding.ts +539 -0
- package/src/cli/repositories/locks.ts +262 -0
- package/src/cli/repositories/registry.ts +1057 -0
- package/src/cli/repositories/runtime-storage.ts +411 -0
- package/src/cli/repositories/safe-patch.ts +421 -0
- package/src/cli/repositories/selected-path-actions.ts +341 -0
- package/src/cli/repositories/structured-git.ts +257 -0
- package/src/cli/repositories/types.ts +155 -0
- package/src/cli/repositories/umbrella.ts +134 -0
- package/src/cli/repositories/workbench.ts +140 -0
- package/src/cli/repositories/worktree-storage.ts +137 -0
- package/src/cli/repository/inspector.ts +637 -0
- package/src/cli/repository/session-cache.ts +453 -0
- package/src/cli/runtime/helper-runner.ts +172 -0
- package/src/cli/runtime-invocation.ts +318 -0
- package/src/cli/tools/codegraph.ts +676 -0
- package/src/cli/v81-entry.ts +29 -0
- package/src/core/adoption/gitignore-plan.ts +127 -0
- package/src/core/adoption/helper-wrapper-plan.ts +159 -0
- package/src/core/adoption/manifest-templates.ts +60 -0
- package/src/core/adoption/modes.ts +7 -0
- package/src/core/adoption/operations.ts +126 -0
- package/src/core/adoption/plan.ts +149 -0
- package/src/core/adoption/render.ts +70 -0
- package/src/core/adoption/rollback.ts +62 -0
- package/src/core/adoption/summary.ts +43 -0
- package/src/core/adoption/workflow-contract-asset.ts +14 -0
- package/src/core/adoption/workflow-contract-plan.ts +31 -0
- package/src/effects/fs-transaction.ts +540 -0
- package/src/effects/git-executable.ts +59 -0
- package/src/effects/managed-block.ts +86 -0
- package/src/effects/path-safety.ts +47 -0
- package/src/effects/process-runner.ts +192 -0
- package/src/runtime/assistant/action-proposals.ts +279 -0
- package/src/runtime/assistant/intent.ts +395 -0
- package/src/runtime/assistant/model-provider.ts +460 -0
- package/src/runtime/assistant/openapi.ts +223 -0
- package/src/runtime/assistant/policy.ts +89 -0
- package/src/runtime/assistant/readiness.ts +258 -0
- package/src/runtime/assistant/routine-runtime.ts +477 -0
- package/src/runtime/assistant/schedule-binding.ts +171 -0
- package/src/runtime/assistant/standing-grants.ts +375 -0
- package/src/runtime/assistant/store.ts +243 -0
- package/src/runtime/assistant/types.ts +135 -0
- package/src/runtime/context/codegraph-read-provider.ts +347 -0
- package/src/runtime/context/codegraph-sidecar.cjs +207 -0
- package/src/runtime/control-plane/dispatch-priority.ts +67 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +151 -0
- package/src/runtime/control-plane/execution/execution-identity.ts +456 -0
- package/src/runtime/control-plane/execution/session-store.ts +175 -0
- package/src/runtime/control-plane/execution/validation.ts +117 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +191 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +225 -0
- package/src/runtime/control-plane/execution/work-prepare-request-store.ts +159 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +445 -0
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +821 -0
- package/src/runtime/control-plane/facade/capability-registry.ts +279 -0
- package/src/runtime/control-plane/facade/check-normalization.ts +233 -0
- package/src/runtime/control-plane/facade/codex-delegation.ts +196 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +199 -0
- package/src/runtime/control-plane/facade/facade-result.ts +41 -0
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +177 -0
- package/src/runtime/control-plane/facade/goal-workloop.ts +1216 -0
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +221 -0
- package/src/runtime/control-plane/facade/index.ts +16 -0
- package/src/runtime/control-plane/facade/operation-digest.ts +528 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +403 -0
- package/src/runtime/control-plane/facade/policy-gate.ts +149 -0
- package/src/runtime/control-plane/facade/requirement-board.ts +280 -0
- package/src/runtime/control-plane/facade/self-healing-loop.ts +555 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +108 -0
- package/src/runtime/control-plane/facade/types.ts +729 -0
- package/src/runtime/control-plane/facade/work-admission-policy.ts +93 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +104 -0
- package/src/runtime/control-plane/facade/work-contract-store.ts +1118 -0
- package/src/runtime/control-plane/facade/work-task-projection.ts +66 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +365 -0
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +920 -0
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +89 -0
- package/src/runtime/control-plane/goal-loop/config-facade.ts +700 -0
- package/src/runtime/control-plane/goal-loop/config-store.ts +570 -0
- package/src/runtime/control-plane/goal-loop/config-types.ts +213 -0
- package/src/runtime/control-plane/goal-loop/executor-router.ts +109 -0
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +149 -0
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +304 -0
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +1001 -0
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +160 -0
- package/src/runtime/control-plane/goal-loop/index.ts +13 -0
- package/src/runtime/control-plane/goal-loop/local-tools.ts +290 -0
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +203 -0
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +93 -0
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +545 -0
- package/src/runtime/control-plane/goal-loop/types.ts +378 -0
- package/src/runtime/control-plane/governance/access-policy.ts +147 -0
- package/src/runtime/control-plane/governance/authorization.ts +195 -0
- package/src/runtime/control-plane/governance/external-effects.ts +43 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +97 -0
- package/src/runtime/control-plane/persistence/metadata-payload-policy.ts +53 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +241 -0
- package/src/runtime/control-plane/persistence/sqlite-store.ts +539 -0
- package/src/runtime/control-plane/release-retention.ts +447 -0
- package/src/runtime/control-plane/repo-actor/actor.ts +181 -0
- package/src/runtime/control-plane/repo-actor/registry.ts +20 -0
- package/src/runtime/control-plane/routing/index.ts +1 -0
- package/src/runtime/control-plane/routing/route-policy.ts +379 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +727 -0
- package/src/runtime/control-plane/runtime-generation.ts +509 -0
- package/src/runtime/control-plane/runtime-pid-ownership.ts +11 -0
- package/src/runtime/control-plane/runtime-status-client.ts +90 -0
- package/src/runtime/control-plane/startup-recovery.ts +198 -0
- package/src/runtime/diagnostics/connectivity-observability.ts +475 -0
- package/src/runtime/diagnostics/mcp-timing.ts +64 -0
- package/src/runtime/diagnostics/performance.ts +379 -0
- package/src/runtime/diagnostics/process-facade.ts +350 -0
- package/src/runtime/diagnostics/read-only-tool.ts +226 -0
- package/src/runtime/evidence/artifact-store.ts +154 -0
- package/src/runtime/evidence/event-ledger.ts +80 -0
- package/src/runtime/evidence/evidence-store.ts +68 -0
- package/src/runtime/evidence/process-check-receipt.ts +43 -0
- package/src/runtime/evidence/result-store.ts +209 -0
- package/src/runtime/evidence/sensitive-output.ts +353 -0
- package/src/runtime/execution/jobs/child-reference.ts +124 -0
- package/src/runtime/execution/jobs/legacy-adapter.ts +22 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +119 -0
- package/src/runtime/execution/jobs/receipt-store.ts +243 -0
- package/src/runtime/execution/jobs/store.ts +737 -0
- package/src/runtime/execution/jobs/timeouts.ts +109 -0
- package/src/runtime/execution/jobs/types.ts +230 -0
- package/src/runtime/execution/jobs/wait.ts +45 -0
- package/src/runtime/execution/process-runtime/canary.ts +26 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +272 -0
- package/src/runtime/execution/process-runtime/check-receipt.ts +185 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +60 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +321 -0
- package/src/runtime/execution/process-runtime/gc.ts +171 -0
- package/src/runtime/execution/process-runtime/index.ts +9 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +23 -0
- package/src/runtime/execution/process-runtime/process-runner-entry.ts +466 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +440 -0
- package/src/runtime/execution/process-runtime/runtime.ts +1816 -0
- package/src/runtime/execution/process-runtime/store.ts +687 -0
- package/src/runtime/execution/process-runtime/types.ts +352 -0
- package/src/runtime/execution/thin-harness/async-process.ts +181 -0
- package/src/runtime/execution/thin-harness/batch-executor.ts +671 -0
- package/src/runtime/execution/thin-harness/execution-router.ts +700 -0
- package/src/runtime/execution/thin-harness/fast-executor.ts +1263 -0
- package/src/runtime/execution/thin-harness/fast-receipt.ts +235 -0
- package/src/runtime/execution/thin-harness/fingerprint-worker.ts +191 -0
- package/src/runtime/execution/thin-harness/index.ts +13 -0
- package/src/runtime/execution/thin-harness/latency-trace.ts +107 -0
- package/src/runtime/execution/thin-harness/lightweight-lanes.ts +745 -0
- package/src/runtime/execution/thin-harness/mutation-gate.ts +578 -0
- package/src/runtime/execution/thin-harness/proposal-store.ts +409 -0
- package/src/runtime/execution/thin-harness/request-ledger.ts +595 -0
- package/src/runtime/execution/thin-harness/search-worker.ts +114 -0
- package/src/runtime/execution/thin-harness/types.ts +313 -0
- package/src/runtime/execution/thin-harness/workspace-savepoint.ts +333 -0
- package/src/runtime/execution/workers/executor.ts +687 -0
- package/src/runtime/execution/workers/ownership.ts +94 -0
- package/src/runtime/execution/workers/worker-entry.ts +277 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +2009 -0
- package/src/runtime/gateway/mcp/persisted-check-process.ts +255 -0
- package/src/runtime/gateway/mcp/process-tools.ts +230 -0
- package/src/runtime/gateway/mcp/resource-policy.ts +195 -0
- package/src/runtime/gateway/mcp/router.ts +1088 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +5994 -0
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +219 -0
- package/src/runtime/health/evaluator.ts +474 -0
- package/src/runtime/health/index.ts +2 -0
- package/src/runtime/health/operational-view.ts +198 -0
- package/src/runtime/maintenance/cleanup.ts +419 -0
- package/src/runtime/model-clients/deepseek-adapter.ts +312 -0
- package/src/runtime/model-clients/index.ts +2 -0
- package/src/runtime/model-clients/types.ts +101 -0
- package/src/runtime/observability/gateway-contention-metrics.ts +129 -0
- package/src/runtime/observability/route-session-benchmark.ts +110 -0
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +231 -0
- package/src/runtime/personal-assistant/reporting-runtime.ts +344 -0
- package/src/runtime/personal-assistant/triage-runtime.ts +748 -0
- package/src/runtime/plugins/app-store-connect-adapter.ts +868 -0
- package/src/runtime/plugins/browser-adapter.ts +2812 -0
- package/src/runtime/plugins/browser-automation-service.ts +46 -0
- package/src/runtime/plugins/browser-handoff-host.ts +304 -0
- package/src/runtime/plugins/browser-handoff.ts +371 -0
- package/src/runtime/plugins/browser-macos-bridge.ts +865 -0
- package/src/runtime/plugins/browser-node-bridge-host.ts +64 -0
- package/src/runtime/plugins/browser-node-bridge.ts +249 -0
- package/src/runtime/plugins/desktop-adapter.ts +374 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +418 -0
- package/src/runtime/plugins/errors.ts +39 -0
- package/src/runtime/plugins/execution-origin.ts +26 -0
- package/src/runtime/plugins/external-adapter.ts +322 -0
- package/src/runtime/plugins/external-registration.ts +308 -0
- package/src/runtime/plugins/external-unix-socket-probe.cjs +79 -0
- package/src/runtime/plugins/external-unix-socket.ts +238 -0
- package/src/runtime/plugins/first-party-registry.ts +37 -0
- package/src/runtime/plugins/github-adapter.ts +266 -0
- package/src/runtime/plugins/gmail-adapter.ts +729 -0
- package/src/runtime/plugins/google-calendar-adapter.ts +468 -0
- package/src/runtime/plugins/google-shared.ts +578 -0
- package/src/runtime/plugins/google-tasks-adapter.ts +535 -0
- package/src/runtime/plugins/interaction-session.ts +252 -0
- package/src/runtime/plugins/ios/agent-device-capabilities.ts +258 -0
- package/src/runtime/plugins/ios/agent-device-failures.ts +95 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +63 -0
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +268 -0
- package/src/runtime/plugins/ios/app-adapters.ts +122 -0
- package/src/runtime/plugins/ios-adapter.ts +509 -0
- package/src/runtime/plugins/ios-agent-device.ts +2311 -0
- package/src/runtime/plugins/ios-physical-device.ts +955 -0
- package/src/runtime/plugins/local-system-adapter.ts +1016 -0
- package/src/runtime/plugins/macos-capability-broker.ts +73 -0
- package/src/runtime/plugins/managed-process-adapter.ts +366 -0
- package/src/runtime/plugins/resend-adapter.ts +506 -0
- package/src/runtime/plugins/store.ts +1025 -0
- package/src/runtime/plugins/types.ts +138 -0
- package/src/runtime/projections/controller-context.ts +596 -0
- package/src/runtime/projections/git-status-sampler.ts +84 -0
- package/src/runtime/projections/invalidation.ts +250 -0
- package/src/runtime/projections/materialized-view.ts +694 -0
- package/src/runtime/recovery/actions.ts +158 -0
- package/src/runtime/recovery/audit.ts +38 -0
- package/src/runtime/recovery/classifier.ts +163 -0
- package/src/runtime/recovery/index.ts +9 -0
- package/src/runtime/recovery/local-jobs-repair.ts +393 -0
- package/src/runtime/recovery/maintenance-executor.ts +822 -0
- package/src/runtime/recovery/patch-handoff.ts +50 -0
- package/src/runtime/recovery/probe.ts +308 -0
- package/src/runtime/recovery/store.ts +35 -0
- package/src/runtime/recovery/types.ts +175 -0
- package/src/runtime/release/identity.ts +101 -0
- package/src/runtime/release/release-gate.ts +137 -0
- package/src/runtime/resources/claims/conflicts.ts +205 -0
- package/src/runtime/resources/index.ts +1 -0
- package/src/runtime/resources/leases/store.ts +539 -0
- package/src/runtime/resources/leases/types.ts +50 -0
- package/src/runtime/resources/ownership.ts +72 -0
- package/src/runtime/root/controller-services.ts +42 -0
- package/src/runtime/root/entry.ts +70 -0
- package/src/runtime/root/gateway-adapter.ts +46 -0
- package/src/runtime/root/local-bridge.ts +72 -0
- package/src/runtime/root/mcp-transport.ts +198 -0
- package/src/runtime/root/ownership.ts +94 -0
- package/src/runtime/root/readiness.ts +69 -0
- package/src/runtime/root/release-manifest.ts +147 -0
- package/src/runtime/root/release-materialize.ts +509 -0
- package/src/runtime/root/release-store.ts +236 -0
- package/src/runtime/root/runtime.ts +318 -0
- package/src/runtime/root/scheduler.ts +58 -0
- package/src/runtime/root/service-runner-entry.ts +20 -0
- package/src/runtime/root/service-runner.ts +69 -0
- package/src/runtime/root/service.ts +291 -0
- package/src/runtime/root/status.ts +127 -0
- package/src/runtime/root/types.ts +112 -0
- package/src/runtime/root/write-fence.ts +309 -0
- package/src/runtime/safe-tooling/browser-safe.ts +133 -0
- package/src/runtime/safe-tooling/external-filesystem.ts +304 -0
- package/src/runtime/safe-tooling/google-credential-store.ts +95 -0
- package/src/runtime/safe-tooling/google-oauth-broker.ts +286 -0
- package/src/runtime/safe-tooling/index.ts +11 -0
- package/src/runtime/safe-tooling/ios-development.ts +1039 -0
- package/src/runtime/safe-tooling/job-summary.ts +361 -0
- package/src/runtime/safe-tooling/plugin-summary.ts +53 -0
- package/src/runtime/safe-tooling/resend-oauth.ts +387 -0
- package/src/runtime/safe-tooling/review-artifacts.ts +133 -0
- package/src/runtime/safe-tooling/types.ts +103 -0
- package/src/runtime/safe-tooling/workspace-auth.ts +116 -0
- package/src/runtime/shared/bounded-child-supervisor.ts +235 -0
- package/src/runtime/shared/idempotent-retry.ts +128 -0
- package/src/runtime/shared/json-files.ts +43 -0
- package/src/runtime/shared/local-bridge-surface.ts +231 -0
- package/src/runtime/shared/managed-env.ts +154 -0
- package/src/runtime/shared/node-ts-loader.mjs +50 -0
- package/src/runtime/shared/process-environment.ts +97 -0
- package/src/runtime/shared/process-identity.ts +72 -0
- package/src/runtime/shared/process-tree.ts +227 -0
- package/src/runtime/shared/response-budget.ts +246 -0
- package/src/runtime/standalone-recovery/core.ts +2275 -0
- package/src/runtime/standalone-recovery/entry.ts +686 -0
- package/src/runtime/standalone-recovery/http-transport.ts +317 -0
- package/src/runtime/standalone-recovery/installer.ts +747 -0
- package/src/runtime/standalone-recovery/release.ts +241 -0
- package/src/runtime/watchdog/workflow-watchdog.ts +118 -0
- package/src/runtime/workflow/campaigns/cleanup.ts +263 -0
- package/src/runtime/workflow/campaigns/engine.ts +112 -0
- package/src/runtime/workflow/campaigns/normalize.ts +100 -0
- package/src/runtime/workflow/campaigns/review.ts +315 -0
- package/src/runtime/workflow/campaigns/store.ts +526 -0
- package/src/runtime/workflow/campaigns/supervisor.ts +112 -0
- package/src/runtime/workflow/campaigns/types.ts +290 -0
- package/src/runtime/workflow/campaigns/workspace-agent.ts +155 -0
- package/src/runtime/workflow/campaigns/workspace.ts +200 -0
- package/src/runtime/workflow/findings/store.ts +165 -0
- package/src/runtime/workflow/findings/types.ts +36 -0
- package/src/runtime/workflow/portfolio/engine.ts +20 -0
- package/src/runtime/workflow/portfolio/store.ts +185 -0
- package/src/runtime/workflow/portfolio/types.ts +29 -0
- package/src/runtime/workflow/schedules/engine.ts +620 -0
- package/src/runtime/workflow/schedules/settlement.ts +144 -0
- package/src/runtime/workflow/schedules/store.ts +417 -0
- package/src/runtime/workflow/schedules/types.ts +124 -0
- package/src/runtime/workspace-targets/index.ts +24 -0
- package/src/runtime/workspace-targets/target-grant.ts +721 -0
- package/src/testing/test-governance.ts +626 -0
- package/src/version.ts +21 -0
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'crypto';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join, sep } from 'path';
|
|
5
|
+
import type {
|
|
6
|
+
CompletionReceipt,
|
|
7
|
+
ControllerAgent,
|
|
8
|
+
ControllerIssue,
|
|
9
|
+
ControllerTask,
|
|
10
|
+
GitHubIssueLink,
|
|
11
|
+
IssueKind,
|
|
12
|
+
IssueReadiness,
|
|
13
|
+
IssueReadinessFinding,
|
|
14
|
+
IssueStatus,
|
|
15
|
+
TaskDraft,
|
|
16
|
+
TaskStatus,
|
|
17
|
+
TaskVerification,
|
|
18
|
+
TaskReadiness,
|
|
19
|
+
} from './types';
|
|
20
|
+
import { clearCurrentIssue, loadControllerProjectState, saveControllerProjectState } from './project-state';
|
|
21
|
+
import { readActiveRunEvidence, readIssueRunEvidence, readTaskRunEvidence } from './run-evidence';
|
|
22
|
+
import {
|
|
23
|
+
resolveEffectiveTaskState,
|
|
24
|
+
resolveIssueLifecycleStatus,
|
|
25
|
+
resolveIssueTaskStates,
|
|
26
|
+
resolveTaskDependencies,
|
|
27
|
+
} from './task-status-resolver';
|
|
28
|
+
import { tryAppendControllerWorklogEvent } from './worklog';
|
|
29
|
+
import { completionEvidenceComplete, executionScopesConflict, taskExecutionPolicy, verificationEvidencePassed } from './execution-policy';
|
|
30
|
+
import { resolveCompletionTargetBranch } from './completion-target';
|
|
31
|
+
import { listControllerChecks } from './check-runner';
|
|
32
|
+
import { normalizeCheckIds } from '../../runtime/control-plane/facade/check-normalization';
|
|
33
|
+
import { getWorkContract } from '../../runtime/control-plane/facade/work-contract-store';
|
|
34
|
+
import { projectControllerTaskFromWork } from '../../runtime/control-plane/facade/work-task-projection';
|
|
35
|
+
import type { WorkContract } from '../../runtime/control-plane/facade/types';
|
|
36
|
+
import { resolveRepoPreferredControllerHome } from '../repositories/controller-home';
|
|
37
|
+
import { markControllerContextProjectionDirty } from '../../runtime/projections/controller-context';
|
|
38
|
+
import {
|
|
39
|
+
assertLegacyIssueWritesAllowed,
|
|
40
|
+
assertLegacyProjectBoardAvailable,
|
|
41
|
+
frozenLegacyIssueProjection,
|
|
42
|
+
legacyIssueAuthorityRetired,
|
|
43
|
+
listFrozenLegacyIssueProjections,
|
|
44
|
+
migratedIssueReadinessProjection,
|
|
45
|
+
} from './legacy-issue-cutover';
|
|
46
|
+
|
|
47
|
+
const ISSUE_ROOT = 'tasks/issues';
|
|
48
|
+
const EPHEMERAL_ISSUE_ROOT = '.ai/harness/ephemeral-issues';
|
|
49
|
+
const ISSUE_SUMMARY_NOTE_LIMIT = 2;
|
|
50
|
+
const ISSUE_SUMMARY_NOTE_CHARS = 280;
|
|
51
|
+
const ISSUE_SUMMARY_RUN_ID_LIMIT = 10;
|
|
52
|
+
|
|
53
|
+
function slugify(value: string): string {
|
|
54
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'issue';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dateStamp(date = new Date()): string {
|
|
58
|
+
return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function issueId(): string {
|
|
62
|
+
return `ISS-${dateStamp()}-${randomBytes(3).toString('hex').toUpperCase()}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeStrings(value: unknown): string[] {
|
|
66
|
+
if (!Array.isArray(value)) return [];
|
|
67
|
+
return Array.from(new Set(value.map((entry) => String(entry).trim()).filter(Boolean)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function issueRoot(issue?: Pick<ControllerIssue, 'ephemeral'>): string {
|
|
71
|
+
return issue?.ephemeral ? EPHEMERAL_ISSUE_ROOT : ISSUE_ROOT;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function issueFiles(repoRoot: string, includeEphemeral = false): Array<{ root: string; name: string }> {
|
|
75
|
+
const roots = includeEphemeral ? [ISSUE_ROOT, EPHEMERAL_ISSUE_ROOT] : [ISSUE_ROOT];
|
|
76
|
+
return roots.flatMap((root) => {
|
|
77
|
+
const absolute = join(repoRoot, root);
|
|
78
|
+
if (!existsSync(absolute)) return [];
|
|
79
|
+
return readdirSync(absolute)
|
|
80
|
+
.filter((name) => name.endsWith('.issue.json'))
|
|
81
|
+
.sort()
|
|
82
|
+
.map((name) => ({ root, name }));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function issuePath(repoRoot: string, issue: Pick<ControllerIssue, 'id' | 'slug' | 'ephemeral'>): string {
|
|
87
|
+
const absolute = join(repoRoot, issueRoot(issue), `${issue.id}-${issue.slug}.issue.json`);
|
|
88
|
+
if (issue.ephemeral && absolute.includes(`${sep}tasks${sep}issues${sep}`)) {
|
|
89
|
+
throw new Error(`EPHEMERAL_ISSUE_PATH_INVALID: refused durable path for ephemeral Issue ${issue.id}`);
|
|
90
|
+
}
|
|
91
|
+
return absolute;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function markdownPath(repoRoot: string, issue: Pick<ControllerIssue, 'id' | 'slug' | 'ephemeral'>): string {
|
|
95
|
+
const absolute = join(repoRoot, issueRoot(issue), `${issue.id}-${issue.slug}.issue.md`);
|
|
96
|
+
if (issue.ephemeral && absolute.includes(`${sep}tasks${sep}issues${sep}`)) {
|
|
97
|
+
throw new Error(`EPHEMERAL_ISSUE_PATH_INVALID: refused durable path for ephemeral Issue ${issue.id}`);
|
|
98
|
+
}
|
|
99
|
+
return absolute;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderGitHubLink(link?: GitHubIssueLink): string[] {
|
|
103
|
+
if (!link) return ['- Not published.'];
|
|
104
|
+
return [
|
|
105
|
+
`- Issue: ${link.url}`,
|
|
106
|
+
`- Repository: \`${link.owner}/${link.repo}\``,
|
|
107
|
+
...(link.projectNumber ? [`- Project: \`${link.projectOwner ?? link.owner}/${link.projectNumber}\``] : []),
|
|
108
|
+
`- Last synced: ${link.syncedAt}`,
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inlineArtifactText(value: unknown): string {
|
|
113
|
+
return typeof value === 'string'
|
|
114
|
+
? value.replace(/\s+/g, ' ').trim()
|
|
115
|
+
: '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderRelatedArtifact(item: unknown): string {
|
|
119
|
+
if (typeof item === 'string') return `- \`${item}\``;
|
|
120
|
+
if (item && typeof item === 'object') {
|
|
121
|
+
const artifact = item as Record<string, unknown>;
|
|
122
|
+
const kind = inlineArtifactText(artifact.kind);
|
|
123
|
+
const path = inlineArtifactText(artifact.path);
|
|
124
|
+
const summary = inlineArtifactText(artifact.summary);
|
|
125
|
+
const head = [kind ? `[${kind}]` : '', path ? `\`${path}\`` : ''].filter(Boolean).join(' ');
|
|
126
|
+
if (head || summary) return `- ${head || 'Artifact'}${summary ? ` — ${summary}` : ''}`;
|
|
127
|
+
try {
|
|
128
|
+
return `- \`${JSON.stringify(artifact)}\``;
|
|
129
|
+
} catch {
|
|
130
|
+
return '- Unreadable artifact metadata.';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return `- \`${String(item)}\``;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderIssueMarkdown(issue: ControllerIssue): string {
|
|
137
|
+
const taskLines = issue.tasks.length === 0
|
|
138
|
+
? ['- No tasks planned yet.']
|
|
139
|
+
: issue.tasks.flatMap((task) => [
|
|
140
|
+
`### ${task.id} — ${task.title}`,
|
|
141
|
+
'',
|
|
142
|
+
`- Status: \`${task.status}\``,
|
|
143
|
+
`- Objective: ${task.objective}`,
|
|
144
|
+
`- Depends on: ${task.dependsOn.length ? task.dependsOn.map((item) => `\`${item}\``).join(', ') : 'none'}`,
|
|
145
|
+
`- Allowed paths: ${task.allowedPaths.length ? task.allowedPaths.map((item) => `\`${item}\``).join(', ') : 'not defined'}`,
|
|
146
|
+
`- Checks: ${task.checks.length ? task.checks.map((item) => `\`${item}\``).join(', ') : 'not defined'}`,
|
|
147
|
+
`- Execution hint: ${task.recommendedAgent ? `agent / ${task.recommendedAgent}` : 'selected at runtime'}`,
|
|
148
|
+
...(task.github ? [`- GitHub: ${task.github.url}`] : []),
|
|
149
|
+
...(task.supersededBy?.length ? [`- Superseded by: ${task.supersededBy.map((item) => `\`${item}\``).join(', ')}`] : []),
|
|
150
|
+
'',
|
|
151
|
+
]);
|
|
152
|
+
return [
|
|
153
|
+
'---',
|
|
154
|
+
`id: ${JSON.stringify(issue.id)}`,
|
|
155
|
+
`kind: ${JSON.stringify(issue.kind)}`,
|
|
156
|
+
`status: ${JSON.stringify(issue.status)}`,
|
|
157
|
+
`updated_at: ${JSON.stringify(issue.updatedAt)}`,
|
|
158
|
+
...(issue.archivedAt ? [`archived_at: ${JSON.stringify(issue.archivedAt)}`] : []),
|
|
159
|
+
'source: "forge"',
|
|
160
|
+
'---',
|
|
161
|
+
'',
|
|
162
|
+
`# ${issue.title}`,
|
|
163
|
+
'',
|
|
164
|
+
issue.summary || 'No summary provided.',
|
|
165
|
+
'',
|
|
166
|
+
'## Goals',
|
|
167
|
+
'',
|
|
168
|
+
...(issue.goals.length ? issue.goals.map((item) => `- ${item}`) : ['- TBD']),
|
|
169
|
+
'',
|
|
170
|
+
'## Non-goals',
|
|
171
|
+
'',
|
|
172
|
+
...(issue.nonGoals.length ? issue.nonGoals.map((item) => `- ${item}`) : ['- None recorded.']),
|
|
173
|
+
'',
|
|
174
|
+
'## Acceptance Criteria',
|
|
175
|
+
'',
|
|
176
|
+
...(issue.acceptanceCriteria.length ? issue.acceptanceCriteria.map((item) => `- [ ] ${item}`) : ['- [ ] Define issue-level acceptance criteria.']),
|
|
177
|
+
'',
|
|
178
|
+
'## GitHub',
|
|
179
|
+
'',
|
|
180
|
+
...renderGitHubLink(issue.github),
|
|
181
|
+
'',
|
|
182
|
+
'## Tasks',
|
|
183
|
+
'',
|
|
184
|
+
...taskLines,
|
|
185
|
+
'## Related Artifacts',
|
|
186
|
+
'',
|
|
187
|
+
...(issue.relatedArtifacts.length ? (issue.relatedArtifacts as unknown[]).map(renderRelatedArtifact) : ['- None.']),
|
|
188
|
+
'',
|
|
189
|
+
].join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function writeIssue(repoRoot: string, issue: ControllerIssue): ControllerIssue {
|
|
193
|
+
assertLegacyIssueWritesAllowed(repoRoot, issue);
|
|
194
|
+
const root = join(repoRoot, issueRoot(issue));
|
|
195
|
+
mkdirSync(root, { recursive: true });
|
|
196
|
+
issue.schemaVersion = 5;
|
|
197
|
+
const expectedJson = issuePath(repoRoot, issue);
|
|
198
|
+
const expectedMarkdown = markdownPath(repoRoot, issue);
|
|
199
|
+
for (const name of readdirSync(root)) {
|
|
200
|
+
if (!name.startsWith(`${issue.id}-`)) continue;
|
|
201
|
+
const candidate = join(root, name);
|
|
202
|
+
if (candidate !== expectedJson && candidate !== expectedMarkdown) rmSync(candidate, { force: true });
|
|
203
|
+
}
|
|
204
|
+
writeFileSync(expectedJson, `${JSON.stringify(issue, null, 2)}\n`, 'utf-8');
|
|
205
|
+
writeFileSync(expectedMarkdown, renderIssueMarkdown(issue), 'utf-8');
|
|
206
|
+
markControllerContextProjectionDirty(repoRoot, `issue:${issue.id}:updated`);
|
|
207
|
+
const projectState = loadControllerProjectState(repoRoot);
|
|
208
|
+
if (projectState.currentIssueId === issue.id && (issue.archivedAt || ['done', 'cancelled'].includes(issue.status))) {
|
|
209
|
+
clearCurrentIssue(repoRoot, 'issue-terminal-convergence');
|
|
210
|
+
}
|
|
211
|
+
return issue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeLoadedIssue(issue: ControllerIssue): ControllerIssue {
|
|
215
|
+
issue.schemaVersion = issue.schemaVersion ?? 1;
|
|
216
|
+
issue.tasks = (issue.tasks ?? []).map((task) => ({
|
|
217
|
+
...task,
|
|
218
|
+
allowedPaths: task.allowedPaths ?? [],
|
|
219
|
+
forbiddenPaths: task.forbiddenPaths ?? [],
|
|
220
|
+
checks: task.checks ?? [],
|
|
221
|
+
acceptanceCriteria: task.acceptanceCriteria ?? [],
|
|
222
|
+
risk: task.risk ?? 'medium',
|
|
223
|
+
recommendedAgent: task.recommendedAgent,
|
|
224
|
+
notes: task.notes ?? [],
|
|
225
|
+
runIds: task.runIds ?? [],
|
|
226
|
+
}));
|
|
227
|
+
return issue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function listIssues(repoRoot: string, options: { includeEphemeral?: boolean } = {}): ControllerIssue[] {
|
|
231
|
+
return issueFiles(repoRoot, options.includeEphemeral ?? false)
|
|
232
|
+
.map(({ root, name }) => normalizeLoadedIssue(JSON.parse(readFileSync(join(repoRoot, root, name), 'utf-8')) as ControllerIssue))
|
|
233
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getIssue(repoRoot: string, id: string): ControllerIssue {
|
|
237
|
+
const issue = listIssues(repoRoot, { includeEphemeral: true }).find((entry) => entry.id === id);
|
|
238
|
+
if (!issue) throw new Error(`issue not found: ${id}`);
|
|
239
|
+
return issue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function projectIssueTasksFromWork(repoRoot: string, issue: ControllerIssue): ControllerTask[] {
|
|
243
|
+
const controllerHomes = [...new Set([
|
|
244
|
+
resolveRepoPreferredControllerHome(repoRoot),
|
|
245
|
+
join(repoRoot, '_ops', 'controller-home'),
|
|
246
|
+
])];
|
|
247
|
+
return issue.tasks.map((task) => {
|
|
248
|
+
if (!task.workId) return task;
|
|
249
|
+
const repoId = task.repoId ?? issue.repoId;
|
|
250
|
+
if (!repoId) return task.status === 'done' ? task : { ...task, status: 'launch_blocked' };
|
|
251
|
+
for (const controllerHome of controllerHomes) {
|
|
252
|
+
try {
|
|
253
|
+
const work = getWorkContract({ controllerHome, repoId }, task.workId);
|
|
254
|
+
if (!work || (work.issueId && work.issueId !== issue.id) || (work.taskId && work.taskId !== task.id)) continue;
|
|
255
|
+
if (work.status === 'completed' && !work.completionReceipt) continue;
|
|
256
|
+
return projectControllerTaskFromWork(task, work);
|
|
257
|
+
} catch {
|
|
258
|
+
// A compatibility read never manufactures state from an unavailable
|
|
259
|
+
// Work authority. Try the bounded repo-local authority, then close.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return task.status === 'done' ? task : { ...task, status: 'launch_blocked' };
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function projectIssueEffectiveView(repoRoot: string, issue: ControllerIssue) {
|
|
267
|
+
const projectedIssue = { ...issue, tasks: projectIssueTasksFromWork(repoRoot, issue) };
|
|
268
|
+
const states = resolveIssueTaskStates(projectedIssue, readIssueRunEvidence(repoRoot, projectedIssue));
|
|
269
|
+
return {
|
|
270
|
+
...projectedIssue,
|
|
271
|
+
lifecycleStatus: resolveIssueLifecycleStatus(projectedIssue),
|
|
272
|
+
tasks: projectedIssue.tasks.map((task) => {
|
|
273
|
+
const state = states.get(task.id)!;
|
|
274
|
+
return {
|
|
275
|
+
...task,
|
|
276
|
+
declaredStatus: state.declaredStatus,
|
|
277
|
+
effectiveStatus: state.effectiveStatus,
|
|
278
|
+
statusReason: state.reason,
|
|
279
|
+
latestRunId: state.latestRunId,
|
|
280
|
+
latestRunStatus: state.latestRunStatus,
|
|
281
|
+
activeRunId: state.activeRunId,
|
|
282
|
+
activeRunStatus: state.activeRunStatus,
|
|
283
|
+
activeRunIds: state.activeRunIds,
|
|
284
|
+
multipleActiveRuns: state.multipleActiveRuns,
|
|
285
|
+
historicalRunOutcomes: state.historicalRunOutcomes,
|
|
286
|
+
verificationStatus: state.verificationStatus,
|
|
287
|
+
terminal: state.terminal,
|
|
288
|
+
inactive: state.inactive,
|
|
289
|
+
dispatchable: state.dispatchable,
|
|
290
|
+
retryable: state.retryable,
|
|
291
|
+
requiresExplicitRetry: state.requiresExplicitRetry,
|
|
292
|
+
dependencyState: resolveTaskDependencies(issue, task, states),
|
|
293
|
+
};
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function summarizeText(value: string, maxChars: number): string {
|
|
299
|
+
return value.length <= maxChars ? value : `${value.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function summarizeNotes(notes: string[]): string[] {
|
|
303
|
+
return notes.slice(-ISSUE_SUMMARY_NOTE_LIMIT).map((note) => summarizeText(note, ISSUE_SUMMARY_NOTE_CHARS));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function summarizeVerification(verification?: TaskVerification) {
|
|
307
|
+
if (!verification) return undefined;
|
|
308
|
+
return {
|
|
309
|
+
repoId: verification.repoId,
|
|
310
|
+
runId: verification.runId,
|
|
311
|
+
integratedRevision: verification.integratedRevision,
|
|
312
|
+
reviewedDiffHash: verification.reviewedDiffHash,
|
|
313
|
+
checkResults: verification.checkResults,
|
|
314
|
+
acceptanceResults: verification.acceptanceResults.map((result) => ({
|
|
315
|
+
criterion: result.criterion,
|
|
316
|
+
ok: result.ok,
|
|
317
|
+
outcome: result.outcome,
|
|
318
|
+
source: result.source,
|
|
319
|
+
})),
|
|
320
|
+
acceptanceResultCount: verification.acceptanceResults.length,
|
|
321
|
+
commandEvidenceCount: verification.commandEvidence?.length ?? 0,
|
|
322
|
+
completionReceipt: verification.completionReceipt
|
|
323
|
+
? {
|
|
324
|
+
receiptId: verification.completionReceipt.receiptId,
|
|
325
|
+
source: verification.completionReceipt.source,
|
|
326
|
+
targetBranch: verification.completionReceipt.targetBranch,
|
|
327
|
+
targetRevision: verification.completionReceipt.targetRevision,
|
|
328
|
+
changedPathCount: verification.completionReceipt.changedPaths.length,
|
|
329
|
+
cleanupStatus: verification.completionReceipt.cleanup.status,
|
|
330
|
+
maintenanceWarningCount: verification.completionReceipt.cleanup.warnings.length,
|
|
331
|
+
blockerCount: verification.completionReceipt.cleanup.blockers.length,
|
|
332
|
+
}
|
|
333
|
+
: undefined,
|
|
334
|
+
reviewer: verification.reviewer,
|
|
335
|
+
verifiedAt: verification.verifiedAt,
|
|
336
|
+
autoCompleted: verification.autoCompleted,
|
|
337
|
+
detailLevel: 'summary' as const,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function summarizeEffectiveTaskView(task: ReturnType<typeof projectIssueEffectiveView>['tasks'][number]) {
|
|
342
|
+
const {
|
|
343
|
+
notes,
|
|
344
|
+
runIds,
|
|
345
|
+
verification,
|
|
346
|
+
historicalRunOutcomes,
|
|
347
|
+
...rest
|
|
348
|
+
} = task;
|
|
349
|
+
return {
|
|
350
|
+
...rest,
|
|
351
|
+
notes: summarizeNotes(notes),
|
|
352
|
+
noteCount: notes.length,
|
|
353
|
+
runIds: runIds.slice(-ISSUE_SUMMARY_RUN_ID_LIMIT),
|
|
354
|
+
runIdCount: runIds.length,
|
|
355
|
+
verification: summarizeVerification(verification),
|
|
356
|
+
historicalRunOutcomeCount: historicalRunOutcomes.length,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function getIssueEffectiveView(repoRoot: string, id: string) {
|
|
361
|
+
return projectIssueEffectiveView(repoRoot, getIssue(repoRoot, id));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function getIssueReadView(
|
|
365
|
+
repoRoot: string,
|
|
366
|
+
id: string,
|
|
367
|
+
detailLevel: 'summary' | 'full' = 'summary',
|
|
368
|
+
) {
|
|
369
|
+
const frozen = frozenLegacyIssueProjection(repoRoot, id, detailLevel);
|
|
370
|
+
if (frozen) return frozen;
|
|
371
|
+
const full = getIssueEffectiveView(repoRoot, id);
|
|
372
|
+
if (detailLevel === 'full') return { ...full, detailLevel };
|
|
373
|
+
return {
|
|
374
|
+
...full,
|
|
375
|
+
detailLevel,
|
|
376
|
+
tasks: full.tasks.map(summarizeEffectiveTaskView),
|
|
377
|
+
next: 'Call get_issue with detail_level=full only when full notes, verification evidence, or run history is required.',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function listIssueEffectiveViews(repoRoot: string) {
|
|
382
|
+
if (legacyIssueAuthorityRetired(repoRoot)) return listFrozenLegacyIssueProjections(repoRoot);
|
|
383
|
+
return listIssues(repoRoot).map((issue) => projectIssueEffectiveView(repoRoot, issue));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function assertIssueExecutionActive(issue: ControllerIssue, operation: string): void {
|
|
387
|
+
const lifecycle = resolveIssueLifecycleStatus(issue);
|
|
388
|
+
if (lifecycle !== 'active') {
|
|
389
|
+
throw new Error(`${operation} is not allowed while Issue ${issue.id} is ${lifecycle}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function effectiveTaskState(repoRoot: string, issue: ControllerIssue, task: ControllerTask) {
|
|
394
|
+
return resolveEffectiveTaskState({ issue, task, runs: readTaskRunEvidence(repoRoot, task) });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function nextTaskId(tasks: ControllerTask[]): string {
|
|
398
|
+
const max = tasks.reduce((current, task) => {
|
|
399
|
+
const match = /^T(\d+)$/.exec(task.id);
|
|
400
|
+
return match ? Math.max(current, Number(match[1])) : current;
|
|
401
|
+
}, 0);
|
|
402
|
+
return `T${max + 1}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function taskFromDraft(id: string, draft: TaskDraft, now: string): ControllerTask {
|
|
406
|
+
return {
|
|
407
|
+
id,
|
|
408
|
+
title: String(draft.title ?? '').trim() || id,
|
|
409
|
+
objective: String(draft.objective ?? '').trim() || 'Complete the scoped task.',
|
|
410
|
+
status: (draft.dependsOn?.length ?? 0) > 0 ? 'planned' : 'ready',
|
|
411
|
+
dependsOn: normalizeStrings(draft.dependsOn),
|
|
412
|
+
allowedPaths: normalizeStrings(draft.allowedPaths),
|
|
413
|
+
forbiddenPaths: normalizeStrings(draft.forbiddenPaths),
|
|
414
|
+
checks: normalizeStrings(draft.checks),
|
|
415
|
+
acceptanceCriteria: normalizeStrings(draft.acceptanceCriteria),
|
|
416
|
+
risk: draft.risk ?? 'medium',
|
|
417
|
+
recommendedAgent: draft.recommendedAgent,
|
|
418
|
+
notes: [],
|
|
419
|
+
runIds: [],
|
|
420
|
+
createdAt: now,
|
|
421
|
+
updatedAt: now,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function validateTaskGraph(tasks: ControllerTask[]): void {
|
|
426
|
+
const ids = new Set(tasks.map((task) => task.id));
|
|
427
|
+
for (const task of tasks) {
|
|
428
|
+
for (const dependency of task.dependsOn) {
|
|
429
|
+
if (!ids.has(dependency)) throw new Error(`unknown task dependency: ${task.id} -> ${dependency}`);
|
|
430
|
+
if (dependency === task.id) throw new Error(`task cannot depend on itself: ${task.id}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const visiting = new Set<string>();
|
|
434
|
+
const visited = new Set<string>();
|
|
435
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
436
|
+
const visit = (id: string): void => {
|
|
437
|
+
if (visiting.has(id)) throw new Error(`task dependency cycle detected at ${id}`);
|
|
438
|
+
if (visited.has(id)) return;
|
|
439
|
+
visiting.add(id);
|
|
440
|
+
for (const dependency of byId.get(id)?.dependsOn ?? []) visit(dependency);
|
|
441
|
+
visiting.delete(id);
|
|
442
|
+
visited.add(id);
|
|
443
|
+
};
|
|
444
|
+
for (const task of tasks) visit(task.id);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function buildTasks(drafts: TaskDraft[], now = new Date().toISOString()): ControllerTask[] {
|
|
448
|
+
const tasks = drafts.map((draft, index) => taskFromDraft(`T${index + 1}`, draft, now));
|
|
449
|
+
validateTaskGraph(tasks);
|
|
450
|
+
return tasks;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
type TaskReadinessOptions = {
|
|
454
|
+
approveRisk?: boolean;
|
|
455
|
+
approveDestructive?: boolean;
|
|
456
|
+
retryFromRunId?: string;
|
|
457
|
+
activeRuns?: ReturnType<typeof readActiveRunEvidence>;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
function buildTaskReadiness(
|
|
461
|
+
repoRoot: string,
|
|
462
|
+
issue: ControllerIssue,
|
|
463
|
+
task: ControllerTask,
|
|
464
|
+
states: ReadonlyMap<string, ReturnType<typeof resolveEffectiveTaskState>>,
|
|
465
|
+
options: TaskReadinessOptions = {},
|
|
466
|
+
): TaskReadiness {
|
|
467
|
+
const blockers: IssueReadinessFinding[] = [];
|
|
468
|
+
const warnings: IssueReadinessFinding[] = [];
|
|
469
|
+
const lifecycle = resolveIssueLifecycleStatus(issue);
|
|
470
|
+
const state = states.get(task.id) ?? effectiveTaskState(repoRoot, issue, task);
|
|
471
|
+
const policy = taskExecutionPolicy(task);
|
|
472
|
+
const add = (code: string, level: 'blocker' | 'warning', message: string): void => {
|
|
473
|
+
(level === 'blocker' ? blockers : warnings).push(finding(code, level, message, task.id));
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
if (lifecycle !== 'active') add('ISSUE_INACTIVE', 'blocker', `Parent Issue lifecycle is ${lifecycle}.`);
|
|
477
|
+
if (!task.objective.trim()) add('TASK_OBJECTIVE_MISSING', 'blocker', 'Task objective is required.');
|
|
478
|
+
for (const message of policy.warnings) add('TASK_POLICY_WARNING', 'warning', message);
|
|
479
|
+
if (task.risk === 'high' && task.recommendedAgent === 'github-copilot') {
|
|
480
|
+
add('HIGH_RISK_CLOUD_AGENT', 'warning', 'High-risk cloud execution requires explicit review before merge.');
|
|
481
|
+
}
|
|
482
|
+
if (state.multipleActiveRuns) add('MULTIPLE_ACTIVE_RUNS', 'blocker', `Task has multiple active Runs: ${state.activeRunIds.join(', ')}.`);
|
|
483
|
+
else if (state.activeRunIds.length > 0) add('ACTIVE_RUN_CONFLICT', 'blocker', `Task already has active Run evidence: ${state.activeRunIds.join(', ')}.`);
|
|
484
|
+
|
|
485
|
+
if (policy.executionClass !== 'read_only' && task.allowedPaths.length > 0) {
|
|
486
|
+
for (const run of options.activeRuns ?? readActiveRunEvidence(repoRoot)) {
|
|
487
|
+
if (run.issueId === issue.id && run.taskId === task.id) continue;
|
|
488
|
+
if (!run.executionClass || !run.allowedPaths) continue;
|
|
489
|
+
if (!executionScopesConflict(
|
|
490
|
+
{ executionClass: policy.executionClass, allowedPaths: task.allowedPaths },
|
|
491
|
+
{ executionClass: run.executionClass, allowedPaths: run.allowedPaths },
|
|
492
|
+
)) continue;
|
|
493
|
+
add('ACTIVE_SCOPE_CONFLICT', 'blocker', `Declared write scope overlaps active Run ${run.runId} (${run.issueId}/${run.taskId}).`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const retryAuthorized = state.requiresExplicitRetry && state.retryable && options.retryFromRunId === state.latestRunId;
|
|
498
|
+
if (state.requiresExplicitRetry && !retryAuthorized) {
|
|
499
|
+
add('EXPLICIT_RETRY_REQUIRED', 'blocker', `Latest Run ${state.latestRunId ?? 'unknown'} requires an explicit retry.`);
|
|
500
|
+
} else if (!state.dispatchable && !retryAuthorized) {
|
|
501
|
+
add('TASK_NOT_DISPATCHABLE', 'blocker', `Effective status ${state.effectiveStatus} is not dispatchable: ${state.reason}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const dependencies = resolveTaskDependencies(issue, task, states);
|
|
505
|
+
for (const id of dependencies.pendingTaskIds) add('PENDING_DEPENDENCY', 'blocker', `Dependency ${id} is not complete.`);
|
|
506
|
+
for (const id of dependencies.cancelledTaskIds) add('CANCELLED_DEPENDENCY', 'blocker', `Dependency ${id} is cancelled or inactive.`);
|
|
507
|
+
for (const id of dependencies.missingTaskIds) add('MISSING_DEPENDENCY', 'blocker', `Dependency ${id} does not exist.`);
|
|
508
|
+
for (const migration of dependencies.supersededMigrations) {
|
|
509
|
+
add('SUPERSEDED_DEPENDENCY_MIGRATION', 'warning', `Dependency ${migration.dependencyTaskId} was superseded by ${migration.replacementTaskIds.join(', ')}; replacement states are authoritative.`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (policy.requiresScopedPaths && task.allowedPaths.length === 0) {
|
|
513
|
+
add('TASK_SCOPE_REQUIRED', 'blocker', `${policy.executionClass} requires an explicit allowed path scope.`);
|
|
514
|
+
}
|
|
515
|
+
if (policy.approval === 'confirm' && !options.approveRisk) {
|
|
516
|
+
add('RISK_CONFIRMATION_ADVISORY', 'warning', `${policy.executionClass} is marked for extra review, but local execution is not approval-gated in V8.`);
|
|
517
|
+
}
|
|
518
|
+
if (policy.approval === 'manual-only' && !options.approveDestructive) {
|
|
519
|
+
add('DESTRUCTIVE_APPROVAL_REQUIRED', 'blocker', 'A destructive or irreversible operation requires explicit authorization.');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const approvalBlockerCodes = new Set(['DESTRUCTIVE_APPROVAL_REQUIRED']);
|
|
523
|
+
const nonApprovalBlockers = blockers.filter((entry) => !approvalBlockerCodes.has(entry.code));
|
|
524
|
+
const approvalSatisfied = !blockers.some((entry) => approvalBlockerCodes.has(entry.code));
|
|
525
|
+
const score = Math.max(0, 100 - blockers.length * 25 - warnings.length * 3);
|
|
526
|
+
return {
|
|
527
|
+
issueId: issue.id,
|
|
528
|
+
taskId: task.id,
|
|
529
|
+
ready: blockers.length === 0,
|
|
530
|
+
queueable: nonApprovalBlockers.length === 0,
|
|
531
|
+
approvalSatisfied,
|
|
532
|
+
score,
|
|
533
|
+
blockers,
|
|
534
|
+
warnings,
|
|
535
|
+
approval: policy.approval,
|
|
536
|
+
executionClass: policy.executionClass,
|
|
537
|
+
effectiveStatus: state.effectiveStatus,
|
|
538
|
+
requiresExplicitRetry: state.requiresExplicitRetry,
|
|
539
|
+
retryable: state.retryable,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function inspectTaskReadiness(
|
|
544
|
+
repoRoot: string,
|
|
545
|
+
issueIdValue: string,
|
|
546
|
+
taskId: string,
|
|
547
|
+
options: TaskReadinessOptions = {},
|
|
548
|
+
): TaskReadiness {
|
|
549
|
+
if (legacyIssueAuthorityRetired(repoRoot)) {
|
|
550
|
+
throw new Error('LEGACY_TASK_READINESS_RETIRED: use PlanStep and Work diagnostics from controller-home SQLite.');
|
|
551
|
+
}
|
|
552
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
553
|
+
const task = issue.tasks.find((entry) => entry.id === taskId);
|
|
554
|
+
if (!task) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
555
|
+
const migrated = migratedIssueReadinessProjection(repoRoot, issue);
|
|
556
|
+
if (migrated) {
|
|
557
|
+
const projectedIssue = { ...issue, status: migrated.issueStatus };
|
|
558
|
+
return buildTaskReadiness(repoRoot, projectedIssue, task, migrated.states, options);
|
|
559
|
+
}
|
|
560
|
+
const states = resolveIssueTaskStates(issue, readIssueRunEvidence(repoRoot, issue));
|
|
561
|
+
return buildTaskReadiness(repoRoot, issue, task, states, options);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function refreshReadiness(repoRoot: string, issue: ControllerIssue): void {
|
|
565
|
+
if (issue.archivedAt || issue.status === 'cancelled') return;
|
|
566
|
+
|
|
567
|
+
const states = resolveIssueTaskStates(issue, readIssueRunEvidence(repoRoot, issue));
|
|
568
|
+
const activeRuns = readActiveRunEvidence(repoRoot);
|
|
569
|
+
for (const task of issue.tasks) {
|
|
570
|
+
// A Work-backed Task is a compatibility projection. Its status is owned by
|
|
571
|
+
// Work and must never be rewritten by legacy readiness refresh logic.
|
|
572
|
+
if (task.workId) continue;
|
|
573
|
+
const state = states.get(task.id)!;
|
|
574
|
+
if (state.terminal || state.inactive || state.requiresExplicitRetry || state.activeRunIds.length > 0) continue;
|
|
575
|
+
if (!['planned', 'ready', 'launch_blocked'].includes(task.status)) continue;
|
|
576
|
+
const readiness = buildTaskReadiness(repoRoot, issue, task, states, {
|
|
577
|
+
// Readiness status reflects executable work, while high-risk confirmation is supplied at dispatch time.
|
|
578
|
+
approveRisk: true,
|
|
579
|
+
approveDestructive: false,
|
|
580
|
+
activeRuns,
|
|
581
|
+
});
|
|
582
|
+
const transitionalBlockerCodes = new Set(['TASK_NOT_DISPATCHABLE']);
|
|
583
|
+
const nonApprovalBlockers = readiness.blockers.filter(
|
|
584
|
+
(entry) => entry.code !== 'DESTRUCTIVE_APPROVAL_REQUIRED' && !transitionalBlockerCodes.has(entry.code),
|
|
585
|
+
);
|
|
586
|
+
task.status = nonApprovalBlockers.length === 0
|
|
587
|
+
? 'ready'
|
|
588
|
+
: nonApprovalBlockers.some((entry) => ['CANCELLED_DEPENDENCY', 'MISSING_DEPENDENCY', 'TASK_SCOPE_REQUIRED'].includes(entry.code))
|
|
589
|
+
? 'launch_blocked'
|
|
590
|
+
: 'planned';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const refreshedStates = resolveIssueTaskStates(issue, readIssueRunEvidence(repoRoot, issue));
|
|
594
|
+
const active = issue.tasks.filter((task) => {
|
|
595
|
+
const state = refreshedStates.get(task.id)!;
|
|
596
|
+
return !state.terminal && !state.inactive;
|
|
597
|
+
});
|
|
598
|
+
if (issue.tasks.length > 0 && issue.tasks.every((task) => refreshedStates.get(task.id)?.effectiveStatus === 'done')) issue.status = 'done';
|
|
599
|
+
else if (active.some((task) => ['queued', 'running', 'waiting_for_user', 'review', 'verifying', 'ready_to_integrate', 'integrating', 'integration_blocked', 'integrated', 'cleanup_pending', 'cleanup_blocked', 'changes_requested', 'verified', 'done'].includes(refreshedStates.get(task.id)!.effectiveStatus))) issue.status = 'in_progress';
|
|
600
|
+
else {
|
|
601
|
+
const executable = active.some((task) => buildTaskReadiness(repoRoot, issue, task, refreshedStates, {
|
|
602
|
+
approveRisk: true,
|
|
603
|
+
approveDestructive: true,
|
|
604
|
+
activeRuns,
|
|
605
|
+
}).ready);
|
|
606
|
+
issue.status = executable || active.length > 0 ? 'planned' : 'launch_blocked';
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
export function createIssue(repoRoot: string, input: {
|
|
610
|
+
title: string;
|
|
611
|
+
kind?: IssueKind;
|
|
612
|
+
summary?: string;
|
|
613
|
+
goals?: string[];
|
|
614
|
+
nonGoals?: string[];
|
|
615
|
+
acceptanceCriteria?: string[];
|
|
616
|
+
relatedArtifacts?: string[];
|
|
617
|
+
tasks?: TaskDraft[];
|
|
618
|
+
allowWhileFocused?: boolean;
|
|
619
|
+
allowDuplicate?: boolean;
|
|
620
|
+
allowWhenPaused?: boolean;
|
|
621
|
+
ephemeral?: boolean;
|
|
622
|
+
ephemeralOwnerJobId?: string;
|
|
623
|
+
}): ControllerIssue {
|
|
624
|
+
if (!input.ephemeral) assertLegacyIssueWritesAllowed(repoRoot);
|
|
625
|
+
const title = input.title.trim();
|
|
626
|
+
if (!title) throw new Error('issue title is required');
|
|
627
|
+
const projectState = loadControllerProjectState(repoRoot);
|
|
628
|
+
const existingIssues = listIssues(repoRoot);
|
|
629
|
+
const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9\u3400-\u9fff]+/g, '');
|
|
630
|
+
const duplicate = existingIssues.find((entry) => !entry.archivedAt && !['done', 'cancelled'].includes(entry.status)
|
|
631
|
+
&& entry.title.toLowerCase().replace(/[^a-z0-9\u3400-\u9fff]+/g, '') === normalizedTitle);
|
|
632
|
+
// Duplicate and focus signals are governance hints, not execution gates.
|
|
633
|
+
const duplicateHint = !input.ephemeral && duplicate && !input.allowDuplicate ? duplicate.id : undefined;
|
|
634
|
+
if (!input.ephemeral && projectState.issueCreationMode === 'paused' && !input.allowWhenPaused) {
|
|
635
|
+
throw new Error('Issue creation is paused; resume the creation policy or explicitly override it');
|
|
636
|
+
}
|
|
637
|
+
const activeExisting = existingIssues.filter((entry) => !entry.archivedAt && !['done', 'cancelled'].includes(entry.status));
|
|
638
|
+
const focused = projectState.currentIssueId
|
|
639
|
+
? activeExisting.find((entry) => entry.id === projectState.currentIssueId)
|
|
640
|
+
: undefined;
|
|
641
|
+
// focus_only is retained as a UI preference only. Multiple active Issues do not block creation or execution.
|
|
642
|
+
const now = new Date().toISOString();
|
|
643
|
+
const issue: ControllerIssue = {
|
|
644
|
+
schemaVersion: 5,
|
|
645
|
+
id: issueId(),
|
|
646
|
+
title,
|
|
647
|
+
slug: slugify(title),
|
|
648
|
+
kind: input.kind ?? 'feature',
|
|
649
|
+
status: input.tasks?.length ? 'planned' : 'analysis',
|
|
650
|
+
summary: input.summary?.trim() ?? '',
|
|
651
|
+
goals: normalizeStrings(input.goals),
|
|
652
|
+
nonGoals: normalizeStrings(input.nonGoals),
|
|
653
|
+
acceptanceCriteria: normalizeStrings(input.acceptanceCriteria),
|
|
654
|
+
relatedArtifacts: normalizeStrings(input.relatedArtifacts),
|
|
655
|
+
tasks: buildTasks(input.tasks ?? [], now),
|
|
656
|
+
...(input.ephemeral ? { ephemeral: true, ephemeralOwnerJobId: input.ephemeralOwnerJobId?.trim() || undefined } : {}),
|
|
657
|
+
createdAt: now,
|
|
658
|
+
updatedAt: now,
|
|
659
|
+
};
|
|
660
|
+
refreshReadiness(repoRoot, issue);
|
|
661
|
+
const written = writeIssue(repoRoot, issue);
|
|
662
|
+
tryAppendControllerWorklogEvent(repoRoot, {
|
|
663
|
+
category: 'issue',
|
|
664
|
+
action: 'issue_created',
|
|
665
|
+
summary: `Created ${issue.id}: ${issue.title}`,
|
|
666
|
+
issueId: issue.id,
|
|
667
|
+
statusTo: issue.status,
|
|
668
|
+
details: {
|
|
669
|
+
kind: issue.kind,
|
|
670
|
+
taskCount: issue.tasks.length,
|
|
671
|
+
creationPolicy: projectState.issueCreationMode,
|
|
672
|
+
policyOverride: Boolean(input.allowWhileFocused || input.allowWhenPaused || input.allowDuplicate),
|
|
673
|
+
ephemeral: Boolean(input.ephemeral),
|
|
674
|
+
duplicateHint,
|
|
675
|
+
focusIsInformational: true,
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
if (!input.ephemeral && !focused && activeExisting.length === 0 && !['done', 'cancelled'].includes(issue.status)) {
|
|
679
|
+
saveControllerProjectState(repoRoot, { currentIssueId: issue.id }, 'issue-creation-policy');
|
|
680
|
+
}
|
|
681
|
+
return written;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export function planIssue(repoRoot: string, id: string, drafts: TaskDraft[]): ControllerIssue {
|
|
685
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id });
|
|
686
|
+
const issue = getIssue(repoRoot, id);
|
|
687
|
+
assertIssueExecutionActive(issue, 'plan_issue');
|
|
688
|
+
if (issue.tasks.some((task) => task.workId)) {
|
|
689
|
+
throw new Error(`TASK_PLAN_WORK_OWNED: ${issue.id} contains Work-backed Tasks; replan through Plan/Work relationships.`);
|
|
690
|
+
}
|
|
691
|
+
if (issue.tasks.some((task) => task.runIds.length > 0)) throw new Error('cannot replace task plan after runs have started; append, split, or supersede Tasks instead');
|
|
692
|
+
const now = new Date().toISOString();
|
|
693
|
+
issue.tasks = buildTasks(drafts, now);
|
|
694
|
+
issue.status = issue.tasks.length ? 'planned' : 'analysis';
|
|
695
|
+
issue.updatedAt = now;
|
|
696
|
+
refreshReadiness(repoRoot, issue);
|
|
697
|
+
const written = writeIssue(repoRoot, issue);
|
|
698
|
+
tryAppendControllerWorklogEvent(repoRoot, { category: 'issue', action: 'issue_planned', summary: `Replaced the Task plan for ${issue.id} with ${issue.tasks.length} Tasks.`, issueId: issue.id, details: { taskIds: issue.tasks.map((task) => task.id) } });
|
|
699
|
+
return written;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function appendTask(repoRoot: string, issueIdValue: string, draft: TaskDraft): ControllerIssue {
|
|
703
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
704
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
705
|
+
assertIssueExecutionActive(issue, 'append_task');
|
|
706
|
+
const now = new Date().toISOString();
|
|
707
|
+
issue.tasks.push(taskFromDraft(nextTaskId(issue.tasks), draft, now));
|
|
708
|
+
validateTaskGraph(issue.tasks);
|
|
709
|
+
issue.updatedAt = now;
|
|
710
|
+
refreshReadiness(repoRoot, issue);
|
|
711
|
+
const task = issue.tasks.at(-1)!;
|
|
712
|
+
const written = writeIssue(repoRoot, issue);
|
|
713
|
+
tryAppendControllerWorklogEvent(repoRoot, { category: 'task', action: 'task_added', summary: `Added ${task.id}: ${task.title}`, issueId: issue.id, taskId: task.id, statusTo: task.status });
|
|
714
|
+
return written;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function splitTask(repoRoot: string, issueIdValue: string, taskId: string, drafts: TaskDraft[]): ControllerIssue {
|
|
718
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
719
|
+
if (drafts.length < 2) throw new Error('split_task requires at least two replacement tasks');
|
|
720
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
721
|
+
const original = issue.tasks.find((task) => task.id === taskId);
|
|
722
|
+
const originalStatus = original?.status;
|
|
723
|
+
if (!original) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
724
|
+
if (original.workId) throw new Error(`TASK_PLAN_WORK_OWNED: ${issueIdValue}/${taskId} is bound to Work ${original.workId}`);
|
|
725
|
+
assertIssueExecutionActive(issue, 'split_task');
|
|
726
|
+
const originalState = effectiveTaskState(repoRoot, issue, original);
|
|
727
|
+
if (originalState.terminal || originalState.inactive) throw new Error(`cannot split task from effective status ${originalState.effectiveStatus}`);
|
|
728
|
+
if (originalState.activeRunIds.length > 0) throw new Error(`cancel active Run(s) ${originalState.activeRunIds.join(', ')} before splitting this Task`);
|
|
729
|
+
if (['verified'].includes(original.status)) throw new Error(`cannot split task from status ${original.status}`);
|
|
730
|
+
const now = new Date().toISOString();
|
|
731
|
+
const replacements: ControllerTask[] = [];
|
|
732
|
+
for (const draft of drafts) {
|
|
733
|
+
const replacement = taskFromDraft(nextTaskId([...issue.tasks, ...replacements]), {
|
|
734
|
+
...draft,
|
|
735
|
+
dependsOn: draft.dependsOn ?? original.dependsOn,
|
|
736
|
+
allowedPaths: draft.allowedPaths ?? original.allowedPaths,
|
|
737
|
+
forbiddenPaths: draft.forbiddenPaths ?? original.forbiddenPaths,
|
|
738
|
+
checks: draft.checks ?? original.checks,
|
|
739
|
+
risk: draft.risk ?? original.risk,
|
|
740
|
+
recommendedAgent: draft.recommendedAgent ?? original.recommendedAgent,
|
|
741
|
+
}, now);
|
|
742
|
+
replacements.push(replacement);
|
|
743
|
+
}
|
|
744
|
+
original.status = 'superseded';
|
|
745
|
+
original.supersededBy = replacements.map((task) => task.id);
|
|
746
|
+
original.updatedAt = now;
|
|
747
|
+
for (const task of issue.tasks) {
|
|
748
|
+
if (!task.dependsOn.includes(original.id)) continue;
|
|
749
|
+
task.dependsOn = Array.from(new Set(task.dependsOn.flatMap((dependency) => dependency === original.id ? replacements.map((item) => item.id) : [dependency])));
|
|
750
|
+
}
|
|
751
|
+
issue.tasks.push(...replacements);
|
|
752
|
+
validateTaskGraph(issue.tasks);
|
|
753
|
+
issue.updatedAt = now;
|
|
754
|
+
refreshReadiness(repoRoot, issue);
|
|
755
|
+
const written = writeIssue(repoRoot, issue);
|
|
756
|
+
tryAppendControllerWorklogEvent(repoRoot, { category: 'task', action: 'task_split', summary: `Split ${taskId} into ${replacements.map((task) => task.id).join(', ')}.`, issueId: issue.id, taskId, statusFrom: originalStatus, statusTo: 'superseded', details: { replacements: replacements.map((task) => task.id) } });
|
|
757
|
+
return written;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function supersedeTask(repoRoot: string, issueIdValue: string, taskId: string, replacementTaskIds: string[] = []): ControllerIssue {
|
|
761
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
762
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
763
|
+
const task = issue.tasks.find((entry) => entry.id === taskId);
|
|
764
|
+
if (!task) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
765
|
+
if (task.workId) throw new Error(`TASK_STATUS_WORK_OWNED: ${issueIdValue}/${taskId} is bound to Work ${task.workId}`);
|
|
766
|
+
assertIssueExecutionActive(issue, 'supersede_task');
|
|
767
|
+
const state = effectiveTaskState(repoRoot, issue, task);
|
|
768
|
+
if (state.terminal || state.inactive) throw new Error(`cannot supersede Task from effective status ${state.effectiveStatus}`);
|
|
769
|
+
if (state.activeRunIds.length > 0) throw new Error(`cancel active Run(s) ${state.activeRunIds.join(', ')} before superseding this Task`);
|
|
770
|
+
for (const replacement of replacementTaskIds) {
|
|
771
|
+
if (!issue.tasks.some((entry) => entry.id === replacement)) throw new Error(`replacement task not found: ${replacement}`);
|
|
772
|
+
}
|
|
773
|
+
const now = new Date().toISOString();
|
|
774
|
+
task.status = 'superseded';
|
|
775
|
+
task.supersededBy = normalizeStrings(replacementTaskIds);
|
|
776
|
+
task.updatedAt = now;
|
|
777
|
+
if (task.supersededBy.length > 0) {
|
|
778
|
+
for (const downstream of issue.tasks) {
|
|
779
|
+
if (!downstream.dependsOn.includes(task.id)) continue;
|
|
780
|
+
downstream.dependsOn = Array.from(new Set(
|
|
781
|
+
downstream.dependsOn.flatMap((dependency) => dependency === task.id ? task.supersededBy! : [dependency]),
|
|
782
|
+
));
|
|
783
|
+
downstream.updatedAt = now;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
issue.updatedAt = now;
|
|
787
|
+
refreshReadiness(repoRoot, issue);
|
|
788
|
+
const written = writeIssue(repoRoot, issue);
|
|
789
|
+
tryAppendControllerWorklogEvent(repoRoot, { category: 'task', action: 'task_superseded', summary: `Superseded ${taskId}.`, issueId: issue.id, taskId, statusTo: 'superseded', details: { replacementTaskIds } });
|
|
790
|
+
return written;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export function setTaskDependencies(repoRoot: string, issueIdValue: string, taskId: string, dependsOn: string[]): ControllerIssue {
|
|
794
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
795
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
796
|
+
const task = issue.tasks.find((entry) => entry.id === taskId);
|
|
797
|
+
if (!task) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
798
|
+
assertIssueExecutionActive(issue, 'set_task_dependencies');
|
|
799
|
+
const state = effectiveTaskState(repoRoot, issue, task);
|
|
800
|
+
if (state.terminal || state.inactive) throw new Error(`cannot change dependencies for effective status ${state.effectiveStatus}`);
|
|
801
|
+
task.dependsOn = normalizeStrings(dependsOn);
|
|
802
|
+
validateTaskGraph(issue.tasks);
|
|
803
|
+
task.updatedAt = new Date().toISOString();
|
|
804
|
+
issue.updatedAt = task.updatedAt;
|
|
805
|
+
refreshReadiness(repoRoot, issue);
|
|
806
|
+
const written = writeIssue(repoRoot, issue);
|
|
807
|
+
tryAppendControllerWorklogEvent(repoRoot, { category: 'task', action: 'task_dependencies_changed', summary: `Updated dependencies for ${taskId}.`, issueId: issue.id, taskId, details: { dependsOn: task.dependsOn } });
|
|
808
|
+
return written;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function updateIssue(repoRoot: string, id: string, patch: {
|
|
812
|
+
title?: string;
|
|
813
|
+
status?: IssueStatus;
|
|
814
|
+
summary?: string;
|
|
815
|
+
goals?: string[];
|
|
816
|
+
nonGoals?: string[];
|
|
817
|
+
acceptanceCriteria?: string[];
|
|
818
|
+
relatedArtifacts?: string[];
|
|
819
|
+
github?: GitHubIssueLink;
|
|
820
|
+
}): ControllerIssue {
|
|
821
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id });
|
|
822
|
+
const issue = getIssue(repoRoot, id);
|
|
823
|
+
const previousStatus = issue.status;
|
|
824
|
+
if (patch.title !== undefined) {
|
|
825
|
+
issue.title = patch.title.trim() || issue.title;
|
|
826
|
+
issue.slug = slugify(issue.title);
|
|
827
|
+
}
|
|
828
|
+
if (patch.status !== undefined) issue.status = patch.status;
|
|
829
|
+
if (patch.summary !== undefined) issue.summary = patch.summary.trim();
|
|
830
|
+
if (patch.goals !== undefined) issue.goals = normalizeStrings(patch.goals);
|
|
831
|
+
if (patch.nonGoals !== undefined) issue.nonGoals = normalizeStrings(patch.nonGoals);
|
|
832
|
+
if (patch.acceptanceCriteria !== undefined) issue.acceptanceCriteria = normalizeStrings(patch.acceptanceCriteria);
|
|
833
|
+
if (patch.relatedArtifacts !== undefined) issue.relatedArtifacts = normalizeStrings(patch.relatedArtifacts);
|
|
834
|
+
if (patch.github !== undefined) issue.github = patch.github;
|
|
835
|
+
issue.updatedAt = new Date().toISOString();
|
|
836
|
+
const written = writeIssue(repoRoot, issue);
|
|
837
|
+
tryAppendControllerWorklogEvent(repoRoot, {
|
|
838
|
+
category: patch.github ? 'github' : 'issue',
|
|
839
|
+
action: patch.github ? 'issue_github_linked' : 'issue_updated',
|
|
840
|
+
summary: patch.status && patch.status !== previousStatus ? `Issue ${issue.id} moved from ${previousStatus} to ${patch.status}.` : `Updated ${issue.id}: ${issue.title}`,
|
|
841
|
+
issueId: issue.id,
|
|
842
|
+
statusFrom: previousStatus,
|
|
843
|
+
statusTo: issue.status,
|
|
844
|
+
details: { fields: Object.keys(patch), githubUrl: patch.github?.url },
|
|
845
|
+
});
|
|
846
|
+
return written;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function updateTask(repoRoot: string, issueIdValue: string, taskId: string, patch: {
|
|
850
|
+
status?: TaskStatus;
|
|
851
|
+
note?: string;
|
|
852
|
+
runId?: string;
|
|
853
|
+
github?: GitHubIssueLink;
|
|
854
|
+
verification?: TaskVerification;
|
|
855
|
+
transition?: 'normal' | 'run_sync' | 'retry' | 'restore';
|
|
856
|
+
}): ControllerIssue {
|
|
857
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
858
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
859
|
+
const task = issue.tasks.find((entry) => entry.id === taskId);
|
|
860
|
+
if (!task) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
861
|
+
const previousStatus = task.status;
|
|
862
|
+
const previousState = resolveEffectiveTaskState({ issue, task, runs: readTaskRunEvidence(repoRoot, task) });
|
|
863
|
+
if (task.workId && patch.status !== undefined) {
|
|
864
|
+
throw new Error(`TASK_STATUS_WORK_OWNED: ${issue.id}/${task.id} is projected from Work ${task.workId}`);
|
|
865
|
+
}
|
|
866
|
+
if (task.workId && patch.verification !== undefined) {
|
|
867
|
+
throw new Error(`TASK_VERIFICATION_WORK_OWNED: ${issue.id}/${task.id} is projected from Work ${task.workId}`);
|
|
868
|
+
}
|
|
869
|
+
if (patch.status && patch.status !== previousStatus) {
|
|
870
|
+
const executableTarget = !['done', 'cancelled', 'superseded'].includes(patch.status);
|
|
871
|
+
if ((task.supersededBy?.length ?? 0) > 0 && patch.status !== 'superseded') {
|
|
872
|
+
throw new Error(`cannot move superseded Task ${task.id} to ${patch.status}; use an explicit replacement/restore operation`);
|
|
873
|
+
}
|
|
874
|
+
if (previousState.issueLifecycleStatus !== 'active' && executableTarget) {
|
|
875
|
+
throw new Error(`cannot move Task ${task.id} to ${patch.status} while parent Issue is ${previousState.issueLifecycleStatus}`);
|
|
876
|
+
}
|
|
877
|
+
if (['done', 'cancelled', 'superseded'].includes(previousStatus) && patch.transition !== 'restore') {
|
|
878
|
+
throw new Error(`explicit terminal Task ${task.id} cannot transition from ${previousStatus} to ${patch.status} without restore/reopen`);
|
|
879
|
+
}
|
|
880
|
+
if (patch.transition === 'run_sync' && (previousState.terminal || previousState.inactive)) {
|
|
881
|
+
throw new Error(`Run reconciliation cannot override effective terminal state ${previousState.effectiveStatus}`);
|
|
882
|
+
}
|
|
883
|
+
task.status = patch.status;
|
|
884
|
+
}
|
|
885
|
+
if (patch.note?.trim()) task.notes.push(patch.note.trim());
|
|
886
|
+
if (patch.runId?.trim() && !task.runIds.includes(patch.runId.trim())) task.runIds.push(patch.runId.trim());
|
|
887
|
+
if (patch.github !== undefined) task.github = patch.github;
|
|
888
|
+
if (patch.verification !== undefined) task.verification = patch.verification;
|
|
889
|
+
task.updatedAt = new Date().toISOString();
|
|
890
|
+
issue.updatedAt = task.updatedAt;
|
|
891
|
+
refreshReadiness(repoRoot, issue);
|
|
892
|
+
const written = writeIssue(repoRoot, issue);
|
|
893
|
+
if (patch.status && patch.status !== previousStatus) {
|
|
894
|
+
tryAppendControllerWorklogEvent(repoRoot, { category: 'task', action: 'task_status_changed', summary: `${task.id} moved from ${previousStatus} to ${task.status}.`, issueId: issue.id, taskId: task.id, statusFrom: previousStatus, statusTo: task.status, details: { title: task.title } });
|
|
895
|
+
}
|
|
896
|
+
if (patch.note?.trim()) tryAppendControllerWorklogEvent(repoRoot, { category: 'task', action: 'task_note_added', summary: patch.note.trim(), issueId: issue.id, taskId: task.id });
|
|
897
|
+
if (patch.runId?.trim()) tryAppendControllerWorklogEvent(repoRoot, { category: 'run', action: 'run_linked_to_task', summary: `Linked ${patch.runId.trim()} to ${task.id}.`, issueId: issue.id, taskId: task.id, runId: patch.runId.trim() });
|
|
898
|
+
if (patch.github) tryAppendControllerWorklogEvent(repoRoot, { category: 'github', action: 'task_github_linked', summary: `Linked ${task.id} to GitHub.`, issueId: issue.id, taskId: task.id, details: { url: patch.github.url } });
|
|
899
|
+
if (patch.verification) tryAppendControllerWorklogEvent(repoRoot, { category: 'verification', action: task.status === 'verified' ? 'verification_passed' : 'verification_recorded', summary: `${task.id} verification recorded by ${patch.verification.reviewer}.`, issueId: issue.id, taskId: task.id, runId: patch.verification.runId, details: { checks: patch.verification.checkResults, acceptance: patch.verification.acceptanceResults } });
|
|
900
|
+
return written;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Bind a legacy Task to a Work without copying any execution contract fields.
|
|
905
|
+
* Existing fields remain readable for migration, but all new execution writes
|
|
906
|
+
* must go through Work and may only project back with `work_projection`.
|
|
907
|
+
*/
|
|
908
|
+
export function bindTaskToWork(
|
|
909
|
+
repoRoot: string,
|
|
910
|
+
issueIdValue: string,
|
|
911
|
+
taskId: string,
|
|
912
|
+
workId: string,
|
|
913
|
+
repoId?: string,
|
|
914
|
+
): ControllerIssue {
|
|
915
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
916
|
+
const normalizedWorkId = workId.trim();
|
|
917
|
+
if (!normalizedWorkId) throw new Error('TASK_WORK_ID_REQUIRED');
|
|
918
|
+
const normalizedRepoId = repoId?.trim();
|
|
919
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
920
|
+
const task = issue.tasks.find((entry) => entry.id === taskId);
|
|
921
|
+
if (!task) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
922
|
+
if (task.workId && task.workId !== normalizedWorkId) {
|
|
923
|
+
throw new Error(`TASK_WORK_REBIND_FORBIDDEN: ${task.workId} -> ${normalizedWorkId}`);
|
|
924
|
+
}
|
|
925
|
+
if (normalizedRepoId && task.repoId && task.repoId !== normalizedRepoId) {
|
|
926
|
+
throw new Error(`TASK_WORK_REPO_REBIND_FORBIDDEN: ${task.repoId} -> ${normalizedRepoId}`);
|
|
927
|
+
}
|
|
928
|
+
if (task.workId === normalizedWorkId && (!normalizedRepoId || task.repoId === normalizedRepoId)) return issue;
|
|
929
|
+
task.workId = normalizedWorkId;
|
|
930
|
+
if (normalizedRepoId) task.repoId = normalizedRepoId;
|
|
931
|
+
task.updatedAt = new Date().toISOString();
|
|
932
|
+
issue.updatedAt = task.updatedAt;
|
|
933
|
+
const written = writeIssue(repoRoot, issue);
|
|
934
|
+
tryAppendControllerWorklogEvent(repoRoot, {
|
|
935
|
+
category: 'task',
|
|
936
|
+
action: 'task_bound_to_work',
|
|
937
|
+
summary: `Bound ${task.id} to Work ${normalizedWorkId}; Work owns execution fields.`,
|
|
938
|
+
issueId: issue.id,
|
|
939
|
+
taskId: task.id,
|
|
940
|
+
details: { workId: normalizedWorkId },
|
|
941
|
+
});
|
|
942
|
+
return written;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Pre-cutover compatibility writer retained only for an unmigrated repository.
|
|
947
|
+
* Once the SQLite migration marker exists this function fails before reading or
|
|
948
|
+
* mutating legacy Issue/Task files; Work remains the sole execution writer.
|
|
949
|
+
*/
|
|
950
|
+
export function projectTaskFromWork(
|
|
951
|
+
repoRoot: string,
|
|
952
|
+
issueIdValue: string,
|
|
953
|
+
taskId: string,
|
|
954
|
+
work: WorkContract,
|
|
955
|
+
input: { verification?: TaskVerification; note?: string } = {},
|
|
956
|
+
): ControllerIssue {
|
|
957
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
958
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
959
|
+
const task = issue.tasks.find((entry) => entry.id === taskId);
|
|
960
|
+
if (!task) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
961
|
+
if (!task.workId || task.workId !== work.workId) {
|
|
962
|
+
throw new Error(`TASK_WORK_PROJECTION_IDENTITY_MISMATCH: ${task.workId ?? 'unbound'} != ${work.workId}`);
|
|
963
|
+
}
|
|
964
|
+
if (task.repoId && task.repoId !== work.repoId) {
|
|
965
|
+
throw new Error(`TASK_WORK_PROJECTION_REPO_MISMATCH: ${task.repoId} != ${work.repoId}`);
|
|
966
|
+
}
|
|
967
|
+
if (work.issueId && work.issueId !== issue.id) throw new Error(`TASK_WORK_PROJECTION_ISSUE_MISMATCH: ${work.issueId} != ${issue.id}`);
|
|
968
|
+
if (work.taskId && work.taskId !== task.id) throw new Error(`TASK_WORK_PROJECTION_TASK_MISMATCH: ${work.taskId} != ${task.id}`);
|
|
969
|
+
if (input.verification?.completionReceipt && input.verification.completionReceipt.receiptId !== work.completionReceipt?.receiptId) {
|
|
970
|
+
throw new Error('TASK_WORK_PROJECTION_RECEIPT_MISMATCH');
|
|
971
|
+
}
|
|
972
|
+
const projected = projectControllerTaskFromWork(task, work);
|
|
973
|
+
if (projected.status === 'done' && !work.completionReceipt) {
|
|
974
|
+
throw new Error('TASK_WORK_PROJECTION_COMPLETION_RECEIPT_REQUIRED');
|
|
975
|
+
}
|
|
976
|
+
task.repoId = work.repoId;
|
|
977
|
+
task.objective = projected.objective;
|
|
978
|
+
task.status = projected.status;
|
|
979
|
+
task.allowedPaths = projected.allowedPaths;
|
|
980
|
+
task.forbiddenPaths = projected.forbiddenPaths;
|
|
981
|
+
task.checks = projected.checks;
|
|
982
|
+
task.acceptanceCriteria = projected.acceptanceCriteria;
|
|
983
|
+
task.risk = projected.risk;
|
|
984
|
+
task.verification = input.verification ?? projected.verification;
|
|
985
|
+
if (input.note?.trim()) task.notes.push(input.note.trim());
|
|
986
|
+
task.updatedAt = new Date().toISOString();
|
|
987
|
+
issue.updatedAt = task.updatedAt;
|
|
988
|
+
refreshReadiness(repoRoot, issue);
|
|
989
|
+
const written = writeIssue(repoRoot, issue);
|
|
990
|
+
tryAppendControllerWorklogEvent(repoRoot, {
|
|
991
|
+
category: 'task',
|
|
992
|
+
action: 'task_projected_from_work',
|
|
993
|
+
summary: `Projected ${task.id} from Work ${work.workId} (${work.phase}/${work.status}).`,
|
|
994
|
+
issueId: issue.id,
|
|
995
|
+
taskId: task.id,
|
|
996
|
+
statusTo: task.status,
|
|
997
|
+
details: { workId: work.workId, phase: work.phase, workStatus: work.status, receiptId: work.completionReceipt?.receiptId },
|
|
998
|
+
});
|
|
999
|
+
return written;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function finding(code: string, level: 'blocker' | 'warning', message: string, taskId?: string): IssueReadinessFinding {
|
|
1003
|
+
return { code, level, message, taskId };
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export function inspectIssueReadiness(repoRoot: string, issueIdValue: string): IssueReadiness {
|
|
1007
|
+
if (legacyIssueAuthorityRetired(repoRoot)) {
|
|
1008
|
+
throw new Error('LEGACY_ISSUE_READINESS_RETIRED: use Requirement Board, Plan and Work diagnostics from controller-home SQLite.');
|
|
1009
|
+
}
|
|
1010
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
1011
|
+
const lifecycle = resolveIssueLifecycleStatus(issue);
|
|
1012
|
+
const states = resolveIssueTaskStates(issue, readIssueRunEvidence(repoRoot, issue));
|
|
1013
|
+
const issueFindings: IssueReadinessFinding[] = [];
|
|
1014
|
+
if (lifecycle !== 'active') issueFindings.push(finding('ISSUE_INACTIVE', 'blocker', `Issue lifecycle is ${lifecycle}.`));
|
|
1015
|
+
if (!issue.summary.trim()) issueFindings.push(finding('ISSUE_SUMMARY_MISSING', 'warning', 'Issue summary is absent; Task execution remains allowed.'));
|
|
1016
|
+
if (issue.goals.length === 0) issueFindings.push(finding('ISSUE_GOALS_MISSING', 'warning', 'Issue has no explicit goals.'));
|
|
1017
|
+
if (issue.acceptanceCriteria.length === 0) issueFindings.push(finding('ISSUE_ACCEPTANCE_MISSING', 'warning', 'Issue-level acceptance criteria are absent; Task-local evidence is authoritative.'));
|
|
1018
|
+
try { validateTaskGraph(issue.tasks); } catch (error) {
|
|
1019
|
+
issueFindings.push(finding(
|
|
1020
|
+
'TASK_GRAPH_INVALID',
|
|
1021
|
+
'warning',
|
|
1022
|
+
`${error instanceof Error ? error.message : String(error)} Task-local dependency findings remain authoritative for launch.`,
|
|
1023
|
+
));
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const activeRuns = readActiveRunEvidence(repoRoot);
|
|
1027
|
+
const taskReadiness = issue.tasks
|
|
1028
|
+
.filter((task) => {
|
|
1029
|
+
const state = states.get(task.id)!;
|
|
1030
|
+
return !state.terminal && !state.inactive;
|
|
1031
|
+
})
|
|
1032
|
+
.map((task) => buildTaskReadiness(repoRoot, issue, task, states, { activeRuns }));
|
|
1033
|
+
const readyTasks = taskReadiness.filter((entry) => entry.ready);
|
|
1034
|
+
const queueableTasks = taskReadiness.filter((entry) => entry.queueable);
|
|
1035
|
+
const agents: Record<ControllerAgent, number> = { codex: 0, claude: 0, 'github-copilot': 0 };
|
|
1036
|
+
for (const entry of queueableTasks) {
|
|
1037
|
+
const task = issue.tasks.find((candidate) => candidate.id === entry.taskId)!;
|
|
1038
|
+
if (task.recommendedAgent) agents[task.recommendedAgent] += 1;
|
|
1039
|
+
}
|
|
1040
|
+
const taskBlockers = taskReadiness.flatMap((entry) => entry.blockers);
|
|
1041
|
+
const taskWarnings = taskReadiness.flatMap((entry) => entry.warnings);
|
|
1042
|
+
const issueBlockers = issueFindings.filter((entry) => entry.level === 'blocker');
|
|
1043
|
+
// Global blockers are reserved for Issue-wide conditions. Task blockers remain local.
|
|
1044
|
+
const blockers = issueBlockers;
|
|
1045
|
+
const warnings = [...issueFindings.filter((entry) => entry.level === 'warning'), ...taskWarnings];
|
|
1046
|
+
const score = taskReadiness.length === 0
|
|
1047
|
+
? Math.max(0, 100 - issueBlockers.length * 30 - warnings.length * 3)
|
|
1048
|
+
: Math.round(taskReadiness.reduce((sum, entry) => sum + entry.score, 0) / taskReadiness.length);
|
|
1049
|
+
return {
|
|
1050
|
+
issueId: issue.id,
|
|
1051
|
+
score,
|
|
1052
|
+
// Issue readiness is an aggregate view only; Task-local readiness remains authoritative.
|
|
1053
|
+
ready: issueBlockers.length === 0 && readyTasks.length > 0,
|
|
1054
|
+
queueable: issueBlockers.length === 0 && queueableTasks.length > 0,
|
|
1055
|
+
blockers,
|
|
1056
|
+
taskBlockers,
|
|
1057
|
+
warnings,
|
|
1058
|
+
readyTaskIds: readyTasks.map((entry) => entry.taskId),
|
|
1059
|
+
queueableTaskIds: queueableTasks.map((entry) => entry.taskId),
|
|
1060
|
+
approvalPendingTaskIds: queueableTasks.filter((entry) => !entry.approvalSatisfied).map((entry) => entry.taskId),
|
|
1061
|
+
blockedTaskIds: taskReadiness.filter((entry) => !entry.queueable).map((entry) => entry.taskId),
|
|
1062
|
+
taskReadiness,
|
|
1063
|
+
suggestedMaxParallel: Math.max(1, Math.min(3, queueableTasks.length || 1)),
|
|
1064
|
+
agents,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
export function removeEphemeralIssue(repoRoot: string, issueIdValue: string): boolean {
|
|
1068
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
1069
|
+
if (!issue.ephemeral) throw new Error(`refusing to remove durable Issue ${issueIdValue}`);
|
|
1070
|
+
rmSync(issuePath(repoRoot, issue), { force: true });
|
|
1071
|
+
rmSync(markdownPath(repoRoot, issue), { force: true });
|
|
1072
|
+
tryAppendControllerWorklogEvent(repoRoot, {
|
|
1073
|
+
category: 'issue',
|
|
1074
|
+
action: 'ephemeral_issue_cleaned',
|
|
1075
|
+
summary: `Cleaned ephemeral Issue ${issue.id}.`,
|
|
1076
|
+
issueId: issue.id,
|
|
1077
|
+
});
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export function archiveIssue(repoRoot: string, issueIdValue: string): ControllerIssue {
|
|
1082
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
1083
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
1084
|
+
if (!['done', 'cancelled'].includes(issue.status)) throw new Error(`only done or cancelled Issues can be archived (current: ${issue.status})`);
|
|
1085
|
+
if (issue.archivedAt) return issue;
|
|
1086
|
+
issue.archivedAt = new Date().toISOString();
|
|
1087
|
+
issue.updatedAt = issue.archivedAt;
|
|
1088
|
+
const written = writeIssue(repoRoot, issue);
|
|
1089
|
+
const projectState = loadControllerProjectState(repoRoot);
|
|
1090
|
+
if (projectState.currentIssueId === issue.id) saveControllerProjectState(repoRoot, { currentIssueId: '' }, 'issue-archive');
|
|
1091
|
+
tryAppendControllerWorklogEvent(repoRoot, { category: 'issue', action: 'issue_archived', summary: `Archived ${issue.id}: ${issue.title}`, issueId: issue.id, statusFrom: issue.status, statusTo: issue.status });
|
|
1092
|
+
return written;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
export function restoreIssue(repoRoot: string, issueIdValue: string): ControllerIssue {
|
|
1096
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
1097
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
1098
|
+
if (!issue.archivedAt) return issue;
|
|
1099
|
+
const archivedAt = issue.archivedAt;
|
|
1100
|
+
delete issue.archivedAt;
|
|
1101
|
+
issue.updatedAt = new Date().toISOString();
|
|
1102
|
+
const written = writeIssue(repoRoot, issue);
|
|
1103
|
+
tryAppendControllerWorklogEvent(repoRoot, { category: 'issue', action: 'issue_restored', summary: `Restored ${issue.id}: ${issue.title}`, issueId: issue.id, details: { archivedAt } });
|
|
1104
|
+
return written;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function gitText(repoRoot: string, args: string[]): string {
|
|
1108
|
+
const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf-8' });
|
|
1109
|
+
if (result.status !== 0 || result.error) return '';
|
|
1110
|
+
return typeof result.stdout === 'string' ? result.stdout.trim() : '';
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function taskHasActiveRun(repoRoot: string, task: ControllerTask): boolean {
|
|
1114
|
+
return readTaskRunEvidence(repoRoot, task).some((run) => ['queued', 'starting', 'running'].includes(run.status));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function statusPath(line: string): string {
|
|
1118
|
+
const raw = line.slice(3).trim();
|
|
1119
|
+
const renamed = raw.includes(' -> ') ? raw.slice(raw.lastIndexOf(' -> ') + 4) : raw;
|
|
1120
|
+
return renamed.replace(/^"|"$/g, '');
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function unrelatedWorkspaceChanges(repoRoot: string, issue: ControllerIssue): string[] | undefined {
|
|
1124
|
+
const result = spawnSync('git', ['status', '--porcelain=v1', '--untracked-files=all'], {
|
|
1125
|
+
cwd: repoRoot,
|
|
1126
|
+
encoding: 'utf-8',
|
|
1127
|
+
});
|
|
1128
|
+
if (result.status !== 0 || result.error || typeof result.stdout !== 'string') return undefined;
|
|
1129
|
+
const allowedPrefix = `${issueRoot(issue)}/${issue.id}-`;
|
|
1130
|
+
return result.stdout
|
|
1131
|
+
.split(/\r?\n/)
|
|
1132
|
+
.filter(Boolean)
|
|
1133
|
+
.map(statusPath)
|
|
1134
|
+
.filter((path) => !path.startsWith('.ai/harness/'))
|
|
1135
|
+
.filter((path) => !path.startsWith(allowedPrefix));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function historicalTaskCompletionReceipt(
|
|
1139
|
+
repoRoot: string,
|
|
1140
|
+
issue: ControllerIssue,
|
|
1141
|
+
task: ControllerTask,
|
|
1142
|
+
verification: TaskVerification | undefined = task.verification,
|
|
1143
|
+
): CompletionReceipt | undefined {
|
|
1144
|
+
if (!verification || taskHasActiveRun(repoRoot, task)) return undefined;
|
|
1145
|
+
const policy = taskExecutionPolicy(task);
|
|
1146
|
+
const outcome = verificationEvidencePassed(task, verification, policy);
|
|
1147
|
+
if (!outcome.ok) return undefined;
|
|
1148
|
+
const currentBranch = gitText(repoRoot, ['branch', '--show-current']);
|
|
1149
|
+
const targetBranch = verification.integrationEvidence?.targetBranch ?? resolveCompletionTargetBranch(repoRoot);
|
|
1150
|
+
if (!currentBranch || currentBranch !== targetBranch) return undefined;
|
|
1151
|
+
const targetRevision = verification.integratedRevision ?? verification.integrationEvidence?.targetRevision;
|
|
1152
|
+
if (!targetRevision) return undefined;
|
|
1153
|
+
const reachable = spawnSync(
|
|
1154
|
+
'git',
|
|
1155
|
+
['merge-base', '--is-ancestor', targetRevision, targetBranch],
|
|
1156
|
+
{ cwd: repoRoot, encoding: 'utf-8' },
|
|
1157
|
+
);
|
|
1158
|
+
if (reachable.status !== 0 || reachable.error) return undefined;
|
|
1159
|
+
const unrelatedChanges = unrelatedWorkspaceChanges(repoRoot, issue);
|
|
1160
|
+
if (!unrelatedChanges || unrelatedChanges.length > 0) return undefined;
|
|
1161
|
+
const recordedAt = new Date().toISOString();
|
|
1162
|
+
return {
|
|
1163
|
+
schemaVersion: 1,
|
|
1164
|
+
receiptId: `REC-reconciled-${createHash('sha256').update(`${issue.id}\0${task.id}\0${targetRevision}`).digest('hex').slice(0, 16)}`,
|
|
1165
|
+
source: verification.runId ? 'workspace_run' : 'remote_no_change_execution',
|
|
1166
|
+
issueId: issue.id,
|
|
1167
|
+
taskId: task.id,
|
|
1168
|
+
runId: verification.runId,
|
|
1169
|
+
editSessionId: verification.integrationEvidence?.editSessionId,
|
|
1170
|
+
targetBranch,
|
|
1171
|
+
targetRevision,
|
|
1172
|
+
sourceRevision: verification.integrationEvidence?.sourceRevision ?? targetRevision,
|
|
1173
|
+
baseRevision: verification.integrationEvidence?.baseRevision,
|
|
1174
|
+
changedPaths: [],
|
|
1175
|
+
delivery: {
|
|
1176
|
+
kind: verification.integrationEvidence?.kind ?? 'commit',
|
|
1177
|
+
status: 'integrated',
|
|
1178
|
+
strategy: verification.integrationEvidence?.strategy === 'no_change' ? 'no_change'
|
|
1179
|
+
: verification.integrationEvidence?.strategy === 'already_integrated' ? 'already_integrated'
|
|
1180
|
+
: verification.runId ? 'already_integrated' : 'remote',
|
|
1181
|
+
reachable: true,
|
|
1182
|
+
recordedAt,
|
|
1183
|
+
},
|
|
1184
|
+
cleanup: {
|
|
1185
|
+
status: 'complete',
|
|
1186
|
+
warnings: [],
|
|
1187
|
+
blockers: [],
|
|
1188
|
+
recordedAt,
|
|
1189
|
+
},
|
|
1190
|
+
verifiedAt: verification.verifiedAt,
|
|
1191
|
+
recordedAt,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
export function acceptVerifiedTask(repoRoot: string, issueIdValue: string, taskId: string, note = 'Accepted after controller verification.'): ControllerIssue {
|
|
1196
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
1197
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
1198
|
+
const task = issue.tasks.find((entry) => entry.id === taskId);
|
|
1199
|
+
if (!task) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
1200
|
+
if (task.workId) throw new Error(`TASK_COMPLETION_WORK_OWNED: ${issueIdValue}/${taskId} is projected from Work ${task.workId}`);
|
|
1201
|
+
if (task.status === 'done') return issue;
|
|
1202
|
+
if (task.status !== 'verified' || !task.verification) throw new Error(`task must have passed required verification before acceptance (current: ${task.status})`);
|
|
1203
|
+
const verification = task.verification;
|
|
1204
|
+
if (!completionEvidenceComplete(verification, {
|
|
1205
|
+
issueId: issue.id,
|
|
1206
|
+
taskId: task.id,
|
|
1207
|
+
runId: verification.runId,
|
|
1208
|
+
targetBranch: resolveCompletionTargetBranch(repoRoot),
|
|
1209
|
+
})) {
|
|
1210
|
+
throw new Error('task completion requires a complete delivery receipt bound to this Task and integration target, or compatible legacy Run evidence');
|
|
1211
|
+
}
|
|
1212
|
+
const integration = verification.completionReceipt
|
|
1213
|
+
? {
|
|
1214
|
+
targetBranch: verification.completionReceipt.targetBranch,
|
|
1215
|
+
targetRevision: verification.completionReceipt.targetRevision,
|
|
1216
|
+
}
|
|
1217
|
+
: verification.integrationEvidence;
|
|
1218
|
+
if (!integration) throw new Error('task completion is missing target revision evidence');
|
|
1219
|
+
const reachable = spawnSync(
|
|
1220
|
+
'git',
|
|
1221
|
+
['merge-base', '--is-ancestor', integration.targetRevision, integration.targetBranch],
|
|
1222
|
+
{ cwd: repoRoot, encoding: 'utf-8' },
|
|
1223
|
+
);
|
|
1224
|
+
if (reachable.status !== 0 || reachable.error) {
|
|
1225
|
+
throw new Error(`integrated revision ${integration.targetRevision} is not reachable from ${integration.targetBranch}`);
|
|
1226
|
+
}
|
|
1227
|
+
return updateTask(repoRoot, issueIdValue, taskId, { status: 'done', note });
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
export function recordTaskVerification(
|
|
1231
|
+
repoRoot: string,
|
|
1232
|
+
issueIdValue: string,
|
|
1233
|
+
taskId: string,
|
|
1234
|
+
verification: TaskVerification,
|
|
1235
|
+
options: { completingRunId?: string } = {},
|
|
1236
|
+
): ControllerIssue {
|
|
1237
|
+
assertLegacyIssueWritesAllowed(repoRoot, { id: issueIdValue });
|
|
1238
|
+
const issue = getIssue(repoRoot, issueIdValue);
|
|
1239
|
+
const task = issue.tasks.find((entry) => entry.id === taskId);
|
|
1240
|
+
if (!task) throw new Error(`task not found: ${issueIdValue}/${taskId}`);
|
|
1241
|
+
if (task.workId) throw new Error(`TASK_VERIFICATION_WORK_OWNED: ${issueIdValue}/${taskId} is projected from Work ${task.workId}`);
|
|
1242
|
+
const taskState = effectiveTaskState(repoRoot, issue, task);
|
|
1243
|
+
const doneEvidenceBackfill = task.status === 'done' && taskState.reason === 'completion_evidence_missing';
|
|
1244
|
+
if ((taskState.terminal || taskState.inactive) && !doneEvidenceBackfill) {
|
|
1245
|
+
throw new Error(`task cannot be verified from effective status ${taskState.effectiveStatus}`);
|
|
1246
|
+
}
|
|
1247
|
+
const completingRunOwnsVerification = Boolean(
|
|
1248
|
+
options.completingRunId && verification.runId === options.completingRunId,
|
|
1249
|
+
);
|
|
1250
|
+
const blockingRunIds = taskState.activeRunIds.filter((runId) =>
|
|
1251
|
+
!(completingRunOwnsVerification && runId === options.completingRunId),
|
|
1252
|
+
);
|
|
1253
|
+
if (blockingRunIds.length > 0) throw new Error(`task cannot be verified while Run(s) are active: ${blockingRunIds.join(', ')}`);
|
|
1254
|
+
if (!doneEvidenceBackfill && !['planned', 'ready', 'launch_blocked', 'review', 'verifying', 'ready_to_integrate', 'integrating', 'integration_blocked', 'integrated', 'cleanup_pending', 'cleanup_blocked', 'changes_requested', 'verified'].includes(task.status)) {
|
|
1255
|
+
throw new Error(`task cannot be verified from status ${task.status}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (!verification.reviewer.trim()) throw new Error('verification reviewer is required');
|
|
1258
|
+
if (verification.checkResults.some((entry) => !entry.checkId.trim())) throw new Error('verification check IDs cannot be empty');
|
|
1259
|
+
if ((verification.commandEvidence ?? []).some((entry) => entry.command.length === 0 || entry.command.some((part) => !part.trim()))) {
|
|
1260
|
+
throw new Error('reported command evidence must contain a non-empty argv');
|
|
1261
|
+
}
|
|
1262
|
+
const policy = taskExecutionPolicy(task);
|
|
1263
|
+
if (policy.requiresScopedPaths && task.allowedPaths.length === 0) {
|
|
1264
|
+
throw new Error(`${policy.executionClass} cannot be verified without an explicit allowed path scope`);
|
|
1265
|
+
}
|
|
1266
|
+
const normalizedDeclaredChecks = normalizeCheckIds(task.checks, listControllerChecks(repoRoot));
|
|
1267
|
+
const verificationTask = { ...task, checks: normalizedDeclaredChecks.validCheckIds };
|
|
1268
|
+
const outcome = verificationEvidencePassed(verificationTask, verification, policy);
|
|
1269
|
+
const successful = outcome.status === 'passed';
|
|
1270
|
+
if (doneEvidenceBackfill) {
|
|
1271
|
+
if (!successful) {
|
|
1272
|
+
throw new Error(`done Task completion-evidence backfill requires passing verification: ${outcome.reasons.join(' ')}`);
|
|
1273
|
+
}
|
|
1274
|
+
const completionReceipt = historicalTaskCompletionReceipt(repoRoot, issue, task, verification);
|
|
1275
|
+
if (!completionReceipt) {
|
|
1276
|
+
throw new Error('done Task completion-evidence backfill requires a reachable integrated revision, target branch checkout, no active Run, and no unrelated workspace changes');
|
|
1277
|
+
}
|
|
1278
|
+
verification.autoCompleted = true;
|
|
1279
|
+
verification.completionReceipt = completionReceipt;
|
|
1280
|
+
return updateTask(repoRoot, issueIdValue, taskId, {
|
|
1281
|
+
verification,
|
|
1282
|
+
note: `Reconciled missing completion evidence with receipt ${completionReceipt.receiptId}; declared Task status remains done.`,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
const verificationStatus = successful
|
|
1286
|
+
? 'verified'
|
|
1287
|
+
: outcome.status === 'failed'
|
|
1288
|
+
? 'changes_requested'
|
|
1289
|
+
: 'verifying';
|
|
1290
|
+
// Run-backed verification must stop at verified. Integration and cleanup
|
|
1291
|
+
// evidence are finalized by the completion orchestrator before Task done.
|
|
1292
|
+
const autoComplete = false;
|
|
1293
|
+
const invalidCheckNote = normalizedDeclaredChecks.invalidCheckIds.length > 0
|
|
1294
|
+
? ` Ignored unregistered check id(s): ${normalizedDeclaredChecks.invalidCheckIds.join(', ')}. Invalid check ids are verification infrastructure metadata, not actual check failures.`
|
|
1295
|
+
: '';
|
|
1296
|
+
verification.autoCompleted = autoComplete;
|
|
1297
|
+
return updateTask(repoRoot, issueIdValue, taskId, {
|
|
1298
|
+
status: verificationStatus,
|
|
1299
|
+
verification,
|
|
1300
|
+
note: successful
|
|
1301
|
+
? `Required verification evidence passed; a completion receipt remains required before completion.${invalidCheckNote}`
|
|
1302
|
+
: outcome.status === 'failed'
|
|
1303
|
+
? `Verification evidence failed: ${outcome.reasons.join(' ')}${invalidCheckNote}`
|
|
1304
|
+
: `Verification evidence is incomplete: ${outcome.reasons.join(' ')}${invalidCheckNote}`,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
export function projectBoard(repoRoot: string): {
|
|
1309
|
+
issues: Array<Record<string, unknown>>;
|
|
1310
|
+
counts: Record<string, number>;
|
|
1311
|
+
declaredCounts: Record<string, number>;
|
|
1312
|
+
archivedCounts: Record<string, number>;
|
|
1313
|
+
readyTasks: Array<Record<string, string>>;
|
|
1314
|
+
queueableTasks: Array<Record<string, string>>;
|
|
1315
|
+
currentIssueId?: string;
|
|
1316
|
+
archivedIssueCount: number;
|
|
1317
|
+
} {
|
|
1318
|
+
assertLegacyProjectBoardAvailable(repoRoot);
|
|
1319
|
+
const issues = listIssues(repoRoot);
|
|
1320
|
+
const projectState = loadControllerProjectState(repoRoot);
|
|
1321
|
+
const currentIssueId = projectState.currentIssueId
|
|
1322
|
+
&& issues.some((issue) => issue.id === projectState.currentIssueId && !issue.archivedAt && !['done', 'cancelled'].includes(issue.status))
|
|
1323
|
+
? projectState.currentIssueId
|
|
1324
|
+
: undefined;
|
|
1325
|
+
const counts: Record<string, number> = {};
|
|
1326
|
+
const declaredCounts: Record<string, number> = {};
|
|
1327
|
+
const archivedCounts: Record<string, number> = {};
|
|
1328
|
+
const readyTasks: Array<Record<string, string>> = [];
|
|
1329
|
+
const queueableTasks: Array<Record<string, string>> = [];
|
|
1330
|
+
const statesByIssue = new Map<string, Map<string, ReturnType<typeof resolveEffectiveTaskState>>>();
|
|
1331
|
+
const runsByIssue = new Map<string, Map<string, ReturnType<typeof readTaskRunEvidence>>>();
|
|
1332
|
+
const readinessByTask = new Map<string, TaskReadiness>();
|
|
1333
|
+
const activeRuns = readActiveRunEvidence(repoRoot);
|
|
1334
|
+
for (const issue of issues) {
|
|
1335
|
+
const runEvidence = readIssueRunEvidence(repoRoot, issue);
|
|
1336
|
+
const states = resolveIssueTaskStates(issue, runEvidence);
|
|
1337
|
+
statesByIssue.set(issue.id, states);
|
|
1338
|
+
runsByIssue.set(issue.id, runEvidence);
|
|
1339
|
+
const targetCounts = issue.archivedAt ? archivedCounts : counts;
|
|
1340
|
+
for (const task of issue.tasks) {
|
|
1341
|
+
const state = states.get(task.id)!;
|
|
1342
|
+
targetCounts[state.effectiveStatus] = (targetCounts[state.effectiveStatus] ?? 0) + 1;
|
|
1343
|
+
declaredCounts[state.declaredStatus] = (declaredCounts[state.declaredStatus] ?? 0) + 1;
|
|
1344
|
+
const readiness = buildTaskReadiness(repoRoot, issue, task, states, { activeRuns });
|
|
1345
|
+
readinessByTask.set(`${issue.id}/${task.id}`, readiness);
|
|
1346
|
+
const boardTask = {
|
|
1347
|
+
issueId: issue.id,
|
|
1348
|
+
taskId: task.id,
|
|
1349
|
+
title: task.title,
|
|
1350
|
+
agent: task.recommendedAgent ?? "runtime-selected",
|
|
1351
|
+
effectiveStatus: state.effectiveStatus,
|
|
1352
|
+
executionClass: readiness.executionClass,
|
|
1353
|
+
approval: readiness.approval,
|
|
1354
|
+
};
|
|
1355
|
+
if (readiness.queueable) queueableTasks.push(boardTask);
|
|
1356
|
+
if (readiness.ready) readyTasks.push(boardTask);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
issues: issues.map((issue) => {
|
|
1361
|
+
const states = statesByIssue.get(issue.id)!;
|
|
1362
|
+
const runEvidence = runsByIssue.get(issue.id)!;
|
|
1363
|
+
return {
|
|
1364
|
+
id: issue.id,
|
|
1365
|
+
title: issue.title,
|
|
1366
|
+
kind: issue.kind,
|
|
1367
|
+
status: issue.status,
|
|
1368
|
+
lifecycleStatus: resolveIssueLifecycleStatus(issue),
|
|
1369
|
+
github: issue.github,
|
|
1370
|
+
archivedAt: issue.archivedAt,
|
|
1371
|
+
updatedAt: issue.updatedAt,
|
|
1372
|
+
isCurrent: issue.id === currentIssueId,
|
|
1373
|
+
tasks: issue.tasks.map((task) => {
|
|
1374
|
+
const state = states.get(task.id)!;
|
|
1375
|
+
const readiness = readinessByTask.get(`${issue.id}/${task.id}`)!;
|
|
1376
|
+
const latestRun = (runEvidence.get(task.id) ?? []).at(-1);
|
|
1377
|
+
return {
|
|
1378
|
+
id: task.id,
|
|
1379
|
+
title: task.title,
|
|
1380
|
+
objective: task.objective,
|
|
1381
|
+
allowedPaths: task.allowedPaths,
|
|
1382
|
+
forbiddenPaths: task.forbiddenPaths,
|
|
1383
|
+
checks: task.checks,
|
|
1384
|
+
acceptanceCriteria: task.acceptanceCriteria,
|
|
1385
|
+
risk: task.risk,
|
|
1386
|
+
status: task.status,
|
|
1387
|
+
declaredStatus: state.declaredStatus,
|
|
1388
|
+
effectiveStatus: state.effectiveStatus,
|
|
1389
|
+
statusReason: state.reason,
|
|
1390
|
+
latestRunStatus: state.latestRunStatus,
|
|
1391
|
+
latestRunClosureState: latestRun?.closureState ?? 'none',
|
|
1392
|
+
activeRunId: state.activeRunId,
|
|
1393
|
+
activeRunStatus: state.activeRunStatus,
|
|
1394
|
+
activeRunIds: state.activeRunIds,
|
|
1395
|
+
multipleActiveRuns: state.multipleActiveRuns,
|
|
1396
|
+
verificationStatus: state.verificationStatus,
|
|
1397
|
+
dispatchable: readiness.ready,
|
|
1398
|
+
queueable: readiness.queueable,
|
|
1399
|
+
readiness,
|
|
1400
|
+
retryable: state.retryable,
|
|
1401
|
+
requiresExplicitRetry: state.requiresExplicitRetry,
|
|
1402
|
+
dependsOn: task.dependsOn,
|
|
1403
|
+
dependencyState: resolveTaskDependencies(issue, task, states),
|
|
1404
|
+
supersededBy: task.supersededBy,
|
|
1405
|
+
agent: task.recommendedAgent ?? "runtime-selected",
|
|
1406
|
+
runIds: task.runIds,
|
|
1407
|
+
github: task.github,
|
|
1408
|
+
};
|
|
1409
|
+
}),
|
|
1410
|
+
};
|
|
1411
|
+
}),
|
|
1412
|
+
counts,
|
|
1413
|
+
declaredCounts,
|
|
1414
|
+
archivedCounts,
|
|
1415
|
+
// Every active Issue contributes independent Tasks. Current focus is informational only.
|
|
1416
|
+
readyTasks,
|
|
1417
|
+
queueableTasks,
|
|
1418
|
+
currentIssueId,
|
|
1419
|
+
archivedIssueCount: issues.filter((issue) => Boolean(issue.archivedAt)).length,
|
|
1420
|
+
};
|
|
1421
|
+
}
|