ai-delivery-workflow 0.2.2 → 0.4.0
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/README.md +45 -14
- package/bin/ai-delivery.mjs +181 -6
- package/bin/benchmark-autonomy.mjs +217 -0
- package/bin/release-check.mjs +382 -0
- package/docs/CLI-PARAMETER-REFERENCE.zh-CN.md +346 -0
- package/docs/CODEGRAPH-INTEGRATION.zh-CN.md +89 -0
- package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +2 -2
- package/docs/EVOLUTION-MAINTENANCE.zh-CN.md +7 -7
- package/docs/EVOLUTION-USER-GUIDE.zh-CN.md +2 -2
- package/docs/FILE-REFERENCE.zh-CN.md +120 -39
- package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +51 -0
- package/docs/MOCK-SYSTEM.zh-CN.md +141 -0
- package/docs/NPM-RELEASE-MANAGEMENT.zh-CN.md +106 -0
- package/docs/PROJECT-MANUAL.zh-CN.md +154 -95
- package/docs/PROTOTYPE-MIGRATION.zh-CN.md +164 -0
- package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +35 -17
- package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +115 -39
- package/docs/VIEWER-MAINTENANCE.zh-CN.md +101 -324
- package/docs/VIEWER-USER-GUIDE.zh-CN.md +68 -190
- package/docs/adr/0001-prototype-first-ui-governance.md +3 -0
- package/docs/adr/0002-microcks-as-default-mock-system.md +3 -0
- package/docs/adr/0003-workflow-manager-governed-control-surface.md +46 -0
- package/docs/adr/0004-operational-carbon-visual-standard.md +9 -0
- package/docs/adr/0005-prototype-management-as-routed-pages.md +7 -0
- package/docs/agents/domain.md +12 -0
- package/docs/agents/issue-tracker.md +27 -0
- package/docs/agents/triage-labels.md +11 -0
- package/docs/agents/workflow-manager-acceptance-matrix.md +93 -0
- package/docs/agents/workflow-manager-agent-interface.md +145 -0
- package/docs/agents/workflow-manager-current-state.md +205 -0
- package/lib/artifact-files.mjs +47 -0
- package/lib/codegraph.mjs +405 -0
- package/lib/delivery-state.mjs +3079 -198
- package/lib/frontend-guard.mjs +288 -0
- package/lib/fs-utils.mjs +53 -0
- package/lib/history-anchor.mjs +98 -0
- package/lib/mock-scenario-pack.mjs +687 -0
- package/lib/mock-system.mjs +897 -0
- package/lib/npm-package-result.mjs +11 -0
- package/lib/png-evidence.mjs +149 -0
- package/lib/project-bootstrap.mjs +96 -55
- package/lib/project-installer.mjs +1484 -164
- package/lib/project-migration.mjs +1193 -0
- package/lib/prototype-migration.mjs +713 -0
- package/lib/prototype-page-review.mjs +384 -0
- package/lib/prototype-review-target.mjs +334 -0
- package/lib/prototype-revision.mjs +812 -0
- package/lib/stable-value.mjs +15 -0
- package/lib/toml-hooks.mjs +69 -0
- package/lib/ui-acceptance.mjs +565 -0
- package/lib/visual-standard.mjs +232 -0
- package/lib/workflow-manager-contracts.mjs +367 -0
- package/lib/workflow-manager-facts.mjs +778 -0
- package/lib/workflow-manager-governance.mjs +514 -0
- package/lib/workflow-manager-mock.mjs +183 -0
- package/lib/workflow-manager-prototypes.mjs +1847 -0
- package/lib/workflow-manager-public/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/lib/workflow-manager-public/assets/index-Chlg6r8f.css +1 -0
- package/lib/workflow-manager-public/assets/index-E-hGB5ZD.js +13 -0
- package/lib/workflow-manager-public/fonts/OFL-1.1.txt +90 -0
- package/lib/workflow-manager-public/index.html +14 -0
- package/lib/workflow-manager-systems.mjs +519 -0
- package/lib/workflow-manager-transaction.mjs +397 -0
- package/lib/workflow-manager.mjs +745 -0
- package/package.json +8 -2
- package/skills/ai-delivery-assemble-release/agents/openai.yaml +3 -3
- package/skills/ai-delivery-assemble-release/assets/release-material-index.csv +1 -1
- package/skills/ai-delivery-assemble-release/assets/version-inclusion.csv +1 -1
- package/skills/ai-delivery-bootstrap/SKILL.md +4 -4
- package/skills/ai-delivery-bootstrap/agents/openai.yaml +3 -3
- package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +4 -2
- package/skills/ai-delivery-checkpoint-task/SKILL.md +17 -3
- package/skills/ai-delivery-checkpoint-task/agents/openai.yaml +3 -3
- package/skills/ai-delivery-checkpoint-task/assets/runtime-template/task.json +7 -1
- package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +22 -8
- package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +91 -0
- package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +1086 -51
- package/skills/ai-delivery-close-version/SKILL.md +9 -8
- package/skills/ai-delivery-close-version/agents/openai.yaml +3 -3
- package/skills/ai-delivery-close-version/assets/version-lineage.yaml +1 -1
- package/skills/ai-delivery-close-version/references/version-closeout-contract.md +2 -2
- package/skills/ai-delivery-define-product/SKILL.md +8 -4
- package/skills/ai-delivery-define-product/agents/openai.yaml +4 -4
- package/skills/ai-delivery-define-product/references/product-contract.md +2 -2
- package/skills/ai-delivery-deploy-production/agents/openai.yaml +3 -3
- package/skills/ai-delivery-design-architecture/SKILL.md +9 -5
- package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +16 -16
- package/skills/ai-delivery-design-architecture/references/architecture-contract.md +1 -1
- package/skills/ai-delivery-design-experience/SKILL.md +36 -30
- package/skills/ai-delivery-design-experience/agents/openai.yaml +4 -4
- package/skills/ai-delivery-design-experience/assets/experience-template/LEGACY-COMPATIBILITY.md +16 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/design-reference-selection.yaml +32 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/experience-baseline.yaml +2 -2
- package/skills/ai-delivery-design-experience/assets/experience-template/formal-ui-confirmation.yaml +16 -3
- package/skills/ai-delivery-design-experience/assets/experience-template/frontend-change-scope.yaml +30 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/interaction-contract.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/mock-admin-export.yaml +8 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/mock-scenario-mappings.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/mock-scenario-pack-request.yaml +24 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +2 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-adjustment-log.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-component-bindings.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-component-candidates.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-components.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-confirmation.yaml +18 -18
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-data-contracts.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-impact-assessment.yaml +5 -5
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-implementation-bindings.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +2 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-pages.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-product-coverage.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-comments.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-decision.yaml +13 -12
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-scenes.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-sessions.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-unit-pages.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-units.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-revision.yaml +88 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-routes.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +2 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-source-inspection-snapshots.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-terminals.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-candidates.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-selection.yaml +2 -3
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-traceability.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-trigger-conditions.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype.yaml +6 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-selection.yaml +17 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +6 -5
- package/skills/ai-delivery-design-experience/assets/experience-template/ui-mapping.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/visual-direction.yaml +4 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/visual-reference-manifest.yaml +23 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/visual-reference.html +163 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/visual-system-confirmation.yaml +11 -0
- package/skills/ai-delivery-design-experience/references/experience-contract.md +26 -27
- package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +42 -21
- package/skills/ai-delivery-design-tests/SKILL.md +13 -7
- package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-scope.csv +1 -1
- package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-validation.yaml +17 -16
- package/skills/ai-delivery-design-tests/references/test-design-contract.md +7 -5
- package/skills/ai-delivery-develop-iteration/SKILL.md +18 -11
- package/skills/ai-delivery-develop-iteration/agents/openai.yaml +3 -3
- package/skills/ai-delivery-develop-iteration/assets/development-template/candidate-manifest.yaml +36 -0
- package/skills/ai-delivery-develop-iteration/assets/development-template/task-prototype-traceability.csv +1 -1
- package/skills/ai-delivery-develop-iteration/references/development-contract.md +5 -3
- package/skills/ai-delivery-evolve-workflow/agents/openai.yaml +3 -3
- package/skills/ai-delivery-execute-test/SKILL.md +6 -2
- package/skills/ai-delivery-execute-test/agents/openai.yaml +4 -4
- package/skills/ai-delivery-execute-work-package/SKILL.md +10 -3
- package/skills/ai-delivery-execute-work-package/agents/openai.yaml +4 -4
- package/skills/ai-delivery-execute-work-package/references/work-package-contract.md +1 -1
- package/skills/ai-delivery-manage-git/agents/openai.yaml +3 -3
- package/skills/ai-delivery-manage-standards/agents/openai.yaml +4 -4
- package/skills/ai-delivery-orchestrate/SKILL.md +47 -29
- package/skills/ai-delivery-orchestrate/agents/openai.yaml +3 -3
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/control/migrations/.gitkeep +0 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/control/repairs/.gitkeep +0 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/project.yaml +1 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/workflow-state.yaml +2 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/frontend-guard/frontend-guard.mjs +50 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +91 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/manager.mjs +64 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/start.cmd +4 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/start.ps1 +2 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/{viewer → manager}/start.sh +1 -1
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/mock/mock.mjs +50 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/ui-acceptance/ui-acceptance.mjs +37 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +32 -6
- package/skills/ai-delivery-orchestrate/assets/project-template/gitignore.template +1 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/CONTRIBUTING.md +25 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/LICENSE +21 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/README.md +250 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/UPSTREAM.yaml +19 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airbnb/DESIGN.md +545 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airbnb/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airtable/DESIGN.md +554 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airtable/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/apple/DESIGN.md +562 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/apple/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/binance/DESIGN.md +634 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/binance/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw/DESIGN.md +544 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw-m/DESIGN.md +503 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw-m/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bugatti/DESIGN.md +454 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bugatti/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cal/DESIGN.md +542 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cal/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/claude/DESIGN.md +589 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/claude/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clay/DESIGN.md +541 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clay/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clickhouse/DESIGN.md +544 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clickhouse/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cohere/DESIGN.md +451 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cohere/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/coinbase/DESIGN.md +570 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/coinbase/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/composio/DESIGN.md +506 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/composio/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cursor/DESIGN.md +537 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cursor/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/dell-1996/DESIGN.md +632 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/dell-1996/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/elevenlabs/DESIGN.md +504 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/elevenlabs/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/expo/DESIGN.md +526 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/expo/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ferrari/DESIGN.md +531 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ferrari/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/figma/DESIGN.md +578 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/figma/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/framer/DESIGN.md +544 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/framer/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hashicorp/DESIGN.md +575 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hashicorp/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hp/DESIGN.md +670 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hp/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ibm/DESIGN.md +550 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ibm/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/intercom/DESIGN.md +546 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/intercom/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/kraken/DESIGN.md +125 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/kraken/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lamborghini/DESIGN.md +288 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lamborghini/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/linear.app/DESIGN.md +548 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/linear.app/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lovable/DESIGN.md +298 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lovable/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mastercard/DESIGN.md +365 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mastercard/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/meta/DESIGN.md +683 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/meta/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/minimax/DESIGN.md +746 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/minimax/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mintlify/DESIGN.md +852 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mintlify/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/miro/DESIGN.md +825 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/miro/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mistral.ai/DESIGN.md +773 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mistral.ai/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mongodb/DESIGN.md +767 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mongodb/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nike/DESIGN.md +575 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nike/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nintendo-2001/DESIGN.md +649 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nintendo-2001/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/notion/DESIGN.md +821 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/notion/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nvidia/DESIGN.md +640 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nvidia/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ollama/DESIGN.md +539 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ollama/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/opencode.ai/DESIGN.md +521 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/opencode.ai/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/pinterest/DESIGN.md +597 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/pinterest/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/playstation/DESIGN.md +661 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/playstation/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/posthog/DESIGN.md +690 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/posthog/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/raycast/DESIGN.md +669 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/raycast/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/renault/DESIGN.md +589 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/renault/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/replicate/DESIGN.md +616 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/replicate/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/resend/DESIGN.md +585 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/resend/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/revolut/DESIGN.md +636 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/revolut/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/runwayml/DESIGN.md +244 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/runwayml/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sanity/DESIGN.md +357 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sanity/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sentry/DESIGN.md +551 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sentry/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/shopify/DESIGN.md +516 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/shopify/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/slack/DESIGN.md +482 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spacex/DESIGN.md +363 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spacex/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spotify/DESIGN.md +246 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spotify/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/starbucks/DESIGN.md +580 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/starbucks/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/stripe/DESIGN.md +487 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/stripe/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/supabase/DESIGN.md +462 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/supabase/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/superhuman/DESIGN.md +448 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/superhuman/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/tesla/DESIGN.md +286 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/tesla/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/theverge/DESIGN.md +339 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/theverge/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/together.ai/DESIGN.md +633 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/together.ai/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/uber/DESIGN.md +636 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/uber/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vercel/DESIGN.md +736 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vercel/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vodafone/DESIGN.md +538 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vodafone/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/voltagent/DESIGN.md +521 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/voltagent/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/warp/DESIGN.md +526 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/warp/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/webflow/DESIGN.md +588 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/webflow/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wired/DESIGN.md +497 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wired/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wise/DESIGN.md +544 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wise/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/x.ai/DESIGN.md +465 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/x.ai/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/zapier/DESIGN.md +537 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/zapier/README.md +5 -0
- package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +65 -20
- package/skills/ai-delivery-orchestrate/references/workflow-model.md +7 -5
- package/skills/ai-delivery-orchestrate-release/SKILL.md +2 -2
- package/skills/ai-delivery-orchestrate-release/agents/openai.yaml +4 -4
- package/skills/ai-delivery-orchestrate-release/references/release-workflow-contract.md +1 -1
- package/skills/ai-delivery-plan-iteration/SKILL.md +11 -4
- package/skills/ai-delivery-plan-iteration/agents/openai.yaml +4 -4
- package/skills/ai-delivery-plan-iteration/assets/planning-template/scope-manifest.yaml +17 -0
- package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-prototype-traceability.csv +1 -1
- package/skills/ai-delivery-plan-iteration/references/planning-contract.md +8 -2
- package/skills/ai-delivery-prepare-platform/SKILL.md +5 -1
- package/skills/ai-delivery-prepare-platform/agents/openai.yaml +4 -4
- package/skills/ai-delivery-prepare-release/agents/openai.yaml +4 -4
- package/skills/ai-delivery-review-change/SKILL.md +14 -4
- package/skills/ai-delivery-review-change/agents/openai.yaml +4 -4
- package/skills/ai-delivery-review-change/references/review-contract.md +6 -0
- package/skills/ai-delivery-triage-failure/SKILL.md +5 -1
- package/skills/ai-delivery-triage-failure/agents/openai.yaml +4 -4
- package/skills/ai-delivery-validate-artifacts/SKILL.md +6 -3
- package/skills/ai-delivery-validate-artifacts/agents/openai.yaml +4 -4
- package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +13 -13
- package/skills/ai-delivery-verify-candidate/SKILL.md +13 -6
- package/skills/ai-delivery-verify-candidate/agents/openai.yaml +3 -3
- package/skills/ai-delivery-verify-candidate/assets/verification-template/ui-acceptance-request.yaml +19 -0
- package/skills/ai-delivery-verify-candidate/assets/verification-template/ui-acceptance-results.yaml +53 -0
- package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +10 -2
- package/skills/ai-delivery-verify-production/agents/openai.yaml +3 -3
- package/skills/ai-delivery-design-experience/assets/experience-template/low-fidelity-confirmation.yaml +0 -32
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/app.js +0 -546
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/index.html +0 -199
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/styles.css +0 -529
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/server.mjs +0 -780
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.cmd +0 -4
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.ps1 +0 -2
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
6
7
|
|
|
7
8
|
const moduleRequire = createRequire(import.meta.url);
|
|
8
9
|
|
|
@@ -12,13 +13,48 @@ const VALID_STATUS = new Set([
|
|
|
12
13
|
]);
|
|
13
14
|
const FINISH_STATUS = new Set(["completed", "blocked", "interrupted", "failed", "abandoned"]);
|
|
14
15
|
const CLOSED_STATUS = new Set(["completed", "failed", "abandoned"]);
|
|
16
|
+
const RECOVERY_COMPLETE_STATUS = new Set(["completed", "abandoned"]);
|
|
15
17
|
const RESUMABLE_STATUS = new Set([
|
|
16
18
|
"preparing", "running", "checkpointed", "pending-review", "blocked", "interrupted",
|
|
17
19
|
]);
|
|
18
20
|
const VALID_NEXT_ACTION_OWNER = new Set(["agent", "user", "external"]);
|
|
21
|
+
const VALID_PHASE = new Set([
|
|
22
|
+
"planning", "implementation", "review", "triage", "repair", "test", "verification", "handoff",
|
|
23
|
+
]);
|
|
24
|
+
const VALID_PHASE_STATUS = new Set([
|
|
25
|
+
"active", "completed", "failed", "blocked", "interrupted", "abandoned",
|
|
26
|
+
]);
|
|
27
|
+
const VALID_FINDING_SEVERITY = new Set(["info", "warning", "blocking"]);
|
|
28
|
+
const VALID_FINDING_STATUS = new Set(["open", "resolved", "accepted"]);
|
|
29
|
+
const VALID_COLLECTION_DECISION = new Set(["pass", "fail", "blocked"]);
|
|
30
|
+
const VALID_TIME_CATEGORY = new Set([
|
|
31
|
+
"implementation", "test", "review", "workflow", "user-wait", "external-wait", "rework",
|
|
32
|
+
]);
|
|
33
|
+
const TASK_STATUS_PROJECTION = new Map([
|
|
34
|
+
["completed", { attempt: "completed", phase: "completed" }],
|
|
35
|
+
["failed", { attempt: "failed", phase: "failed" }],
|
|
36
|
+
["abandoned", { attempt: "abandoned", phase: "abandoned" }],
|
|
37
|
+
["blocked", { attempt: "paused", phase: "blocked" }],
|
|
38
|
+
["interrupted", { attempt: "paused", phase: "interrupted" }],
|
|
39
|
+
]);
|
|
19
40
|
const REPEATABLE_ARGS = new Set([
|
|
20
41
|
"scope", "input-artifact", "standard", "planned-output", "actual-output",
|
|
21
|
-
"completed-step", "blocker", "evidence", "depends-on",
|
|
42
|
+
"completed-step", "blocker", "evidence", "depends-on", "acceptance", "finding-json",
|
|
43
|
+
"change-set-json",
|
|
44
|
+
]);
|
|
45
|
+
const LEGACY_DEVELOPMENT_NODE_IDS = new Set([
|
|
46
|
+
"01-product-discovery",
|
|
47
|
+
"02-product-definition",
|
|
48
|
+
"03-architecture-initial",
|
|
49
|
+
"04-experience-design",
|
|
50
|
+
"05-architecture-reconcile",
|
|
51
|
+
"06-platform-preparation",
|
|
52
|
+
"07-iteration-planning",
|
|
53
|
+
"08-test-design",
|
|
54
|
+
"09-development",
|
|
55
|
+
"10-change-review",
|
|
56
|
+
"11-candidate-verification",
|
|
57
|
+
"12-version-closeout",
|
|
22
58
|
]);
|
|
23
59
|
|
|
24
60
|
function parseArgs(argv) {
|
|
@@ -47,6 +83,13 @@ function requireValue(args, key) {
|
|
|
47
83
|
return value;
|
|
48
84
|
}
|
|
49
85
|
|
|
86
|
+
function assertNotLegacyDevelopmentNode(node) {
|
|
87
|
+
if (LEGACY_DEVELOPMENT_NODE_IDS.has(node)) {
|
|
88
|
+
throw new Error(`Legacy workflow node is read-only: ${node}`);
|
|
89
|
+
}
|
|
90
|
+
return node;
|
|
91
|
+
}
|
|
92
|
+
|
|
50
93
|
function now() {
|
|
51
94
|
return new Date().toISOString();
|
|
52
95
|
}
|
|
@@ -67,6 +110,265 @@ function parseSequence(value) {
|
|
|
67
110
|
return sequence;
|
|
68
111
|
}
|
|
69
112
|
|
|
113
|
+
function parsePhase(value, fallback = null) {
|
|
114
|
+
const phase = value === undefined || value === null || value === "" ? fallback : value;
|
|
115
|
+
if (phase !== null && !VALID_PHASE.has(phase)) {
|
|
116
|
+
throw new Error(`Invalid attempt phase: ${phase}; use ${[...VALID_PHASE].join(", ")}.`);
|
|
117
|
+
}
|
|
118
|
+
return phase;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseElapsedMinutes(value) {
|
|
122
|
+
if (value === undefined || value === null || value === "") return null;
|
|
123
|
+
const minutes = Number(value);
|
|
124
|
+
if (!Number.isFinite(minutes) || minutes < 0) {
|
|
125
|
+
throw new Error(`Invalid non-negative elapsed minutes: ${value}`);
|
|
126
|
+
}
|
|
127
|
+
return minutes;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeRelativeFiles(values, label) {
|
|
131
|
+
if (!Array.isArray(values)) return [];
|
|
132
|
+
return [...new Set(values.map((value) => {
|
|
133
|
+
const normalized = String(value).trim().replaceAll("\\", "/");
|
|
134
|
+
if (!normalized || path.posix.isAbsolute(normalized) || normalized.split("/").includes("..")) {
|
|
135
|
+
throw new Error(`${label} must contain repository-relative file paths.`);
|
|
136
|
+
}
|
|
137
|
+
return normalized;
|
|
138
|
+
}))];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeFinding(value) {
|
|
142
|
+
let finding;
|
|
143
|
+
try {
|
|
144
|
+
finding = typeof value === "string" ? JSON.parse(value) : value;
|
|
145
|
+
} catch {
|
|
146
|
+
throw new Error("--finding-json must be valid JSON.");
|
|
147
|
+
}
|
|
148
|
+
if (!finding || typeof finding !== "object" || Array.isArray(finding)) {
|
|
149
|
+
throw new Error("--finding-json must contain one finding object.");
|
|
150
|
+
}
|
|
151
|
+
const findingId = sanitizeId(String(finding.finding_id || ""));
|
|
152
|
+
const summary = typeof finding.summary === "string" ? finding.summary.trim() : "";
|
|
153
|
+
if (!summary) throw new Error(`Finding ${findingId} requires a non-empty summary.`);
|
|
154
|
+
const severity = finding.severity || "warning";
|
|
155
|
+
if (!VALID_FINDING_SEVERITY.has(severity)) {
|
|
156
|
+
throw new Error(`Finding ${findingId} has invalid severity: ${severity}`);
|
|
157
|
+
}
|
|
158
|
+
const status = finding.status || "open";
|
|
159
|
+
if (!VALID_FINDING_STATUS.has(status)) {
|
|
160
|
+
throw new Error(`Finding ${findingId} has invalid status: ${status}`);
|
|
161
|
+
}
|
|
162
|
+
const evidence = Array.isArray(finding.evidence)
|
|
163
|
+
? [...new Set(finding.evidence.map((item) => String(item).trim()).filter(Boolean))]
|
|
164
|
+
: [];
|
|
165
|
+
const changedFiles = normalizeRelativeFiles(finding.changed_files, `Finding ${findingId} changed_files`);
|
|
166
|
+
const changeSetId = finding.change_set_id ? sanitizeId(String(finding.change_set_id)) : null;
|
|
167
|
+
return {
|
|
168
|
+
finding_id: findingId,
|
|
169
|
+
severity,
|
|
170
|
+
status,
|
|
171
|
+
summary,
|
|
172
|
+
evidence,
|
|
173
|
+
...(changedFiles.length ? { changed_files: changedFiles } : {}),
|
|
174
|
+
...(changeSetId ? { change_set_id: changeSetId } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeChangeSet(value, approvalTimestamp = null) {
|
|
179
|
+
let changeSet;
|
|
180
|
+
try {
|
|
181
|
+
changeSet = typeof value === "string" ? JSON.parse(value) : value;
|
|
182
|
+
} catch {
|
|
183
|
+
throw new Error("--change-set-json must be valid JSON.");
|
|
184
|
+
}
|
|
185
|
+
if (!changeSet || typeof changeSet !== "object" || Array.isArray(changeSet)) {
|
|
186
|
+
throw new Error("--change-set-json must contain one change-set object.");
|
|
187
|
+
}
|
|
188
|
+
const changeSetId = sanitizeId(String(changeSet.change_set_id || ""));
|
|
189
|
+
const suppliedCollectionChecksum = changeSet.collection_checksum
|
|
190
|
+
? String(changeSet.collection_checksum).toLowerCase()
|
|
191
|
+
: null;
|
|
192
|
+
if (suppliedCollectionChecksum && !/^[a-f0-9]{64}$/.test(suppliedCollectionChecksum)) {
|
|
193
|
+
throw new Error(`Change set ${changeSetId} collection_checksum must be a SHA-256 checksum.`);
|
|
194
|
+
}
|
|
195
|
+
const approvalActor = typeof changeSet.approval_actor === "string" ? changeSet.approval_actor.trim() : "";
|
|
196
|
+
if (!approvalActor) throw new Error(`Change set ${changeSetId} requires approval_actor.`);
|
|
197
|
+
const rationale = typeof changeSet.rationale === "string" ? changeSet.rationale.trim() : "";
|
|
198
|
+
if (!rationale) throw new Error(`Change set ${changeSetId} requires rationale.`);
|
|
199
|
+
const findingIds = Array.isArray(changeSet.finding_ids)
|
|
200
|
+
? [...new Set(changeSet.finding_ids.map((item) => sanitizeId(String(item))))]
|
|
201
|
+
: [];
|
|
202
|
+
if (!findingIds.length) throw new Error(`Change set ${changeSetId} requires finding_ids.`);
|
|
203
|
+
const allowedFiles = normalizeRelativeFiles(changeSet.allowed_files, `Change set ${changeSetId} allowed_files`);
|
|
204
|
+
if (!allowedFiles.length) throw new Error(`Change set ${changeSetId} requires allowed_files.`);
|
|
205
|
+
const expectedEvidence = Array.isArray(changeSet.expected_evidence)
|
|
206
|
+
? [...new Set(changeSet.expected_evidence.map((item) => String(item).trim()).filter(Boolean))]
|
|
207
|
+
: [];
|
|
208
|
+
if (!expectedEvidence.length) throw new Error(`Change set ${changeSetId} requires expected_evidence.`);
|
|
209
|
+
const common = {
|
|
210
|
+
change_set_id: changeSetId,
|
|
211
|
+
approval_actor: approvalActor,
|
|
212
|
+
rationale,
|
|
213
|
+
finding_ids: findingIds,
|
|
214
|
+
allowed_files: allowedFiles,
|
|
215
|
+
expected_evidence: expectedEvidence,
|
|
216
|
+
};
|
|
217
|
+
if (approvalTimestamp) {
|
|
218
|
+
const reviewAxes = Array.isArray(changeSet.review_axes)
|
|
219
|
+
? [...new Set(changeSet.review_axes.map((item) => String(item).trim()).filter(Boolean))]
|
|
220
|
+
: [];
|
|
221
|
+
if (!reviewAxes.length) throw new Error(`Change set ${changeSetId} requires review_axes.`);
|
|
222
|
+
const collectionDecision = String(changeSet.collection_decision || "").trim();
|
|
223
|
+
if (!VALID_COLLECTION_DECISION.has(collectionDecision)) {
|
|
224
|
+
throw new Error(`Change set ${changeSetId} collection_decision must be pass, fail, or blocked.`);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
...common,
|
|
228
|
+
review_axes: reviewAxes,
|
|
229
|
+
collection_decision: collectionDecision,
|
|
230
|
+
supplied_collection_checksum: suppliedCollectionChecksum,
|
|
231
|
+
approved_at: approvalTimestamp,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const repairBaseline = normalizeCodeBaseline(
|
|
236
|
+
changeSet.repair_baseline,
|
|
237
|
+
`Change set ${changeSetId} repair_baseline`,
|
|
238
|
+
);
|
|
239
|
+
const feedbackCollection = normalizeFeedbackCollection(changeSet.feedback_collection, changeSetId);
|
|
240
|
+
if (feedbackCollection.subject_checksum !== repairBaseline.subject_checksum) {
|
|
241
|
+
throw new Error(`Change set ${changeSetId} feedback collection does not match its repair baseline subject_checksum.`);
|
|
242
|
+
}
|
|
243
|
+
const collectionChecksum = checksumJson(feedbackCollection);
|
|
244
|
+
if (suppliedCollectionChecksum !== collectionChecksum) {
|
|
245
|
+
throw new Error(`Change set ${changeSetId} collection_checksum does not match its persisted feedback collection.`);
|
|
246
|
+
}
|
|
247
|
+
const collectionFindingIds = feedbackCollection.findings.map((finding) => finding.finding_id);
|
|
248
|
+
if (!sameStringSet(findingIds, collectionFindingIds)) {
|
|
249
|
+
throw new Error(`Change set ${changeSetId} finding_ids do not match its persisted feedback collection.`);
|
|
250
|
+
}
|
|
251
|
+
const approvedAt = changeSet.approved_at;
|
|
252
|
+
if (!approvedAt) throw new Error(`Change set ${changeSetId} requires approved_at.`);
|
|
253
|
+
return {
|
|
254
|
+
...common,
|
|
255
|
+
feedback_collection: feedbackCollection,
|
|
256
|
+
collection_checksum: collectionChecksum,
|
|
257
|
+
repair_baseline: repairBaseline,
|
|
258
|
+
approved_at: approvedAt,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeCodeBaseline(value, label) {
|
|
263
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
264
|
+
throw new Error(`${label} must be an object.`);
|
|
265
|
+
}
|
|
266
|
+
const codeCommit = String(value.code_commit || "").toLowerCase();
|
|
267
|
+
if (!/^[a-f0-9]{40,64}$/.test(codeCommit)) {
|
|
268
|
+
throw new Error(`${label} requires a Git code_commit.`);
|
|
269
|
+
}
|
|
270
|
+
const dirtyFiles = Array.isArray(value.dirty_files) ? value.dirty_files.map((file) => {
|
|
271
|
+
if (!file || typeof file !== "object" || Array.isArray(file)) {
|
|
272
|
+
throw new Error(`${label} dirty_files must contain objects.`);
|
|
273
|
+
}
|
|
274
|
+
const [relativePath] = normalizeRelativeFiles([file.path], `${label} dirty_files`);
|
|
275
|
+
const checksum = file.checksum === null ? null : String(file.checksum || "").toLowerCase();
|
|
276
|
+
if (checksum !== null && !/^[a-f0-9]{64}$/.test(checksum)) {
|
|
277
|
+
throw new Error(`${label} has an invalid checksum for ${relativePath}.`);
|
|
278
|
+
}
|
|
279
|
+
return { path: relativePath, checksum };
|
|
280
|
+
}) : [];
|
|
281
|
+
dirtyFiles.sort((left, right) => left.path.localeCompare(right.path));
|
|
282
|
+
if (new Set(dirtyFiles.map((file) => file.path)).size !== dirtyFiles.length) {
|
|
283
|
+
throw new Error(`${label} contains duplicate dirty files.`);
|
|
284
|
+
}
|
|
285
|
+
const baseline = { code_commit: codeCommit, dirty_files: dirtyFiles };
|
|
286
|
+
const subjectChecksum = checksumJson(baseline);
|
|
287
|
+
if (String(value.subject_checksum || "").toLowerCase() !== subjectChecksum) {
|
|
288
|
+
throw new Error(`${label} subject_checksum does not match its persisted baseline.`);
|
|
289
|
+
}
|
|
290
|
+
return { ...baseline, subject_checksum: subjectChecksum };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function normalizeFeedbackCollection(value, changeSetId) {
|
|
294
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
295
|
+
throw new Error(`Change set ${changeSetId} requires feedback_collection.`);
|
|
296
|
+
}
|
|
297
|
+
const subjectChecksum = String(value.subject_checksum || "").toLowerCase();
|
|
298
|
+
if (!/^[a-f0-9]{64}$/.test(subjectChecksum)) {
|
|
299
|
+
throw new Error(`Change set ${changeSetId} feedback_collection requires subject_checksum.`);
|
|
300
|
+
}
|
|
301
|
+
const reviewAxes = Array.isArray(value.review_axes)
|
|
302
|
+
? [...new Set(value.review_axes.map((item) => String(item).trim()).filter(Boolean))]
|
|
303
|
+
: [];
|
|
304
|
+
if (!reviewAxes.length) throw new Error(`Change set ${changeSetId} feedback_collection requires review_axes.`);
|
|
305
|
+
const decision = String(value.decision || "").trim();
|
|
306
|
+
if (!VALID_COLLECTION_DECISION.has(decision)) {
|
|
307
|
+
throw new Error(`Change set ${changeSetId} feedback_collection decision must be pass, fail, or blocked.`);
|
|
308
|
+
}
|
|
309
|
+
const findings = Array.isArray(value.findings) ? value.findings.map(normalizeFinding) : [];
|
|
310
|
+
if (!findings.length || findings.some((finding) => finding.status !== "open")) {
|
|
311
|
+
throw new Error(`Change set ${changeSetId} feedback_collection requires open findings.`);
|
|
312
|
+
}
|
|
313
|
+
findings.sort((left, right) => left.finding_id.localeCompare(right.finding_id));
|
|
314
|
+
if (new Set(findings.map((finding) => finding.finding_id)).size !== findings.length) {
|
|
315
|
+
throw new Error(`Change set ${changeSetId} feedback_collection contains duplicate findings.`);
|
|
316
|
+
}
|
|
317
|
+
return { subject_checksum: subjectChecksum, review_axes: reviewAxes, decision, findings };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function projectedTaskStatus(status, target) {
|
|
321
|
+
return TASK_STATUS_PROJECTION.get(status)?.[target] || "active";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function normalizeAttempt(taskId, attempt, fallbackNumber) {
|
|
325
|
+
const number = Number(attempt?.number || fallbackNumber);
|
|
326
|
+
const phases = Array.isArray(attempt?.phases) ? attempt.phases.map((phase, index) => {
|
|
327
|
+
const sequence = Number(phase?.sequence || index + 1);
|
|
328
|
+
const phaseName = parsePhase(phase?.phase, "implementation");
|
|
329
|
+
const normalized = {
|
|
330
|
+
sequence,
|
|
331
|
+
phase: phaseName,
|
|
332
|
+
status: VALID_PHASE_STATUS.has(phase?.status) ? phase.status : "active",
|
|
333
|
+
started_at: phase?.started_at || null,
|
|
334
|
+
finished_at: phase?.finished_at || null,
|
|
335
|
+
findings: Array.isArray(phase?.findings) ? phase.findings.map(normalizeFinding) : [],
|
|
336
|
+
};
|
|
337
|
+
if (phase?.review_subject_baseline) {
|
|
338
|
+
if (phaseName !== "review") {
|
|
339
|
+
throw new Error(`Task ${taskId} phase ${sequence} stores a review subject outside review.`);
|
|
340
|
+
}
|
|
341
|
+
normalized.review_subject_baseline = normalizeCodeBaseline(
|
|
342
|
+
phase.review_subject_baseline,
|
|
343
|
+
`Task ${taskId} review phase ${sequence} review_subject_baseline`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return normalized;
|
|
347
|
+
}) : [];
|
|
348
|
+
const timeEntries = Array.isArray(attempt?.time_entries) ? attempt.time_entries.map((entry) => ({
|
|
349
|
+
category: VALID_TIME_CATEGORY.has(entry?.category) ? entry.category : "workflow",
|
|
350
|
+
elapsed_minutes: parseElapsedMinutes(entry?.elapsed_minutes) || 0,
|
|
351
|
+
phase: parsePhase(entry?.phase, phases.at(-1)?.phase || "implementation"),
|
|
352
|
+
recorded_at: entry?.recorded_at || null,
|
|
353
|
+
})) : [];
|
|
354
|
+
const timeByCategory = {};
|
|
355
|
+
for (const entry of timeEntries) {
|
|
356
|
+
timeByCategory[entry.category] = (timeByCategory[entry.category] || 0) + entry.elapsed_minutes;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
attempt_id: attempt?.attempt_id || `${taskId}/A${number}`,
|
|
360
|
+
number,
|
|
361
|
+
status: new Set(["active", "paused", "completed", "failed", "abandoned"]).has(attempt?.status)
|
|
362
|
+
? attempt.status
|
|
363
|
+
: "active",
|
|
364
|
+
started_at: attempt?.started_at || null,
|
|
365
|
+
finished_at: attempt?.finished_at || null,
|
|
366
|
+
phases,
|
|
367
|
+
time_entries: timeEntries,
|
|
368
|
+
time_by_category: timeByCategory,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
70
372
|
function parseNextActionOwner(value, fallback = "agent") {
|
|
71
373
|
const owner = value === undefined ? fallback : value;
|
|
72
374
|
if (!VALID_NEXT_ACTION_OWNER.has(owner)) {
|
|
@@ -137,10 +439,82 @@ function statePaths(args) {
|
|
|
137
439
|
return { projectRoot, stateRoot, workflowRoot, runtime, marker };
|
|
138
440
|
}
|
|
139
441
|
|
|
442
|
+
let activeWriteTransaction = null;
|
|
443
|
+
|
|
444
|
+
function stagedContent(file) {
|
|
445
|
+
const staged = activeWriteTransaction?.entries.get(path.resolve(file));
|
|
446
|
+
if (staged?.action === "write") return staged.content;
|
|
447
|
+
if (staged?.action === "delete") return null;
|
|
448
|
+
return fs.existsSync(file) ? fs.readFileSync(file) : null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function stageWrite(file, content) {
|
|
452
|
+
if (!activeWriteTransaction) return false;
|
|
453
|
+
const target = path.resolve(file);
|
|
454
|
+
activeWriteTransaction.entries.set(target, {
|
|
455
|
+
action: "write", file: target, content: Buffer.from(content),
|
|
456
|
+
});
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function stageDelete(file) {
|
|
461
|
+
if (!activeWriteTransaction) return false;
|
|
462
|
+
const target = path.resolve(file);
|
|
463
|
+
activeWriteTransaction.entries.set(target, { action: "delete", file: target });
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function commitWriteTransaction() {
|
|
468
|
+
if (!activeWriteTransaction) throw new Error("Task Writer transaction is not active");
|
|
469
|
+
const entries = [...activeWriteTransaction.entries.values()];
|
|
470
|
+
const snapshots = entries.map((entry) => {
|
|
471
|
+
if (fs.existsSync(entry.file) && !fs.statSync(entry.file).isFile()) {
|
|
472
|
+
throw new Error(`Task Writer transaction target must be a file: ${entry.file}`);
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
file: entry.file,
|
|
476
|
+
existed: fs.existsSync(entry.file),
|
|
477
|
+
content: fs.existsSync(entry.file) ? fs.readFileSync(entry.file) : null,
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
const temporary = new Map();
|
|
481
|
+
try {
|
|
482
|
+
for (const entry of entries.filter((item) => item.action === "write")) {
|
|
483
|
+
fs.mkdirSync(path.dirname(entry.file), { recursive: true });
|
|
484
|
+
const temp = `${entry.file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
485
|
+
fs.writeFileSync(temp, entry.content, { flag: "wx" });
|
|
486
|
+
temporary.set(entry.file, temp);
|
|
487
|
+
}
|
|
488
|
+
for (const entry of entries) {
|
|
489
|
+
if (entry.action === "write") fs.renameSync(temporary.get(entry.file), entry.file);
|
|
490
|
+
else fs.rmSync(entry.file, { force: true });
|
|
491
|
+
if (process.env.AI_DELIVERY_TEST_TASK_TRANSACTION_FAILURE_SUFFIX
|
|
492
|
+
&& entry.file.endsWith(process.env.AI_DELIVERY_TEST_TASK_TRANSACTION_FAILURE_SUFFIX)) {
|
|
493
|
+
throw new Error("injected task transaction failure");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch (error) {
|
|
497
|
+
for (const temp of temporary.values()) fs.rmSync(temp, { force: true });
|
|
498
|
+
for (const snapshot of snapshots.reverse()) {
|
|
499
|
+
if (snapshot.existed) {
|
|
500
|
+
fs.mkdirSync(path.dirname(snapshot.file), { recursive: true });
|
|
501
|
+
fs.writeFileSync(snapshot.file, snapshot.content);
|
|
502
|
+
} else {
|
|
503
|
+
fs.rmSync(snapshot.file, { force: true });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
throw error;
|
|
507
|
+
} finally {
|
|
508
|
+
activeWriteTransaction = null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
140
512
|
function atomicWrite(file, value) {
|
|
513
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
514
|
+
if (stageWrite(file, content)) return;
|
|
141
515
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
142
516
|
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
143
|
-
fs.writeFileSync(temp,
|
|
517
|
+
fs.writeFileSync(temp, content, "utf8");
|
|
144
518
|
fs.renameSync(temp, file);
|
|
145
519
|
}
|
|
146
520
|
|
|
@@ -212,17 +586,20 @@ function portablePath(root, target) {
|
|
|
212
586
|
: target;
|
|
213
587
|
}
|
|
214
588
|
|
|
215
|
-
function
|
|
216
|
-
const workspaceFile = path.join(workflowRoot, "config", "workspace.yaml");
|
|
217
|
-
if (!fs.existsSync(workspaceFile)) return null;
|
|
218
|
-
let yaml;
|
|
589
|
+
function yamlLibrary(workflowRoot) {
|
|
219
590
|
try {
|
|
220
|
-
|
|
591
|
+
return moduleRequire("yaml");
|
|
221
592
|
} catch {
|
|
222
593
|
const vendorRoot = path.join(workflowRoot, "tools", "state", "vendor", "yaml");
|
|
223
594
|
const vendorRequire = createRequire(path.join(vendorRoot, "package.json"));
|
|
224
|
-
|
|
595
|
+
return vendorRequire("./");
|
|
225
596
|
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function parseWorkspaceConfiguration(workflowRoot) {
|
|
600
|
+
const workspaceFile = path.join(workflowRoot, "config", "workspace.yaml");
|
|
601
|
+
if (!fs.existsSync(workspaceFile)) return null;
|
|
602
|
+
const yaml = yamlLibrary(workflowRoot);
|
|
226
603
|
return { file: workspaceFile, configuration: yaml.parse(fs.readFileSync(workspaceFile, "utf8")) };
|
|
227
604
|
}
|
|
228
605
|
|
|
@@ -253,7 +630,97 @@ function configuredCodeState(workflowRoot) {
|
|
|
253
630
|
return code;
|
|
254
631
|
}
|
|
255
632
|
|
|
633
|
+
function workingTreeChecksum(repositoryRoot, relativePath) {
|
|
634
|
+
const absolute = path.resolve(repositoryRoot, ...relativePath.split("/"));
|
|
635
|
+
const relative = path.relative(repositoryRoot, absolute);
|
|
636
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
637
|
+
throw new Error(`Git path escapes the configured code repository: ${relativePath}`);
|
|
638
|
+
}
|
|
639
|
+
if (!fs.existsSync(absolute)) return null;
|
|
640
|
+
const stat = fs.lstatSync(absolute);
|
|
641
|
+
if (stat.isSymbolicLink()) return sha256Content(`symlink:${fs.readlinkSync(absolute)}`);
|
|
642
|
+
if (stat.isFile()) return sha256Content(fs.readFileSync(absolute));
|
|
643
|
+
return sha256Content(`non-file:${relativePath}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function captureCodeBaseline(workflowRoot) {
|
|
647
|
+
const code = configuredCodeState(workflowRoot);
|
|
648
|
+
if (!code.head) throw new Error("Configured code repository requires a HEAD before approving a change set.");
|
|
649
|
+
const dirtyFiles = code.dirty_paths
|
|
650
|
+
.map((relativePath) => ({
|
|
651
|
+
path: relativePath,
|
|
652
|
+
checksum: workingTreeChecksum(code.root, relativePath),
|
|
653
|
+
}))
|
|
654
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
655
|
+
const baseline = { code_commit: code.head.toLowerCase(), dirty_files: dirtyFiles };
|
|
656
|
+
return { ...baseline, subject_checksum: checksumJson(baseline) };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function approveChangeSet(candidate, openFindings, reviewSubjectBaseline, workflowRoot) {
|
|
660
|
+
const currentSubject = captureCodeBaseline(workflowRoot);
|
|
661
|
+
if (currentSubject.subject_checksum !== reviewSubjectBaseline.subject_checksum) {
|
|
662
|
+
throw new Error(
|
|
663
|
+
`Change set ${candidate.change_set_id} review subject changed during feedback collection.`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const repairBaseline = reviewSubjectBaseline;
|
|
667
|
+
const feedbackCollection = {
|
|
668
|
+
subject_checksum: repairBaseline.subject_checksum,
|
|
669
|
+
review_axes: candidate.review_axes,
|
|
670
|
+
decision: candidate.collection_decision,
|
|
671
|
+
findings: [...openFindings].sort((left, right) => left.finding_id.localeCompare(right.finding_id)),
|
|
672
|
+
};
|
|
673
|
+
const collectionChecksum = checksumJson(feedbackCollection);
|
|
674
|
+
if (candidate.supplied_collection_checksum && candidate.supplied_collection_checksum !== collectionChecksum) {
|
|
675
|
+
throw new Error(
|
|
676
|
+
`Change set ${candidate.change_set_id} collection_checksum does not match the computed feedback collection.`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
change_set_id: candidate.change_set_id,
|
|
681
|
+
feedback_collection: feedbackCollection,
|
|
682
|
+
collection_checksum: collectionChecksum,
|
|
683
|
+
repair_baseline: repairBaseline,
|
|
684
|
+
approval_actor: candidate.approval_actor,
|
|
685
|
+
rationale: candidate.rationale,
|
|
686
|
+
finding_ids: candidate.finding_ids,
|
|
687
|
+
allowed_files: candidate.allowed_files,
|
|
688
|
+
expected_evidence: candidate.expected_evidence,
|
|
689
|
+
approved_at: candidate.approved_at,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function gitPathList(cwd, args, label) {
|
|
694
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
|
|
695
|
+
if (result.status !== 0) {
|
|
696
|
+
throw new Error(`${label}: ${(result.stderr || result.error?.message || "Git command failed.").trim()}`);
|
|
697
|
+
}
|
|
698
|
+
return result.stdout.split("\0").filter(Boolean).map((item) => item.replaceAll("\\", "/"));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function repairChangesSinceBaseline(workflowRoot, baseline) {
|
|
702
|
+
const code = configuredCodeState(workflowRoot);
|
|
703
|
+
const committedOrTracked = gitPathList(
|
|
704
|
+
code.root,
|
|
705
|
+
["diff", "--name-only", "-z", baseline.code_commit, "--"],
|
|
706
|
+
`Cannot compare the repair baseline ${baseline.code_commit}`,
|
|
707
|
+
);
|
|
708
|
+
const candidates = new Set([
|
|
709
|
+
...committedOrTracked,
|
|
710
|
+
...code.dirty_paths,
|
|
711
|
+
...baseline.dirty_files.map((file) => file.path),
|
|
712
|
+
]);
|
|
713
|
+
const baselineDirty = new Map(baseline.dirty_files.map((file) => [file.path, file.checksum]));
|
|
714
|
+
return [...candidates].filter((relativePath) => {
|
|
715
|
+
const currentChecksum = workingTreeChecksum(code.root, relativePath);
|
|
716
|
+
return baselineDirty.has(relativePath)
|
|
717
|
+
? currentChecksum !== baselineDirty.get(relativePath)
|
|
718
|
+
: true;
|
|
719
|
+
}).sort((left, right) => left.localeCompare(right));
|
|
720
|
+
}
|
|
721
|
+
|
|
256
722
|
function writeSharedTask(runtime, workflowRoot, task) {
|
|
723
|
+
assertNotLegacyDevelopmentNode(task.node);
|
|
257
724
|
const sharedFile = sharedTaskFile(workflowRoot, task.task_id);
|
|
258
725
|
let previousRevision = 0;
|
|
259
726
|
if (fs.existsSync(sharedFile)) {
|
|
@@ -313,22 +780,267 @@ function readTask(runtime, taskId) {
|
|
|
313
780
|
function normalizeTask(task) {
|
|
314
781
|
for (const key of [
|
|
315
782
|
"scope", "input_artifacts", "standards", "planned_outputs", "actual_outputs",
|
|
316
|
-
"completed_steps", "blockers", "evidence", "depends_on",
|
|
783
|
+
"completed_steps", "blockers", "evidence", "depends_on", "acceptance",
|
|
317
784
|
]) {
|
|
318
785
|
if (!Array.isArray(task[key])) task[key] = [];
|
|
319
786
|
task[key] = [...new Set(task[key])];
|
|
320
787
|
}
|
|
321
788
|
task.sequence = parseSequence(task.sequence);
|
|
322
789
|
task.next_action_owner = parseNextActionOwner(task.next_action_owner);
|
|
790
|
+
task.responsibility = task.responsibility || task.skill || "agent";
|
|
791
|
+
task.derived_from = task.derived_from || null;
|
|
792
|
+
task.supersedes = task.supersedes || null;
|
|
793
|
+
if (!Array.isArray(task.attempts)) {
|
|
794
|
+
const legacyNumber = Number(task.attempt || 0);
|
|
795
|
+
task.attempts = legacyNumber > 0 && (task.started_at || task.status !== "queued")
|
|
796
|
+
? [normalizeAttempt(task.task_id, {
|
|
797
|
+
number: legacyNumber,
|
|
798
|
+
status: projectedTaskStatus(task.status, "attempt"),
|
|
799
|
+
started_at: task.started_at || task.created_at || null,
|
|
800
|
+
finished_at: CLOSED_STATUS.has(task.status) ? task.finished_at || task.updated_at || null : null,
|
|
801
|
+
}, legacyNumber)]
|
|
802
|
+
: [];
|
|
803
|
+
} else {
|
|
804
|
+
task.attempts = task.attempts.map((attempt, index) => normalizeAttempt(task.task_id, attempt, index + 1));
|
|
805
|
+
}
|
|
806
|
+
task.change_sets = Array.isArray(task.change_sets) ? task.change_sets.map((item) => normalizeChangeSet(item)) : [];
|
|
807
|
+
task.attempt = task.attempts.at(-1)?.number || 0;
|
|
323
808
|
return task;
|
|
324
809
|
}
|
|
325
810
|
|
|
811
|
+
function logicalContract(task) {
|
|
812
|
+
return {
|
|
813
|
+
iteration_id: task.iteration_id,
|
|
814
|
+
node: task.node,
|
|
815
|
+
skill: task.skill,
|
|
816
|
+
goal: task.goal,
|
|
817
|
+
responsibility: task.responsibility,
|
|
818
|
+
scope: [...task.scope].sort(),
|
|
819
|
+
depends_on: [...task.depends_on].sort(),
|
|
820
|
+
acceptance: [...task.acceptance].sort(),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function logicalContractChanges(existing, candidate) {
|
|
825
|
+
const before = logicalContract(existing);
|
|
826
|
+
const after = logicalContract(candidate);
|
|
827
|
+
return Object.keys(before).filter((key) => JSON.stringify(before[key]) !== JSON.stringify(after[key]));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function currentAttempt(task) {
|
|
831
|
+
return task.attempts.at(-1) || null;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function createAttemptPhase(sequence, phase, timestamp) {
|
|
835
|
+
return {
|
|
836
|
+
sequence,
|
|
837
|
+
phase: parsePhase(phase, "implementation"),
|
|
838
|
+
status: "active",
|
|
839
|
+
started_at: timestamp,
|
|
840
|
+
finished_at: null,
|
|
841
|
+
findings: [],
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function startAttempt(task, phase, timestamp) {
|
|
846
|
+
const number = (currentAttempt(task)?.number || 0) + 1;
|
|
847
|
+
const attempt = normalizeAttempt(task.task_id, {
|
|
848
|
+
number,
|
|
849
|
+
status: "active",
|
|
850
|
+
started_at: timestamp,
|
|
851
|
+
phases: [createAttemptPhase(1, phase, timestamp)],
|
|
852
|
+
time_entries: [],
|
|
853
|
+
}, number);
|
|
854
|
+
task.attempts.push(attempt);
|
|
855
|
+
task.attempt = number;
|
|
856
|
+
return attempt;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function enterAttemptPhase(attempt, phase, timestamp) {
|
|
860
|
+
const selected = parsePhase(phase, attempt.phases.at(-1)?.phase || "implementation");
|
|
861
|
+
let current = attempt.phases.at(-1);
|
|
862
|
+
if (!current || current.phase !== selected) {
|
|
863
|
+
if (current && current.status === "active") {
|
|
864
|
+
current.status = "completed";
|
|
865
|
+
current.finished_at = timestamp;
|
|
866
|
+
}
|
|
867
|
+
current = createAttemptPhase(attempt.phases.length + 1, selected, timestamp);
|
|
868
|
+
attempt.phases.push(current);
|
|
869
|
+
}
|
|
870
|
+
return current;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function recordAttemptProgress(task, args, taskStatus, finish, timestamp, workflowRoot) {
|
|
874
|
+
if (args["phase-status"] !== undefined) {
|
|
875
|
+
throw new Error("--phase-status is not supported; phase status is derived from the task transition.");
|
|
876
|
+
}
|
|
877
|
+
const priorFindings = new Map(currentFindings(task).map((finding) => [finding.finding_id, finding]));
|
|
878
|
+
let attempt = currentAttempt(task);
|
|
879
|
+
if (!attempt) attempt = startAttempt(task, args.phase, task.started_at || timestamp);
|
|
880
|
+
const phase = enterAttemptPhase(attempt, args.phase, timestamp);
|
|
881
|
+
if (phase.phase === "review" && !phase.review_subject_baseline) {
|
|
882
|
+
phase.review_subject_baseline = captureCodeBaseline(workflowRoot);
|
|
883
|
+
}
|
|
884
|
+
const submittedFindings = (args["finding-json"] || []).map(normalizeFinding);
|
|
885
|
+
const currentFindingStates = new Map(priorFindings);
|
|
886
|
+
for (const finding of submittedFindings) {
|
|
887
|
+
if (finding.status === "resolved" && !finding.change_set_id) {
|
|
888
|
+
throw new Error(`Resolved finding ${finding.finding_id} requires change_set_id.`);
|
|
889
|
+
}
|
|
890
|
+
if (finding.status === "resolved" && !finding.evidence.length) {
|
|
891
|
+
throw new Error(`Resolved finding ${finding.finding_id} requires evidence.`);
|
|
892
|
+
}
|
|
893
|
+
if (finding.status === "resolved" && priorFindings.get(finding.finding_id)?.status !== "open") {
|
|
894
|
+
throw new Error(`Resolved finding ${finding.finding_id} requires an existing open finding.`);
|
|
895
|
+
}
|
|
896
|
+
currentFindingStates.set(finding.finding_id, finding);
|
|
897
|
+
}
|
|
898
|
+
for (const rawChangeSet of args["change-set-json"] || []) {
|
|
899
|
+
if (phase.phase !== "review") {
|
|
900
|
+
throw new Error("Approved change sets must be recorded in the review phase.");
|
|
901
|
+
}
|
|
902
|
+
const candidate = normalizeChangeSet(rawChangeSet, timestamp);
|
|
903
|
+
if (task.change_sets.some((item) => item.change_set_id === candidate.change_set_id)) {
|
|
904
|
+
throw new Error(`Change set ${candidate.change_set_id} is already recorded and immutable.`);
|
|
905
|
+
}
|
|
906
|
+
const unavailable = candidate.finding_ids.filter((findingId) => currentFindingStates.get(findingId)?.status !== "open");
|
|
907
|
+
if (unavailable.length) {
|
|
908
|
+
throw new Error(`Change set ${candidate.change_set_id} requires open collected findings: ${unavailable.join(", ")}.`);
|
|
909
|
+
}
|
|
910
|
+
const openFindings = [...currentFindingStates.values()].filter((finding) => finding.status === "open");
|
|
911
|
+
const omitted = openFindings
|
|
912
|
+
.map((finding) => finding.finding_id)
|
|
913
|
+
.filter((findingId) => !candidate.finding_ids.includes(findingId));
|
|
914
|
+
if (omitted.length) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`Change set ${candidate.change_set_id} must include the complete feedback collection; missing: ${omitted.join(", ")}.`,
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
const changeSet = approveChangeSet(candidate, openFindings, phase.review_subject_baseline, workflowRoot);
|
|
920
|
+
task.change_sets.push(changeSet);
|
|
921
|
+
}
|
|
922
|
+
const approvedChangeSets = new Map(task.change_sets.map((item) => [item.change_set_id, item]));
|
|
923
|
+
for (const finding of submittedFindings) {
|
|
924
|
+
if (finding.status !== "resolved") continue;
|
|
925
|
+
const changeSet = approvedChangeSets.get(finding.change_set_id);
|
|
926
|
+
if (!changeSet) {
|
|
927
|
+
throw new Error(`Resolved finding ${finding.finding_id} references change set ${finding.change_set_id}, which is not approved.`);
|
|
928
|
+
}
|
|
929
|
+
if (!changeSet.finding_ids.includes(finding.finding_id)) {
|
|
930
|
+
throw new Error(`Resolved finding ${finding.finding_id} is not included in approved change set ${finding.change_set_id}.`);
|
|
931
|
+
}
|
|
932
|
+
if (phase.phase !== "review") {
|
|
933
|
+
throw new Error(`Resolved finding ${finding.finding_id} must be recorded during concentrated rereview.`);
|
|
934
|
+
}
|
|
935
|
+
if (!finding.changed_files?.length) {
|
|
936
|
+
throw new Error(`Resolved finding ${finding.finding_id} requires changed_files from its approved change set.`);
|
|
937
|
+
}
|
|
938
|
+
const actualChanges = repairChangesSinceBaseline(workflowRoot, changeSet.repair_baseline);
|
|
939
|
+
if (!sameStringSet(finding.changed_files, actualChanges)) {
|
|
940
|
+
const undeclared = actualChanges.filter((file) => !finding.changed_files.includes(file));
|
|
941
|
+
const notActual = finding.changed_files.filter((file) => !actualChanges.includes(file));
|
|
942
|
+
const details = [
|
|
943
|
+
...(undeclared.length ? [`undeclared: ${undeclared.join(", ")}`] : []),
|
|
944
|
+
...(notActual.length ? [`not actual: ${notActual.join(", ")}`] : []),
|
|
945
|
+
].join("; ");
|
|
946
|
+
throw new Error(
|
|
947
|
+
`Resolved finding ${finding.finding_id} changed_files must exactly match actual Git changes since approval${details ? `; ${details}` : ""}.`,
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const outsideFiles = finding.changed_files.filter((file) => !changeSet.allowed_files.includes(file));
|
|
951
|
+
if (outsideFiles.length) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`Resolved finding ${finding.finding_id} changed files outside approved change set ${finding.change_set_id}: ${outsideFiles.join(", ")}.`,
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const missingEvidence = changeSet.expected_evidence.filter((item) => !finding.evidence.includes(item));
|
|
957
|
+
if (missingEvidence.length) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`Resolved finding ${finding.finding_id} is missing expected evidence for ${finding.change_set_id}: ${missingEvidence.join(", ")}.`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
phase.findings.push(...submittedFindings);
|
|
964
|
+
const elapsedMinutes = parseElapsedMinutes(args["elapsed-minutes"]);
|
|
965
|
+
const timeCategory = args["time-category"];
|
|
966
|
+
if ((elapsedMinutes === null) !== (timeCategory === undefined)) {
|
|
967
|
+
throw new Error("--time-category and --elapsed-minutes must be provided together.");
|
|
968
|
+
}
|
|
969
|
+
if (timeCategory !== undefined) {
|
|
970
|
+
if (!VALID_TIME_CATEGORY.has(timeCategory)) {
|
|
971
|
+
throw new Error(`Invalid time category: ${timeCategory}; use ${[...VALID_TIME_CATEGORY].join(", ")}.`);
|
|
972
|
+
}
|
|
973
|
+
attempt.time_entries.push({
|
|
974
|
+
category: timeCategory,
|
|
975
|
+
elapsed_minutes: elapsedMinutes,
|
|
976
|
+
phase: phase.phase,
|
|
977
|
+
recorded_at: timestamp,
|
|
978
|
+
});
|
|
979
|
+
attempt.time_by_category[timeCategory] = (attempt.time_by_category[timeCategory] || 0) + elapsedMinutes;
|
|
980
|
+
}
|
|
981
|
+
phase.status = finish ? projectedTaskStatus(taskStatus, "phase") : "active";
|
|
982
|
+
phase.finished_at = phase.status === "active" ? null : timestamp;
|
|
983
|
+
attempt.status = finish ? projectedTaskStatus(taskStatus, "attempt") : "active";
|
|
984
|
+
attempt.finished_at = new Set(["completed", "failed", "abandoned"]).has(attempt.status) ? timestamp : null;
|
|
985
|
+
return attempt;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function summarizeAttempts(task) {
|
|
989
|
+
const timeByCategory = {};
|
|
990
|
+
for (const attempt of task.attempts) {
|
|
991
|
+
for (const [category, minutes] of Object.entries(attempt.time_by_category || {})) {
|
|
992
|
+
timeByCategory[category] = (timeByCategory[category] || 0) + Number(minutes || 0);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
total: task.attempts.length,
|
|
997
|
+
active: task.attempts.filter((attempt) => attempt.status === "active").length,
|
|
998
|
+
paused: task.attempts.filter((attempt) => attempt.status === "paused").length,
|
|
999
|
+
completed: task.attempts.filter((attempt) => attempt.status === "completed").length,
|
|
1000
|
+
failed: task.attempts.filter((attempt) => attempt.status === "failed").length,
|
|
1001
|
+
abandoned: task.attempts.filter((attempt) => attempt.status === "abandoned").length,
|
|
1002
|
+
current_attempt_id: currentAttempt(task)?.attempt_id || null,
|
|
1003
|
+
current_phase: currentAttempt(task)?.phases.at(-1)?.phase || null,
|
|
1004
|
+
time_by_category: timeByCategory,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function currentFindings(task) {
|
|
1009
|
+
const findingsById = new Map();
|
|
1010
|
+
for (const attempt of task.attempts) {
|
|
1011
|
+
for (const phase of attempt.phases) {
|
|
1012
|
+
for (const finding of phase.findings) findingsById.set(finding.finding_id, finding);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return [...findingsById.values()];
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function assertNoOpenBlockingFindings(task) {
|
|
1019
|
+
const blocking = currentFindings(task)
|
|
1020
|
+
.filter((finding) => finding.severity === "blocking" && finding.status === "open")
|
|
1021
|
+
.map((finding) => finding.finding_id);
|
|
1022
|
+
if (blocking.length) {
|
|
1023
|
+
throw new Error(`Task ${task.task_id} has open blocking findings: ${blocking.join(", ")}.`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
326
1027
|
function stableValue(value) {
|
|
327
1028
|
if (Array.isArray(value)) return value.map(stableValue);
|
|
328
1029
|
if (!value || typeof value !== "object") return value;
|
|
329
1030
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
330
1031
|
}
|
|
331
1032
|
|
|
1033
|
+
function checksumJson(value) {
|
|
1034
|
+
return sha256Content(JSON.stringify(stableValue(value)));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function sameStringSet(left, right) {
|
|
1038
|
+
if (left.length !== right.length) return false;
|
|
1039
|
+
const sortedLeft = [...left].sort();
|
|
1040
|
+
const sortedRight = [...right].sort();
|
|
1041
|
+
return sortedLeft.every((item, index) => item === sortedRight[index]);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
332
1044
|
function comparableTaskState(task) {
|
|
333
1045
|
const comparable = { ...task };
|
|
334
1046
|
const stateRoot = path.resolve(comparable.state_root || ".");
|
|
@@ -370,8 +1082,11 @@ function taskSources(runtime, workflowRoot, stateRoot, taskId) {
|
|
|
370
1082
|
|
|
371
1083
|
function appendEvent(runtime, taskId, type, details = {}) {
|
|
372
1084
|
const file = path.join(runtime, "events", `${taskId}.jsonl`);
|
|
1085
|
+
const previous = stagedContent(file)?.toString("utf8") || "";
|
|
1086
|
+
const content = `${previous}${JSON.stringify({ at: now(), type, task_id: taskId, ...details })}\n`;
|
|
1087
|
+
if (stageWrite(file, content)) return;
|
|
373
1088
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
374
|
-
fs.
|
|
1089
|
+
fs.writeFileSync(file, content, "utf8");
|
|
375
1090
|
}
|
|
376
1091
|
|
|
377
1092
|
function copyIfExists(source, destination) {
|
|
@@ -393,7 +1108,9 @@ function clearTaskMarker(task) {
|
|
|
393
1108
|
if (!fs.existsSync(markerFile)) return;
|
|
394
1109
|
try {
|
|
395
1110
|
const marker = JSON.parse(fs.readFileSync(markerFile, "utf8"));
|
|
396
|
-
if (marker.task_id === task.task_id)
|
|
1111
|
+
if (marker.task_id === task.task_id) {
|
|
1112
|
+
if (!stageDelete(markerFile)) fs.unlinkSync(markerFile);
|
|
1113
|
+
}
|
|
397
1114
|
} catch {
|
|
398
1115
|
// Preserve an invalid marker for explicit reconciliation.
|
|
399
1116
|
}
|
|
@@ -446,12 +1163,87 @@ function copyTaskToPortableArchive(stateRoot, workflowRoot, task, archiveDirecto
|
|
|
446
1163
|
return portableTask;
|
|
447
1164
|
}
|
|
448
1165
|
|
|
1166
|
+
function sha256Content(value) {
|
|
1167
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function formalVersionArchiveReferences(stateRoot, workflowRoot, iterationId, requestedCandidateId) {
|
|
1171
|
+
const stateCli = path.join(workflowRoot, "tools", "state", "state.mjs");
|
|
1172
|
+
if (!fs.existsSync(stateCli)) {
|
|
1173
|
+
throw new Error(`Formal-state CLI is missing: ${stateCli}`);
|
|
1174
|
+
}
|
|
1175
|
+
const command = [
|
|
1176
|
+
stateCli,
|
|
1177
|
+
"snapshot", "references",
|
|
1178
|
+
"--project", stateRoot,
|
|
1179
|
+
"--iteration-id", iterationId,
|
|
1180
|
+
];
|
|
1181
|
+
if (requestedCandidateId) command.push("--candidate-id", requestedCandidateId);
|
|
1182
|
+
const result = spawnSync(process.execPath, command, {
|
|
1183
|
+
cwd: stateRoot,
|
|
1184
|
+
encoding: "utf8",
|
|
1185
|
+
windowsHide: true,
|
|
1186
|
+
});
|
|
1187
|
+
if (result.status !== 0) {
|
|
1188
|
+
const message = (result.stderr || result.error?.message || "Formal-state reference projection failed.")
|
|
1189
|
+
.trim()
|
|
1190
|
+
.replace(/^ai-delivery-state:\s*/, "");
|
|
1191
|
+
throw new Error(message);
|
|
1192
|
+
}
|
|
1193
|
+
let projection;
|
|
1194
|
+
try {
|
|
1195
|
+
projection = JSON.parse(result.stdout);
|
|
1196
|
+
} catch {
|
|
1197
|
+
throw new Error("Formal-state reference projection returned invalid JSON.");
|
|
1198
|
+
}
|
|
1199
|
+
if (projection?.command !== "snapshot references" || !projection.references) {
|
|
1200
|
+
throw new Error("Formal-state reference projection is incomplete.");
|
|
1201
|
+
}
|
|
1202
|
+
return projection.references;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function archivedTaskReferences(stateRoot, archivedTasks) {
|
|
1206
|
+
const taskSummaries = [];
|
|
1207
|
+
const attemptReceipts = [];
|
|
1208
|
+
for (const task of archivedTasks) {
|
|
1209
|
+
const taskRef = `${task.archive_path}/task.json`;
|
|
1210
|
+
const taskFile = path.resolve(stateRoot, ...taskRef.split("/"));
|
|
1211
|
+
taskSummaries.push({
|
|
1212
|
+
task_id: task.task_id,
|
|
1213
|
+
status: task.status,
|
|
1214
|
+
finished_at: task.finished_at,
|
|
1215
|
+
ref: taskRef,
|
|
1216
|
+
checksum_algorithm: "sha256",
|
|
1217
|
+
checksum: sha256Content(fs.readFileSync(taskFile)),
|
|
1218
|
+
});
|
|
1219
|
+
for (const [index, attempt] of (task.attempts || []).entries()) {
|
|
1220
|
+
const receipt = stableValue({
|
|
1221
|
+
task_id: task.task_id,
|
|
1222
|
+
attempt_id: attempt.attempt_id,
|
|
1223
|
+
number: attempt.number,
|
|
1224
|
+
status: attempt.status,
|
|
1225
|
+
started_at: attempt.started_at,
|
|
1226
|
+
finished_at: attempt.finished_at,
|
|
1227
|
+
phase_count: Array.isArray(attempt.phases) ? attempt.phases.length : 0,
|
|
1228
|
+
});
|
|
1229
|
+
attemptReceipts.push({
|
|
1230
|
+
...receipt,
|
|
1231
|
+
ref: `${taskRef}#/attempts/${index}`,
|
|
1232
|
+
checksum_algorithm: "sha256",
|
|
1233
|
+
receipt_checksum: sha256Content(JSON.stringify(stableValue(attempt))),
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return { task_summaries: taskSummaries, attempt_receipts: attemptReceipts };
|
|
1238
|
+
}
|
|
1239
|
+
|
|
449
1240
|
function writeCheckpoint(runtime, task) {
|
|
450
1241
|
const stamp = now().replaceAll(":", "-");
|
|
451
1242
|
atomicWrite(path.join(runtime, "checkpoints", task.task_id, `${stamp}.json`), task);
|
|
452
1243
|
}
|
|
453
1244
|
|
|
454
1245
|
function writeResume(runtime, task, reconciliation = null) {
|
|
1246
|
+
const attemptSummary = summarizeAttempts(task);
|
|
455
1247
|
const lines = [
|
|
456
1248
|
`# Task Recovery: ${task.task_id}`,
|
|
457
1249
|
"",
|
|
@@ -459,6 +1251,9 @@ function writeResume(runtime, task, reconciliation = null) {
|
|
|
459
1251
|
"## Recorded State", "", `- Status: ${task.status}`, `- Node: ${task.node}`, `- Skill: ${task.skill}`,
|
|
460
1252
|
`- Sequence: ${task.sequence || "not recorded"}`,
|
|
461
1253
|
`- Dependencies: ${task.depends_on.length ? task.depends_on.join(", ") : "none"}`,
|
|
1254
|
+
`- Attempts: ${attemptSummary.total}`,
|
|
1255
|
+
`- Current attempt: ${attemptSummary.current_attempt_id || "not started"}`,
|
|
1256
|
+
`- Current phase: ${attemptSummary.current_phase || "not started"}`,
|
|
462
1257
|
`- Next action owner: ${task.next_action_owner}`,
|
|
463
1258
|
`- Branch: ${task.branch || "not recorded"}`, `- Worktree: ${task.worktree || "not recorded"}`, "",
|
|
464
1259
|
"## Completed Work And Evidence", "",
|
|
@@ -473,6 +1268,7 @@ function writeResume(runtime, task, reconciliation = null) {
|
|
|
473
1268
|
lines.push("## Reconciled Workspace And Git State", "", "```json", JSON.stringify(reconciliation, null, 2), "```", "");
|
|
474
1269
|
}
|
|
475
1270
|
const file = path.join(runtime, "resumes", `${task.task_id}.md`);
|
|
1271
|
+
if (stageWrite(file, `${lines.join("\n")}\n`)) return;
|
|
476
1272
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
477
1273
|
fs.writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
|
|
478
1274
|
}
|
|
@@ -483,13 +1279,26 @@ function gitState(cwd) {
|
|
|
483
1279
|
if (root.status !== 0) return { repository: false };
|
|
484
1280
|
const branch = run(["branch", "--show-current"]);
|
|
485
1281
|
const head = run(["rev-parse", "HEAD"]);
|
|
486
|
-
const status = run(["status", "--porcelain=v1"]);
|
|
1282
|
+
const status = run(["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
1283
|
+
const statusFields = status.stdout.split("\0");
|
|
1284
|
+
const dirtyPaths = [];
|
|
1285
|
+
for (let index = 0; index < statusFields.length; index += 1) {
|
|
1286
|
+
const record = statusFields[index];
|
|
1287
|
+
if (!record) continue;
|
|
1288
|
+
const state = record.slice(0, 2);
|
|
1289
|
+
dirtyPaths.push(record.slice(3).replaceAll("\\", "/"));
|
|
1290
|
+
if (state.includes("R") || state.includes("C")) {
|
|
1291
|
+
const source = statusFields[index + 1];
|
|
1292
|
+
if (source) dirtyPaths.push(source.replaceAll("\\", "/"));
|
|
1293
|
+
index += 1;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
487
1296
|
return {
|
|
488
1297
|
repository: true,
|
|
489
1298
|
root: root.stdout.trim(),
|
|
490
1299
|
branch: branch.stdout.trim(),
|
|
491
1300
|
head: head.status === 0 ? head.stdout.trim() : null,
|
|
492
|
-
dirty_paths:
|
|
1301
|
+
dirty_paths: [...new Set(dirtyPaths)].sort((left, right) => left.localeCompare(right)),
|
|
493
1302
|
};
|
|
494
1303
|
}
|
|
495
1304
|
|
|
@@ -507,6 +1316,19 @@ function readRuntimeTasks(runtime) {
|
|
|
507
1316
|
.map((name) => normalizeTask(JSON.parse(fs.readFileSync(path.join(tasksDir, name), "utf8"))));
|
|
508
1317
|
}
|
|
509
1318
|
|
|
1319
|
+
function readReconciledTasks(runtime, workflowRoot, stateRoot) {
|
|
1320
|
+
const localById = new Map(readRuntimeTasks(runtime).map((task) => [task.task_id, task]));
|
|
1321
|
+
const sharedById = new Map(readSharedTasks(workflowRoot, stateRoot).map((task) => [task.task_id, task]));
|
|
1322
|
+
const taskIds = [...new Set([...localById.keys(), ...sharedById.keys()])].sort();
|
|
1323
|
+
const tasks = taskIds.map((taskId) => {
|
|
1324
|
+
const local = localById.get(taskId) || null;
|
|
1325
|
+
const shared = sharedById.get(taskId) || null;
|
|
1326
|
+
if (local && shared) assertTaskSourcesMatch(local, shared);
|
|
1327
|
+
return local || shared;
|
|
1328
|
+
});
|
|
1329
|
+
return { localById, sharedById, tasks };
|
|
1330
|
+
}
|
|
1331
|
+
|
|
510
1332
|
function dependencyState(task, tasksById) {
|
|
511
1333
|
const missing = [];
|
|
512
1334
|
const incomplete = [];
|
|
@@ -526,19 +1348,24 @@ function summarizeIteration(tasks, iterationId) {
|
|
|
526
1348
|
const completed = iterationTasks.filter((task) => task.status === "completed");
|
|
527
1349
|
const closed = iterationTasks.filter((task) => CLOSED_STATUS.has(task.status));
|
|
528
1350
|
const active = iterationTasks.filter((task) => RESUMABLE_STATUS.has(task.status));
|
|
1351
|
+
const retryable = iterationTasks.filter((task) => task.status === "failed");
|
|
529
1352
|
const queued = iterationTasks.filter((task) => task.status === "queued");
|
|
530
1353
|
const ready = queued.filter((task) => dependencyState(task, tasksById).satisfied);
|
|
531
1354
|
const waiting = queued
|
|
532
1355
|
.filter((task) => !ready.includes(task))
|
|
533
1356
|
.map((task) => ({ task_id: task.task_id, ...dependencyState(task, tasksById) }));
|
|
534
|
-
const recommended =
|
|
1357
|
+
const recommended = active.sort(compareTasks)[0]
|
|
1358
|
+
|| retryable.sort(compareTasks)[0]
|
|
1359
|
+
|| ready.sort(compareTasks)[0]
|
|
1360
|
+
|| null;
|
|
535
1361
|
return {
|
|
536
1362
|
iteration_id: iterationId,
|
|
537
1363
|
total_tasks: iterationTasks.length,
|
|
538
1364
|
completed_tasks: completed.length,
|
|
539
1365
|
closed_tasks: closed.length,
|
|
540
|
-
remaining_tasks: iterationTasks.
|
|
1366
|
+
remaining_tasks: iterationTasks.filter((task) => !RECOVERY_COMPLETE_STATUS.has(task.status)).length,
|
|
541
1367
|
active_task_ids: active.map((task) => task.task_id),
|
|
1368
|
+
retryable_task_ids: retryable.map((task) => task.task_id),
|
|
542
1369
|
queued_task_ids: queued.map((task) => task.task_id),
|
|
543
1370
|
ready_task_ids: ready.map((task) => task.task_id),
|
|
544
1371
|
waiting_tasks: waiting,
|
|
@@ -552,6 +1379,7 @@ function summarizeIteration(tasks, iterationId) {
|
|
|
552
1379
|
function commandInit(args) {
|
|
553
1380
|
const taskId = sanitizeId(requireValue(args, "task-id"));
|
|
554
1381
|
const iterationId = sanitizeId(requireValue(args, "iteration-id"));
|
|
1382
|
+
const node = assertNotLegacyDevelopmentNode(requireValue(args, "node"));
|
|
555
1383
|
const { projectRoot, stateRoot, workflowRoot, runtime } = statePaths(args);
|
|
556
1384
|
return withTaskLock(runtime, `version.${iterationId}`, () => withTaskLock(runtime, taskId, () => {
|
|
557
1385
|
if (fs.existsSync(archivedTaskIndexFile(runtime, taskId))) {
|
|
@@ -561,32 +1389,83 @@ function commandInit(args) {
|
|
|
561
1389
|
throw new Error(`Version is already archived and cannot accept new tasks: ${iterationId}`);
|
|
562
1390
|
}
|
|
563
1391
|
const file = taskFile(runtime, taskId);
|
|
564
|
-
const
|
|
1392
|
+
const local = fs.existsSync(file)
|
|
565
1393
|
? normalizeTask(JSON.parse(fs.readFileSync(file, "utf8")))
|
|
566
1394
|
: null;
|
|
1395
|
+
const shared = readSharedTasks(workflowRoot, stateRoot).find((item) => item.task_id === taskId) || null;
|
|
1396
|
+
if (local && shared) assertTaskSourcesMatch(local, shared);
|
|
1397
|
+
const existing = local || shared;
|
|
567
1398
|
if (existing && existing.iteration_id !== iterationId) {
|
|
568
1399
|
throw new Error(`Task ${taskId} already belongs to iteration ${existing.iteration_id}`);
|
|
569
1400
|
}
|
|
570
|
-
const shared = readSharedTasks(workflowRoot, stateRoot).find((item) => item.task_id === taskId);
|
|
571
|
-
if (existing && shared) assertTaskSourcesMatch(existing, shared);
|
|
572
1401
|
if (existing) assertWaitStateTransfer(existing, args, "init");
|
|
573
|
-
const status = args.status || "
|
|
1402
|
+
const status = args.status || "queued";
|
|
1403
|
+
if (!VALID_STATUS.has(status)) throw new Error(`Invalid status: ${status}`);
|
|
1404
|
+
if (CLOSED_STATUS.has(status)) {
|
|
1405
|
+
throw new Error(`init cannot set closed status ${status}; initialize, activate, and finish the task instead.`);
|
|
1406
|
+
}
|
|
1407
|
+
if (status !== "queued") {
|
|
1408
|
+
throw new Error(`init creates queued tasks only, not ${status}; use activate to start the task.`);
|
|
1409
|
+
}
|
|
574
1410
|
const dependsOn = (args["depends-on"] || existing?.depends_on || []).map(sanitizeId);
|
|
575
1411
|
if (dependsOn.includes(taskId)) throw new Error(`Task cannot depend on itself: ${taskId}`);
|
|
576
|
-
const
|
|
577
|
-
const
|
|
1412
|
+
const responsibility = args.responsibility || existing?.responsibility || requireValue(args, "skill");
|
|
1413
|
+
const contractFields = {
|
|
578
1414
|
task_id: taskId,
|
|
579
1415
|
iteration_id: iterationId,
|
|
580
|
-
node
|
|
1416
|
+
node,
|
|
581
1417
|
skill: requireValue(args, "skill"),
|
|
582
|
-
status,
|
|
583
|
-
sequence: parseSequence(args.sequence ?? existing?.sequence),
|
|
584
|
-
depends_on: dependsOn,
|
|
585
|
-
attempt: existing ? Number(existing.attempt || 0) + 1 : 1,
|
|
586
1418
|
goal: requireValue(args, "goal"),
|
|
1419
|
+
responsibility,
|
|
587
1420
|
scope: args.scope || existing?.scope || [],
|
|
1421
|
+
depends_on: dependsOn,
|
|
1422
|
+
acceptance: args.acceptance || existing?.acceptance || [],
|
|
1423
|
+
};
|
|
1424
|
+
const candidateContract = normalizeTask({
|
|
1425
|
+
...(existing || {}),
|
|
1426
|
+
...contractFields,
|
|
1427
|
+
next_action_owner: existing?.next_action_owner || "agent",
|
|
1428
|
+
});
|
|
1429
|
+
if (existing) {
|
|
1430
|
+
const changed = logicalContractChanges(existing, candidateContract);
|
|
1431
|
+
if (changed.length) {
|
|
1432
|
+
throw new Error(
|
|
1433
|
+
`Logical task contract changed for ${taskId}: ${changed.join(", ")}. Use a new task ID and link it with --derived-from or --supersedes.`,
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
return { ...existing, idempotent: true };
|
|
1437
|
+
}
|
|
1438
|
+
const knownTasks = readReconciledTasks(runtime, workflowRoot, stateRoot).tasks;
|
|
1439
|
+
const duplicateContract = knownTasks.find((task) => logicalContractChanges(task, candidateContract).length === 0);
|
|
1440
|
+
if (duplicateContract) {
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
`Task ${taskId} has the same logical contract as ${duplicateContract.task_id}; reuse ${duplicateContract.task_id}.`,
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
const derivedFrom = args["derived-from"] && args["derived-from"] !== true
|
|
1446
|
+
? sanitizeId(args["derived-from"])
|
|
1447
|
+
: null;
|
|
1448
|
+
const supersedes = args.supersedes && args.supersedes !== true ? sanitizeId(args.supersedes) : null;
|
|
1449
|
+
if (derivedFrom && supersedes) throw new Error("Use only one of --derived-from or --supersedes.");
|
|
1450
|
+
if (derivedFrom === taskId || supersedes === taskId) throw new Error(`Task ${taskId} cannot relate to itself.`);
|
|
1451
|
+
const relationTarget = derivedFrom || supersedes;
|
|
1452
|
+
if (relationTarget) {
|
|
1453
|
+
const known = knownTasks.some((task) => task.task_id === relationTarget)
|
|
1454
|
+
|| fs.existsSync(archivedTaskIndexFile(runtime, relationTarget));
|
|
1455
|
+
if (!known) throw new Error(`Related task not found: ${relationTarget}`);
|
|
1456
|
+
}
|
|
1457
|
+
const timestamp = now();
|
|
1458
|
+
const task = {
|
|
1459
|
+
...contractFields,
|
|
1460
|
+
status,
|
|
1461
|
+
sequence: parseSequence(args.sequence ?? existing?.sequence),
|
|
1462
|
+
attempt: 0,
|
|
1463
|
+
attempts: [],
|
|
1464
|
+
change_sets: [],
|
|
588
1465
|
input_artifacts: args["input-artifact"] || existing?.input_artifacts || [],
|
|
589
1466
|
standards: args.standard || existing?.standards || [],
|
|
1467
|
+
derived_from: derivedFrom,
|
|
1468
|
+
supersedes,
|
|
590
1469
|
branch: args.branch || existing?.branch || null,
|
|
591
1470
|
worktree: path.resolve(args.worktree || projectRoot),
|
|
592
1471
|
state_root: stateRoot,
|
|
@@ -598,19 +1477,19 @@ function commandInit(args) {
|
|
|
598
1477
|
next_action_owner: parseNextActionOwner(args["next-action-owner"], existing?.next_action_owner),
|
|
599
1478
|
evidence: existing?.evidence || [],
|
|
600
1479
|
created_at: existing?.created_at || timestamp,
|
|
601
|
-
started_at:
|
|
1480
|
+
started_at: null,
|
|
602
1481
|
updated_at: timestamp,
|
|
603
|
-
finished_at:
|
|
1482
|
+
finished_at: null,
|
|
604
1483
|
};
|
|
605
|
-
if (!VALID_STATUS.has(task.status)) throw new Error(`Invalid status: ${task.status}`);
|
|
606
1484
|
writeSharedTask(runtime, workflowRoot, task);
|
|
607
1485
|
writeResume(runtime, task);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1486
|
+
clearTaskMarker(task);
|
|
1487
|
+
appendEvent(runtime, taskId, "task-initialized", {
|
|
1488
|
+
status: task.status,
|
|
1489
|
+
attempt: task.attempt,
|
|
1490
|
+
derived_from: task.derived_from,
|
|
1491
|
+
supersedes: task.supersedes,
|
|
1492
|
+
});
|
|
614
1493
|
return task;
|
|
615
1494
|
}));
|
|
616
1495
|
}
|
|
@@ -634,7 +1513,14 @@ function commandActivate(args) {
|
|
|
634
1513
|
const currentSources = taskSources(runtime, workflowRoot, stateRoot, taskId);
|
|
635
1514
|
const task = currentSources.task;
|
|
636
1515
|
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
637
|
-
|
|
1516
|
+
const newAttempt = Boolean(args["new-attempt"]);
|
|
1517
|
+
if (task.status === "failed" && !newAttempt) {
|
|
1518
|
+
throw new Error(`Task ${taskId} failed; retry the same logical task with --new-attempt.`);
|
|
1519
|
+
}
|
|
1520
|
+
if (newAttempt && task.status !== "failed") {
|
|
1521
|
+
throw new Error(`--new-attempt is only valid when retrying a failed logical task; ${taskId} is ${task.status}.`);
|
|
1522
|
+
}
|
|
1523
|
+
if (!new Set(["queued", "preparing", "checkpointed", "blocked", "interrupted", "failed"]).has(task.status)) {
|
|
638
1524
|
throw new Error(`Task ${taskId} cannot be activated from status ${task.status}`);
|
|
639
1525
|
}
|
|
640
1526
|
const sharedTasks = readSharedTasks(workflowRoot, stateRoot);
|
|
@@ -660,11 +1546,20 @@ function commandActivate(args) {
|
|
|
660
1546
|
if (task.next_action_owner === "agent" && requestedOwner && requestedOwner !== "agent") {
|
|
661
1547
|
throw new Error("activate --next-action-owner may only be agent; use checkpoint or finish to enter a wait state.");
|
|
662
1548
|
}
|
|
663
|
-
const
|
|
1549
|
+
const timestamp = now();
|
|
664
1550
|
task.status = status;
|
|
665
|
-
|
|
666
|
-
task.
|
|
667
|
-
|
|
1551
|
+
let attempt = currentAttempt(task);
|
|
1552
|
+
if (!attempt || newAttempt) attempt = startAttempt(task, args.phase, timestamp);
|
|
1553
|
+
else {
|
|
1554
|
+
attempt.status = "active";
|
|
1555
|
+
attempt.finished_at = null;
|
|
1556
|
+
const phase = enterAttemptPhase(attempt, args.phase, timestamp);
|
|
1557
|
+
phase.status = "active";
|
|
1558
|
+
phase.finished_at = null;
|
|
1559
|
+
}
|
|
1560
|
+
task.attempt = attempt.number;
|
|
1561
|
+
task.started_at ||= timestamp;
|
|
1562
|
+
task.updated_at = timestamp;
|
|
668
1563
|
task.finished_at = null;
|
|
669
1564
|
task.blockers = [];
|
|
670
1565
|
if (args["next-action"] && args["next-action"] !== true) task.next_action = args["next-action"];
|
|
@@ -676,7 +1571,13 @@ function commandActivate(args) {
|
|
|
676
1571
|
iteration_id: task.iteration_id,
|
|
677
1572
|
state_root: task.state_root,
|
|
678
1573
|
});
|
|
679
|
-
appendEvent(runtime, taskId, "task-activated", {
|
|
1574
|
+
appendEvent(runtime, taskId, "task-activated", {
|
|
1575
|
+
status,
|
|
1576
|
+
attempt: task.attempt,
|
|
1577
|
+
attempt_id: attempt.attempt_id,
|
|
1578
|
+
phase: attempt.phases.at(-1)?.phase || null,
|
|
1579
|
+
new_attempt: newAttempt,
|
|
1580
|
+
});
|
|
680
1581
|
return task;
|
|
681
1582
|
}));
|
|
682
1583
|
}
|
|
@@ -691,12 +1592,21 @@ function commandUpdate(args, finish = false) {
|
|
|
691
1592
|
const task = readTask(runtime, taskId);
|
|
692
1593
|
const shared = readSharedTasks(workflowRoot, stateRoot).find((item) => item.task_id === taskId);
|
|
693
1594
|
if (shared) assertTaskSourcesMatch(task, shared);
|
|
1595
|
+
if (!RESUMABLE_STATUS.has(task.status)) {
|
|
1596
|
+
throw new Error(`Task ${taskId} is ${task.status}; activate it before checkpoint or finish.`);
|
|
1597
|
+
}
|
|
694
1598
|
const requestedOwner = assertWaitStateTransfer(task, args, finish ? "finish" : "checkpoint");
|
|
695
1599
|
const status = requireValue(args, "status");
|
|
696
1600
|
if (!VALID_STATUS.has(status)) throw new Error(`Invalid status: ${status}`);
|
|
697
1601
|
if (finish && !FINISH_STATUS.has(status)) {
|
|
698
1602
|
throw new Error(`finish requires one of: ${[...FINISH_STATUS].join(", ")}`);
|
|
699
1603
|
}
|
|
1604
|
+
if (!finish && CLOSED_STATUS.has(status)) {
|
|
1605
|
+
throw new Error(`checkpoint cannot set closed status ${status}; use finish instead.`);
|
|
1606
|
+
}
|
|
1607
|
+
if (!finish && status === "queued") {
|
|
1608
|
+
throw new Error("checkpoint cannot return an activated task to queued; queued tasks must be started through activate.");
|
|
1609
|
+
}
|
|
700
1610
|
task.status = status;
|
|
701
1611
|
task.next_action = requireValue(args, "next-action");
|
|
702
1612
|
task.next_action_owner = requestedOwner || task.next_action_owner;
|
|
@@ -708,16 +1618,54 @@ function commandUpdate(args, finish = false) {
|
|
|
708
1618
|
for (const output of args["actual-output"] || []) {
|
|
709
1619
|
if (!task.actual_outputs.includes(output)) task.actual_outputs.push(output);
|
|
710
1620
|
}
|
|
711
|
-
|
|
712
|
-
|
|
1621
|
+
const timestamp = now();
|
|
1622
|
+
const attempt = recordAttemptProgress(task, args, status, finish, timestamp, workflowRoot);
|
|
1623
|
+
if (finish && status === "completed") assertNoOpenBlockingFindings(task);
|
|
1624
|
+
task.updated_at = timestamp;
|
|
1625
|
+
task.finished_at = finish ? timestamp : null;
|
|
713
1626
|
writeSharedTask(runtime, workflowRoot, task);
|
|
714
1627
|
writeResume(runtime, task);
|
|
715
|
-
appendEvent(runtime, taskId, finish ? "task-finished" : "task-checkpointed", {
|
|
1628
|
+
appendEvent(runtime, taskId, finish ? "task-finished" : "task-checkpointed", {
|
|
1629
|
+
status,
|
|
1630
|
+
attempt: attempt.number,
|
|
1631
|
+
attempt_id: attempt.attempt_id,
|
|
1632
|
+
phase: attempt.phases.at(-1)?.phase || null,
|
|
1633
|
+
});
|
|
716
1634
|
if (finish && CLOSED_STATUS.has(status)) clearTaskMarker(task);
|
|
717
1635
|
return task;
|
|
718
1636
|
});
|
|
719
1637
|
}
|
|
720
1638
|
|
|
1639
|
+
function commandInspect(args) {
|
|
1640
|
+
const { runtime, workflowRoot, stateRoot } = statePaths(args);
|
|
1641
|
+
const requestedTask = args["task-id"] ? sanitizeId(args["task-id"]) : null;
|
|
1642
|
+
const requestedIteration = args["iteration-id"] ? sanitizeId(args["iteration-id"]) : null;
|
|
1643
|
+
const includeClosed = Boolean(args["include-closed"]);
|
|
1644
|
+
const { localById, sharedById, tasks: allTasks } = readReconciledTasks(runtime, workflowRoot, stateRoot);
|
|
1645
|
+
if (requestedTask && !allTasks.some((task) => task.task_id === requestedTask)) {
|
|
1646
|
+
throw new Error(`Task not found: ${requestedTask}`);
|
|
1647
|
+
}
|
|
1648
|
+
const tasksById = new Map(allTasks.map((task) => [task.task_id, task]));
|
|
1649
|
+
const selected = allTasks
|
|
1650
|
+
.filter((task) => !requestedTask || task.task_id === requestedTask)
|
|
1651
|
+
.filter((task) => !requestedIteration || task.iteration_id === requestedIteration)
|
|
1652
|
+
.filter((task) => includeClosed || !CLOSED_STATUS.has(task.status))
|
|
1653
|
+
.sort(compareTasks);
|
|
1654
|
+
const iterationIds = [...new Set(selected.map((task) => task.iteration_id))].sort();
|
|
1655
|
+
return {
|
|
1656
|
+
read_only: true,
|
|
1657
|
+
tasks: selected.map((task) => ({
|
|
1658
|
+
task,
|
|
1659
|
+
source: localById.has(task.task_id) && sharedById.has(task.task_id)
|
|
1660
|
+
? "local-and-shared"
|
|
1661
|
+
: localById.has(task.task_id) ? "local" : "shared",
|
|
1662
|
+
dependency_state: dependencyState(task, tasksById),
|
|
1663
|
+
attempt_summary: summarizeAttempts(task),
|
|
1664
|
+
})),
|
|
1665
|
+
iterations: iterationIds.map((iterationId) => summarizeIteration(allTasks, iterationId)),
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
721
1669
|
function commandArchiveVersion(args) {
|
|
722
1670
|
const { runtime, stateRoot, workflowRoot } = statePaths(args);
|
|
723
1671
|
const iterationId = sanitizeId(requireValue(args, "iteration-id"));
|
|
@@ -748,6 +1696,7 @@ function commandArchiveVersion(args) {
|
|
|
748
1696
|
.map((name) => normalizeTask(JSON.parse(fs.readFileSync(path.join(tasksDir, name), "utf8"))))
|
|
749
1697
|
.filter((task) => task.iteration_id === iterationId);
|
|
750
1698
|
if (!tasks.length) throw new Error(`No tasks found for version: ${iterationId}`);
|
|
1699
|
+
for (const task of tasks) assertNotLegacyDevelopmentNode(task.node);
|
|
751
1700
|
const sharedById = new Map(readSharedTasks(workflowRoot, stateRoot).map((task) => [task.task_id, task]));
|
|
752
1701
|
for (const task of tasks) {
|
|
753
1702
|
const shared = sharedById.get(task.task_id);
|
|
@@ -758,6 +1707,9 @@ function commandArchiveVersion(args) {
|
|
|
758
1707
|
if (notClosed.length) {
|
|
759
1708
|
throw new Error(`Version ${iterationId} is not closed; active tasks: ${notClosed.map((task) => `${task.task_id}:${task.status}`).join(", ")}`);
|
|
760
1709
|
}
|
|
1710
|
+
const formalReferences = formalVersionArchiveReferences(
|
|
1711
|
+
stateRoot, workflowRoot, iterationId, candidateId,
|
|
1712
|
+
);
|
|
761
1713
|
|
|
762
1714
|
const archivedAt = now();
|
|
763
1715
|
const writeRuntimeArchive = !Boolean(args["portable-only"]);
|
|
@@ -767,6 +1719,7 @@ function commandArchiveVersion(args) {
|
|
|
767
1719
|
}
|
|
768
1720
|
const archivedTasks = tasks.map((task) =>
|
|
769
1721
|
copyTaskToPortableArchive(stateRoot, workflowRoot, task, portableArchiveDirectory, archivedAt));
|
|
1722
|
+
const taskReferences = archivedTaskReferences(stateRoot, archivedTasks);
|
|
770
1723
|
const relativeArchive = path.relative(stateRoot, portableArchiveDirectory).replaceAll("\\", "/");
|
|
771
1724
|
const manifest = {
|
|
772
1725
|
iteration_id: iterationId,
|
|
@@ -777,6 +1730,13 @@ function commandArchiveVersion(args) {
|
|
|
777
1730
|
task_count: archivedTasks.length,
|
|
778
1731
|
archived_at: archivedAt,
|
|
779
1732
|
archive_path: relativeArchive,
|
|
1733
|
+
references: {
|
|
1734
|
+
...taskReferences,
|
|
1735
|
+
history_anchor: formalReferences?.history_anchor || null,
|
|
1736
|
+
scope: formalReferences?.scope || null,
|
|
1737
|
+
candidate: formalReferences?.candidate || null,
|
|
1738
|
+
gates: formalReferences?.gates || [],
|
|
1739
|
+
},
|
|
780
1740
|
tasks: archivedTasks.map((task) => ({
|
|
781
1741
|
task_id: task.task_id,
|
|
782
1742
|
node: task.node,
|
|
@@ -816,11 +1776,12 @@ function commandRecover(args) {
|
|
|
816
1776
|
const includeClosed = Boolean(args["include-closed"]);
|
|
817
1777
|
const localTasks = readRuntimeTasks(runtime);
|
|
818
1778
|
const localById = new Map(localTasks.map((task) => [task.task_id, task]));
|
|
1779
|
+
const missingLocalTaskIds = new Set();
|
|
819
1780
|
for (const shared of readSharedTasks(workflowRoot, stateRoot)) {
|
|
820
1781
|
const local = localById.get(shared.task_id);
|
|
821
1782
|
if (!local) {
|
|
822
1783
|
localById.set(shared.task_id, shared);
|
|
823
|
-
if (shared.next_action_owner === "agent")
|
|
1784
|
+
if (shared.next_action_owner === "agent") missingLocalTaskIds.add(shared.task_id);
|
|
824
1785
|
} else {
|
|
825
1786
|
assertTaskSourcesMatch(local, shared);
|
|
826
1787
|
}
|
|
@@ -831,10 +1792,15 @@ function commandRecover(args) {
|
|
|
831
1792
|
? allTasks.filter((task) => task.iteration_id === requestedIteration)
|
|
832
1793
|
: allTasks;
|
|
833
1794
|
const iterationIds = [...new Set(selectedTasks.map((task) => task.iteration_id))].sort();
|
|
1795
|
+
const reportableTasks = selectedTasks
|
|
1796
|
+
.filter((task) => !requested || task.task_id === requested)
|
|
1797
|
+
.filter((task) => includeClosed || !RECOVERY_COMPLETE_STATUS.has(task.status));
|
|
1798
|
+
for (const task of reportableTasks) assertNotLegacyDevelopmentNode(task.node);
|
|
1799
|
+
for (const task of reportableTasks) {
|
|
1800
|
+
if (missingLocalTaskIds.has(task.task_id)) atomicWrite(taskFile(runtime, task.task_id), task);
|
|
1801
|
+
}
|
|
834
1802
|
const reports = [];
|
|
835
|
-
for (const task of
|
|
836
|
-
if (requested && task.task_id !== requested) continue;
|
|
837
|
-
if (!includeClosed && CLOSED_STATUS.has(task.status)) continue;
|
|
1803
|
+
for (const task of reportableTasks.sort(compareTasks)) {
|
|
838
1804
|
const heartbeatFile = path.join(runtime, "heartbeats", `${task.task_id}.json`);
|
|
839
1805
|
const heartbeat = fs.existsSync(heartbeatFile) ? JSON.parse(fs.readFileSync(heartbeatFile, "utf8")) : null;
|
|
840
1806
|
const latest = new Date(heartbeat?.updated_at || task.updated_at).getTime();
|
|
@@ -886,6 +1852,8 @@ function commandPublish(args) {
|
|
|
886
1852
|
const sharedFile = sharedTaskFile(workflowRoot, taskId);
|
|
887
1853
|
const checkpointDirectory = path.join(workflowRoot, "control", "checkpoints", taskId);
|
|
888
1854
|
if (!fs.existsSync(sharedFile)) throw new Error(`Shared task state not found: ${taskId}`);
|
|
1855
|
+
const shared = JSON.parse(fs.readFileSync(sharedFile, "utf8"));
|
|
1856
|
+
assertNotLegacyDevelopmentNode(shared.node);
|
|
889
1857
|
|
|
890
1858
|
const repository = gitState(stateRoot);
|
|
891
1859
|
if (!repository.repository || path.resolve(repository.root).toLowerCase() !== path.resolve(stateRoot).toLowerCase()) {
|
|
@@ -926,7 +1894,6 @@ function commandPublish(args) {
|
|
|
926
1894
|
if (upstream.status === 0 && upstream.stdout.trim() !== `origin/${expectedBranch}`) {
|
|
927
1895
|
throw new Error(`Workflow upstream must be origin/${expectedBranch}, found ${upstream.stdout.trim()}.`);
|
|
928
1896
|
}
|
|
929
|
-
const shared = JSON.parse(fs.readFileSync(sharedFile, "utf8"));
|
|
930
1897
|
const workflowDocumentRoot = path.join(stateRoot, ".workflow");
|
|
931
1898
|
const taskOwnedArtifacts = [...(shared.actual_outputs || []), ...(shared.evidence || [])]
|
|
932
1899
|
.map((value) => String(value).replaceAll("\\", "/"))
|
|
@@ -1007,6 +1974,32 @@ function taskEvolutionContext(args, command) {
|
|
|
1007
1974
|
};
|
|
1008
1975
|
}
|
|
1009
1976
|
|
|
1977
|
+
function assertWritableEvolutionTarget(args, command) {
|
|
1978
|
+
if (!new Set(["init", "activate", "checkpoint", "finish", "archive-version"]).has(command)) return;
|
|
1979
|
+
if (command === "init") {
|
|
1980
|
+
const node = args.node && args.node !== true ? String(args.node) : null;
|
|
1981
|
+
if (node) assertNotLegacyDevelopmentNode(node);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const { runtime, workflowRoot, stateRoot } = statePaths(args);
|
|
1985
|
+
if (command === "archive-version") {
|
|
1986
|
+
const iterationId = args["iteration-id"] && args["iteration-id"] !== true
|
|
1987
|
+
? sanitizeId(args["iteration-id"])
|
|
1988
|
+
: null;
|
|
1989
|
+
const tasksDir = path.join(runtime, "tasks");
|
|
1990
|
+
if (!iterationId || !fs.existsSync(tasksDir)) return;
|
|
1991
|
+
for (const name of fs.readdirSync(tasksDir).filter((item) => item.endsWith(".json"))) {
|
|
1992
|
+
const task = normalizeTask(JSON.parse(fs.readFileSync(path.join(tasksDir, name), "utf8")));
|
|
1993
|
+
if (task.iteration_id === iterationId) assertNotLegacyDevelopmentNode(task.node);
|
|
1994
|
+
}
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const taskId = args["task-id"] && args["task-id"] !== true ? sanitizeId(args["task-id"]) : null;
|
|
1998
|
+
if (!taskId) return;
|
|
1999
|
+
const task = taskSources(runtime, workflowRoot, stateRoot, taskId).task;
|
|
2000
|
+
if (task) assertNotLegacyDevelopmentNode(task.node);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1010
2003
|
function dispatchTaskEvolution(context, phase, result = null, error = null) {
|
|
1011
2004
|
if (!context) return { decision: "pass" };
|
|
1012
2005
|
const identity = context.taskId || context.iterationId || "unspecified";
|
|
@@ -1049,24 +2042,66 @@ function dispatchTaskEvolution(context, phase, result = null, error = null) {
|
|
|
1049
2042
|
|
|
1050
2043
|
const args = parseArgs(process.argv.slice(2));
|
|
1051
2044
|
const command = args._[0];
|
|
1052
|
-
|
|
2045
|
+
let evolutionContext = null;
|
|
1053
2046
|
try {
|
|
2047
|
+
assertWritableEvolutionTarget(args, command);
|
|
2048
|
+
evolutionContext = taskEvolutionContext(args, command);
|
|
1054
2049
|
const before = dispatchTaskEvolution(evolutionContext, "before");
|
|
1055
2050
|
if (before.decision === "block") {
|
|
1056
2051
|
throw new Error(`Evolution Hook blocked ${command}: ${(before.messages || []).join("; ") || "no rationale"}`);
|
|
1057
2052
|
}
|
|
2053
|
+
let managerTransactionDescriptor = null;
|
|
2054
|
+
if (args["manager-transaction"]) {
|
|
2055
|
+
if (!new Set(["init", "activate", "checkpoint", "finish"]).has(command)) {
|
|
2056
|
+
throw new Error("Manager task transaction is limited to task mutation commands");
|
|
2057
|
+
}
|
|
2058
|
+
try {
|
|
2059
|
+
managerTransactionDescriptor = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
2060
|
+
} catch {
|
|
2061
|
+
throw new Error("Manager task transaction descriptor must be valid JSON");
|
|
2062
|
+
}
|
|
2063
|
+
activeWriteTransaction = { entries: new Map() };
|
|
2064
|
+
}
|
|
1058
2065
|
let result;
|
|
1059
2066
|
if (command === "init") result = commandInit(args);
|
|
1060
2067
|
else if (command === "activate") result = commandActivate(args);
|
|
1061
2068
|
else if (command === "checkpoint") result = commandUpdate(args, false);
|
|
1062
2069
|
else if (command === "finish") result = commandUpdate(args, true);
|
|
2070
|
+
else if (command === "inspect") result = commandInspect(args);
|
|
1063
2071
|
else if (command === "archive-version") result = commandArchiveVersion(args);
|
|
1064
2072
|
else if (command === "recover") result = commandRecover(args);
|
|
1065
2073
|
else if (command === "publish") result = commandPublish(args);
|
|
1066
|
-
else throw new Error("Use init, activate, checkpoint, finish, archive-version, recover, or publish.");
|
|
2074
|
+
else throw new Error("Use init, activate, checkpoint, finish, inspect, archive-version, recover, or publish.");
|
|
2075
|
+
if (managerTransactionDescriptor) {
|
|
2076
|
+
const managerRuntime = path.join(
|
|
2077
|
+
path.resolve(args["state-root"] || args.root || findProjectRoot()),
|
|
2078
|
+
".workflow", "tools", "manager", "workflow-manager-transaction.mjs",
|
|
2079
|
+
);
|
|
2080
|
+
if (!fs.existsSync(managerRuntime)) {
|
|
2081
|
+
throw new Error(`Workflow Manager transaction runtime is missing: ${managerRuntime}`);
|
|
2082
|
+
}
|
|
2083
|
+
const { prepareWorkflowManagerTaskTransactionWrites } = await import(pathToFileURL(managerRuntime).href);
|
|
2084
|
+
const prepared = prepareWorkflowManagerTaskTransactionWrites(
|
|
2085
|
+
path.resolve(args["state-root"] || args.root || findProjectRoot()),
|
|
2086
|
+
managerTransactionDescriptor,
|
|
2087
|
+
result,
|
|
2088
|
+
);
|
|
2089
|
+
for (const write of prepared.writes) stageWrite(write.file, write.content);
|
|
2090
|
+
commitWriteTransaction();
|
|
2091
|
+
result = {
|
|
2092
|
+
...result,
|
|
2093
|
+
manager_transaction: {
|
|
2094
|
+
outcome: prepared.outcome,
|
|
2095
|
+
result: prepared.result,
|
|
2096
|
+
receipt: prepared.receipt,
|
|
2097
|
+
event: prepared.event,
|
|
2098
|
+
},
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
1067
2101
|
dispatchTaskEvolution(evolutionContext, "after", result);
|
|
1068
2102
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1069
2103
|
} catch (error) {
|
|
2104
|
+
activeWriteTransaction = null;
|
|
1070
2105
|
dispatchTaskEvolution(evolutionContext, "failed", null, error);
|
|
1071
2106
|
process.stderr.write(`${error.message}\n`);
|
|
1072
2107
|
process.exitCode = 1;
|