@xulthekl/team-flow 0.22.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.agents/skills +1 -0
- package/.claude/always/phase-guard.md +3 -0
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +31 -0
- package/.codex-plugin/plugin.json +45 -0
- package/.codexignore +6 -0
- package/.cursor-plugin/marketplace.json +17 -0
- package/.cursor-plugin/plugin.json +31 -0
- package/.github/copilot-instructions.md +15 -0
- package/.github/dependabot.yml +10 -0
- package/.github/plugin/marketplace.json +42 -0
- package/.github/workflows/ci.yml +98 -0
- package/.github/workflows/hol-plugin-scanner.yml +17 -0
- package/.opencode/INSTALL.md +61 -0
- package/.opencode/plugins/team-flow.js +70 -0
- package/AGENTS.md +280 -0
- package/CHANGELOG.md +635 -0
- package/CONTRIBUTING.md +115 -0
- package/GEMINI.md +14 -0
- package/HANDOFF.md +219 -0
- package/INSTALL.md +810 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/SECURITY.md +37 -0
- package/agents/bug-investigator.md +180 -0
- package/agents/change-split-auditor.md +257 -0
- package/agents/code-reviewer.md +171 -0
- package/agents/cross-change-consistency-checker.md +215 -0
- package/agents/prd-completeness-reviewer.md +197 -0
- package/agents/prototype-builder.md +230 -0
- package/agents/prototype-env-scout.md +156 -0
- package/agents/prototype-reviewer.md +226 -0
- package/assets/icon.svg +6 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +5 -0
- package/dist/parsing/change-parser.d.ts +12 -0
- package/dist/parsing/change-parser.js +54 -0
- package/dist/parsing/requirement-blocks.d.ts +31 -0
- package/dist/parsing/requirement-blocks.js +192 -0
- package/dist/schema/base.d.ts +7 -0
- package/dist/schema/base.js +1 -0
- package/dist/schema/change.d.ts +25 -0
- package/dist/schema/change.js +1 -0
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/spec.d.ts +11 -0
- package/dist/schema/spec.js +1 -0
- package/dist/validation/constants.d.ts +42 -0
- package/dist/validation/constants.js +42 -0
- package/dist/validation/tokenizer.d.ts +17 -0
- package/dist/validation/tokenizer.js +173 -0
- package/dist/validation/types.d.ts +40 -0
- package/dist/validation/types.js +1 -0
- package/dist/validation/validator.d.ts +21 -0
- package/dist/validation/validator.js +489 -0
- package/docs/README_en.md +358 -0
- package/docs/artifact-contract.md +96 -0
- package/docs/decision-points.md +88 -0
- package/docs/e2e-integration-design.md +207 -0
- package/docs/examples/README.md +25 -0
- package/docs/examples/add-dark-mode/README.md +13 -0
- package/docs/examples/add-dark-mode/design.md +49 -0
- package/docs/examples/add-dark-mode/execution-contract.md +56 -0
- package/docs/examples/add-dark-mode/proposal.md +46 -0
- package/docs/examples/add-dark-mode/specs/ui-theme/spec.md +35 -0
- package/docs/examples/add-dark-mode/tasks.md +26 -0
- package/docs/examples/refactor-auth-boundary/README.md +15 -0
- package/docs/examples/refactor-auth-boundary/design.md +41 -0
- package/docs/examples/refactor-auth-boundary/execution-contract.md +61 -0
- package/docs/examples/refactor-auth-boundary/proposal.md +47 -0
- package/docs/examples/refactor-auth-boundary/specs/auth-boundary/spec.md +45 -0
- package/docs/examples/refactor-auth-boundary/tasks.md +25 -0
- package/docs/plans/2026-07-07-fix-batch-design.md +68 -0
- package/docs/plans/2026-07-20-001-refactor-skill-collaboration-protocol-plan.md +128 -0
- package/docs/platform-matrix.md +45 -0
- package/docs/prototype-design-research.md +206 -0
- package/docs/release-checklist.md +96 -0
- package/docs/showcase.html +699 -0
- package/docs/skill-rename-v0.8.0.md +49 -0
- package/docs/state-machine.md +162 -0
- package/gemini-extension.json +6 -0
- package/hooks/hooks-cursor.json +8 -0
- package/hooks/hooks.json +26 -0
- package/hooks/pre-tool-use-guard +116 -0
- package/hooks/session-start +14 -0
- package/llms.txt +51 -0
- package/package.json +48 -0
- package/plugin.json +27 -0
- package/prd/v1/plan.md +443 -0
- package/prd/v1/prd.md +580 -0
- package/scripts/check-update.mjs +91 -0
- package/scripts/check-version-consistency.mjs +345 -0
- package/scripts/ensure-branch.mjs +71 -0
- package/scripts/get-config +31 -0
- package/scripts/guard/checks/artifacts-exist.mjs +36 -0
- package/scripts/guard/checks/contract-current.mjs +33 -0
- package/scripts/guard/checks/contract-fresh.mjs +17 -0
- package/scripts/guard/checks/dp-gate-passed.mjs +35 -0
- package/scripts/guard/checks/dp3-approved.mjs +22 -0
- package/scripts/guard/checks/execution-plan-ready.mjs +40 -0
- package/scripts/guard/checks/execution-reviews-passed.mjs +36 -0
- package/scripts/guard/checks/schema-valid.mjs +63 -0
- package/scripts/guard/checks/specs-merged.mjs +51 -0
- package/scripts/guard/checks/tasks-complete.mjs +31 -0
- package/scripts/guard/checks/tests-passing.mjs +31 -0
- package/scripts/guard/design-token-guard.mjs +237 -0
- package/scripts/guard/guard.mjs +205 -0
- package/scripts/infer-workflow.mjs +136 -0
- package/scripts/install-amazon-q.mjs +9 -0
- package/scripts/install-cline.mjs +9 -0
- package/scripts/install-continue.mjs +9 -0
- package/scripts/install-cursor.mjs +265 -0
- package/scripts/install-git-hooks.mjs +82 -0
- package/scripts/install-kiro.mjs +9 -0
- package/scripts/install-pi.mjs +9 -0
- package/scripts/install-qoder.mjs +9 -0
- package/scripts/install-qwen.mjs +9 -0
- package/scripts/install-roocode.mjs +9 -0
- package/scripts/install-windsurf.mjs +9 -0
- package/scripts/install-zcode.mjs +265 -0
- package/scripts/lib/cmd-audit.mjs +127 -0
- package/scripts/lib/cmd-checkpoint.mjs +118 -0
- package/scripts/lib/cmd-config.mjs +88 -0
- package/scripts/lib/cmd-doctor.mjs +267 -0
- package/scripts/lib/cmd-execution.mjs +206 -0
- package/scripts/lib/cmd-handoff.mjs +94 -0
- package/scripts/lib/cmd-inject.mjs +310 -0
- package/scripts/lib/cmd-install-amazon-q.mjs +11 -0
- package/scripts/lib/cmd-install-cline.mjs +11 -0
- package/scripts/lib/cmd-install-continue.mjs +11 -0
- package/scripts/lib/cmd-install-cursor.mjs +14 -0
- package/scripts/lib/cmd-install-kiro.mjs +11 -0
- package/scripts/lib/cmd-install-pi.mjs +11 -0
- package/scripts/lib/cmd-install-qoder.mjs +11 -0
- package/scripts/lib/cmd-install-qwen.mjs +11 -0
- package/scripts/lib/cmd-install-roocode.mjs +11 -0
- package/scripts/lib/cmd-install-windsurf.mjs +11 -0
- package/scripts/lib/cmd-install-workbuddy.mjs +359 -0
- package/scripts/lib/cmd-install-zcode.mjs +14 -0
- package/scripts/lib/cmd-isolate.mjs +37 -0
- package/scripts/lib/cmd-list.mjs +71 -0
- package/scripts/lib/cmd-runtime.mjs +85 -0
- package/scripts/lib/cmd-solutions.mjs +71 -0
- package/scripts/lib/cmd-state.mjs +231 -0
- package/scripts/lib/cmd-sync.mjs +102 -0
- package/scripts/lib/cmd-validate.mjs +105 -0
- package/scripts/lib/cmd-version.mjs +148 -0
- package/scripts/lib/config-loader.mjs +107 -0
- package/scripts/lib/execution-plan.mjs +492 -0
- package/scripts/lib/execution-recommendation.mjs +221 -0
- package/scripts/lib/hash.mjs +74 -0
- package/scripts/lib/install.mjs +267 -0
- package/scripts/lib/platform-runtime-inventory.mjs +27 -0
- package/scripts/lib/platforms.mjs +165 -0
- package/scripts/lib/prototype-sync.mjs +192 -0
- package/scripts/lib/sdd-overlay.mjs +262 -0
- package/scripts/lib/shell-quote.mjs +4 -0
- package/scripts/lib/solutions-capture.mjs +102 -0
- package/scripts/lib/solutions-index-gen.mjs +112 -0
- package/scripts/lib/solutions-inject.mjs +77 -0
- package/scripts/lib/solutions-promote.mjs +145 -0
- package/scripts/lib/spec-paths.mjs +85 -0
- package/scripts/lib/state-loader.mjs +154 -0
- package/scripts/lint/lint-skills.mjs +149 -0
- package/scripts/lint/rules/behavior-consistency.mjs +56 -0
- package/scripts/lint/rules/dp-trigger-points.mjs +60 -0
- package/scripts/lint/rules/exception-handling.mjs +59 -0
- package/scripts/lint/rules/no-contradictory-instructions.mjs +67 -0
- package/scripts/lint/rules/no-redundant-checks.mjs +50 -0
- package/scripts/lint/rules/token-rules.mjs +122 -0
- package/scripts/review-package +44 -0
- package/scripts/task-brief +40 -0
- package/scripts/team-flow.mjs +164 -0
- package/scripts/token-baseline.mjs +158 -0
- package/scripts/validate-artifacts +96 -0
- package/scripts/verify-marketplace-release.mjs +59 -0
- package/skills/architecture-design/SKILL.md +140 -0
- package/skills/architecture-design/chapters/ch01-4a-domains.md +27 -0
- package/skills/architecture-design/chapters/ch02-change-cascade.md +27 -0
- package/skills/architecture-design/chapters/ch03-architecture-outputs.md +25 -0
- package/skills/architecture-design/chapters/ch04-entity-to-aggregate.md +27 -0
- package/skills/architecture-design/chapters/ch05-cqrs.md +32 -0
- package/skills/architecture-design/chapters/ch06-integration.md +54 -0
- package/skills/architecture-design/cheatsheet.md +51 -0
- package/skills/architecture-design/glossary.md +23 -0
- package/skills/architecture-design/patterns.md +32 -0
- package/skills/bug-investigator/SKILL.md +77 -0
- package/skills/build-executor/SKILL.md +163 -0
- package/skills/build-executor/implementer-prompt.md +162 -0
- package/skills/build-executor/references/execution-modes.md +86 -0
- package/skills/build-executor/task-reviewer-prompt.md +195 -0
- package/skills/ce-brainstorm/SKILL.md +415 -0
- package/skills/ce-brainstorm/references/agents/slack-researcher.md +127 -0
- package/skills/ce-brainstorm/references/blindspot-pass.md +70 -0
- package/skills/ce-brainstorm/references/brainstorm-sections.md +285 -0
- package/skills/ce-brainstorm/references/handoff.md +168 -0
- package/skills/ce-brainstorm/references/html-rendering.md +634 -0
- package/skills/ce-brainstorm/references/markdown-rendering.md +241 -0
- package/skills/ce-brainstorm/references/model-tiers.md +9 -0
- package/skills/ce-brainstorm/references/prd-mapping.md +71 -0
- package/skills/ce-brainstorm/references/product-pressure-test.md +42 -0
- package/skills/ce-brainstorm/references/reasoning-elevation.md +63 -0
- package/skills/ce-brainstorm/references/settled-decisions.md +45 -0
- package/skills/ce-brainstorm/references/synthesis-summary.md +282 -0
- package/skills/ce-brainstorm/references/universal-brainstorming.md +73 -0
- package/skills/ce-brainstorm/references/verdict-routing.md +26 -0
- package/skills/ce-brainstorm/references/visual-probes.md +158 -0
- package/skills/ce-brainstorm/scripts/visual-probe-server.js +418 -0
- package/skills/ce-compound/SKILL.md +114 -0
- package/skills/ce-compound/assets/resolution-template.md +94 -0
- package/skills/ce-compound/references/agents/best-practices-researcher.md +115 -0
- package/skills/ce-compound/references/agents/data-integrity-guardian.md +68 -0
- package/skills/ce-compound/references/agents/framework-docs-researcher.md +93 -0
- package/skills/ce-compound/references/agents/pattern-recognition-specialist.md +55 -0
- package/skills/ce-compound/references/agents/performance-oracle.md +108 -0
- package/skills/ce-compound/references/agents/security-sentinel.md +91 -0
- package/skills/ce-compound/references/agents/session-historian.md +83 -0
- package/skills/ce-compound/references/concepts-vocabulary.md +78 -0
- package/skills/ce-compound/references/full-mode-workflow.md +413 -0
- package/skills/ce-compound/references/grounding-validation.md +84 -0
- package/skills/ce-compound/references/lightweight-mode.md +53 -0
- package/skills/ce-compound/references/output-standards.md +182 -0
- package/skills/ce-compound/references/promotion-rules.md +38 -0
- package/skills/ce-compound/references/schema.yaml +231 -0
- package/skills/ce-compound/references/three-tier-index.md +55 -0
- package/skills/ce-compound/references/v0.5-upgrade.md +32 -0
- package/skills/ce-compound/references/write-flow.md +36 -0
- package/skills/ce-compound/references/yaml-schema.md +118 -0
- package/skills/ce-compound/scripts/session-history/discover-sessions.sh +130 -0
- package/skills/ce-compound/scripts/session-history/extract-errors.py +254 -0
- package/skills/ce-compound/scripts/session-history/extract-metadata.py +456 -0
- package/skills/ce-compound/scripts/session-history/extract-skeleton.py +575 -0
- package/skills/ce-compound/scripts/validate-doc-claims.py +346 -0
- package/skills/ce-compound/scripts/validate-frontmatter.py +137 -0
- package/skills/ce-ideate/SKILL.md +402 -0
- package/skills/ce-ideate/references/agents/issue-intelligence-analyst.md +200 -0
- package/skills/ce-ideate/references/agents/learnings-researcher.md +247 -0
- package/skills/ce-ideate/references/agents/slack-researcher.md +127 -0
- package/skills/ce-ideate/references/agents/web-researcher.md +121 -0
- package/skills/ce-ideate/references/divergent-ideation.md +89 -0
- package/skills/ce-ideate/references/html-rendering.md +634 -0
- package/skills/ce-ideate/references/ideation-sections.md +191 -0
- package/skills/ce-ideate/references/markdown-rendering.md +238 -0
- package/skills/ce-ideate/references/post-ideation-workflow.md +167 -0
- package/skills/ce-ideate/references/universal-ideation.md +107 -0
- package/skills/ce-ideate/references/web-research-cache.md +56 -0
- package/skills/ce-plan/SKILL.md +137 -0
- package/skills/ce-plan/references/agents/agent-native-planning-strategist.md +62 -0
- package/skills/ce-plan/references/agents/architecture-strategist.md +46 -0
- package/skills/ce-plan/references/agents/best-practices-researcher.md +114 -0
- package/skills/ce-plan/references/agents/data-integrity-guardian.md +68 -0
- package/skills/ce-plan/references/agents/data-migration-reviewer.md +103 -0
- package/skills/ce-plan/references/agents/deployment-verification-agent.md +157 -0
- package/skills/ce-plan/references/agents/framework-docs-researcher.md +93 -0
- package/skills/ce-plan/references/agents/git-history-analyzer.md +40 -0
- package/skills/ce-plan/references/agents/learnings-researcher.md +247 -0
- package/skills/ce-plan/references/agents/pattern-recognition-specialist.md +55 -0
- package/skills/ce-plan/references/agents/performance-oracle.md +108 -0
- package/skills/ce-plan/references/agents/repo-research-analyst.md +258 -0
- package/skills/ce-plan/references/agents/security-sentinel.md +91 -0
- package/skills/ce-plan/references/agents/slack-researcher.md +127 -0
- package/skills/ce-plan/references/agents/spec-flow-analyzer.md +80 -0
- package/skills/ce-plan/references/agents/web-researcher.md +121 -0
- package/skills/ce-plan/references/approach-altitude.md +55 -0
- package/skills/ce-plan/references/change-splitting.md +65 -0
- package/skills/ce-plan/references/completion-contract.md +33 -0
- package/skills/ce-plan/references/core-principles.md +40 -0
- package/skills/ce-plan/references/deepening-workflow.md +263 -0
- package/skills/ce-plan/references/dependency-graph.md +36 -0
- package/skills/ce-plan/references/html-rendering.md +634 -0
- package/skills/ce-plan/references/intake-routing.md +189 -0
- package/skills/ce-plan/references/markdown-rendering.md +238 -0
- package/skills/ce-plan/references/plan-handoff.md +150 -0
- package/skills/ce-plan/references/plan-sections.md +427 -0
- package/skills/ce-plan/references/plan-structure.md +120 -0
- package/skills/ce-plan/references/planning-modes.md +69 -0
- package/skills/ce-plan/references/reasoning-elevation.md +63 -0
- package/skills/ce-plan/references/research-workflow.md +165 -0
- package/skills/ce-plan/references/review-and-write.md +127 -0
- package/skills/ce-plan/references/settled-decisions.md +45 -0
- package/skills/ce-plan/references/synthesis-summary.md +418 -0
- package/skills/ce-plan/references/universal-planning.md +168 -0
- package/skills/ce-proof/SKILL.md +346 -0
- package/skills/ce-strategy/SKILL.md +97 -0
- package/skills/ce-strategy/references/interview.md +143 -0
- package/skills/ce-strategy/references/strategy-template.md +89 -0
- package/skills/code-reviewer/SKILL.md +84 -0
- package/skills/code-reviewer/code-reviewer-prompt.md +195 -0
- package/skills/contract-builder/SKILL.md +72 -0
- package/skills/design-system/SKILL.md +75 -0
- package/skills/design-system/references/agents/design-system-architect.md +138 -0
- package/skills/design-system/references/creation-flow.md +170 -0
- package/skills/design-system/references/preview-template.html +319 -0
- package/skills/design-system/references/token-derivation.md +115 -0
- package/skills/design-system/references/variant-schema.md +60 -0
- package/skills/e2e/SKILL.md +56 -0
- package/skills/e2e/references/test-verifier.md +40 -0
- package/skills/need-explorer/SKILL.md +79 -0
- package/skills/prototype/SKILL.md +98 -0
- package/skills/prototype/references/agents/design-system-architect.md +122 -0
- package/skills/prototype/references/checklist.md +82 -0
- package/skills/prototype/references/craft/accessibility-baseline.md +71 -0
- package/skills/prototype/references/craft/anti-ai-slop.md +87 -0
- package/skills/prototype/references/craft/laws-of-ux.md +60 -0
- package/skills/prototype/references/craft/state-coverage.md +81 -0
- package/skills/prototype/references/craft/typography-hierarchy.md +50 -0
- package/skills/prototype/references/layouts.md +223 -0
- package/skills/prototype/references/orchestration-flow.md +101 -0
- package/skills/prototype/references/prototype-scaffold/assets/design-tokens.css +19 -0
- package/skills/prototype/references/prototype-scaffold/components/button.html +3 -0
- package/skills/prototype/references/prototype-scaffold/design-system.md +7 -0
- package/skills/prototype/references/prototype-scaffold/flow.md +11 -0
- package/skills/prototype/references/prototype-scaffold/index.html +20 -0
- package/skills/prototype/references/prototype-scaffold/pages/index.html +11 -0
- package/skills/prototype/references/template.html +396 -0
- package/skills/release-archivist/SKILL.md +166 -0
- package/skills/release-archivist/references/closing-procedures.md +81 -0
- package/skills/session-handoff/SKILL.md +110 -0
- package/skills/session-handoff/references/context-extraction.md +70 -0
- package/skills/session-handoff/references/handoff-template.md +90 -0
- package/skills/session-handoff/references/skill-recommendation.md +43 -0
- package/skills/spec-merger/SKILL.md +66 -0
- package/skills/spec-writer/SKILL.md +149 -0
- package/skills/workflow-bootstrap/SKILL.md +148 -0
- package/skills/workflow-bootstrap/references/agents/codebase-recon-analyst.md +121 -0
- package/skills/workflow-bootstrap/references/b1-reconnaissance.md +80 -0
- package/skills/workflow-bootstrap/scripts/recon-probe.sh +328 -0
- package/skills/workflow-feedback/SKILL.md +120 -0
- package/skills/workflow-feedback/references/category-guide.md +121 -0
- package/skills/workflow-feedback/references/feedback-template.md +73 -0
- package/skills/workflow-feedback/references/review-mode.md +92 -0
- package/skills/workflow-orchestrator/SKILL.md +94 -0
- package/skills/workflow-orchestrator/references/feedback-loops.md +56 -0
- package/skills/workflow-orchestrator/references/s1-path-router.md +81 -0
- package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +96 -0
- package/skills/workflow-orchestrator/references/s3-plan-pipeline.md +50 -0
- package/skills/workflow-orchestrator/references/s4-split-validate.md +108 -0
- package/skills/workflow-orchestrator/references/s5-monitoring.md +61 -0
- package/skills/workflow-orchestrator/references/state-model.md +185 -0
- package/skills/workflow-start/SKILL.md +147 -0
- package/skills/workflow-start/references/routing-rules.md +155 -0
- package/src/index.ts +16 -0
- package/src/parsing/change-parser.ts +79 -0
- package/src/parsing/requirement-blocks.ts +244 -0
- package/src/schema/base.ts +8 -0
- package/src/schema/change.ts +29 -0
- package/src/schema/index.ts +3 -0
- package/src/schema/spec.ts +12 -0
- package/src/validation/constants.ts +53 -0
- package/src/validation/tokenizer.ts +180 -0
- package/src/validation/types.ts +47 -0
- package/src/validation/validator.ts +552 -0
- package/templates/abandonment-summary.md +29 -0
- package/templates/design.md +37 -0
- package/templates/execution-contract.md +99 -0
- package/templates/plan.md +86 -0
- package/templates/prd.md +468 -0
- package/templates/proposal.md +37 -0
- package/templates/spec.md +31 -0
- package/templates/tasks.md +47 -0
- package/tests/e2e.test.mjs +266 -0
- package/tests/lib/cmd-audit.test.mjs +202 -0
- package/tests/lib/cmd-checkpoint.test.mjs +98 -0
- package/tests/lib/cmd-config.test.mjs +101 -0
- package/tests/lib/cmd-doctor.test.mjs +450 -0
- package/tests/lib/cmd-execution.test.mjs +647 -0
- package/tests/lib/cmd-handoff.test.mjs +137 -0
- package/tests/lib/cmd-inject.test.mjs +218 -0
- package/tests/lib/cmd-install-workbuddy.test.mjs +113 -0
- package/tests/lib/cmd-install-zcode.test.mjs +42 -0
- package/tests/lib/cmd-isolate.test.mjs +68 -0
- package/tests/lib/cmd-list.test.mjs +127 -0
- package/tests/lib/cmd-runtime.test.mjs +83 -0
- package/tests/lib/cmd-state-missing.test.mjs +65 -0
- package/tests/lib/cmd-state.test.mjs +365 -0
- package/tests/lib/cmd-sync-paths.test.mjs +84 -0
- package/tests/lib/cmd-validate-paths.test.mjs +90 -0
- package/tests/lib/config-loader.test.mjs +175 -0
- package/tests/lib/ensure-branch.test.mjs +59 -0
- package/tests/lib/execution-control-plane.test.mjs +168 -0
- package/tests/lib/execution-plan.test.mjs +474 -0
- package/tests/lib/guard-specs-merged.test.mjs +108 -0
- package/tests/lib/guard-tests-passing.test.mjs +112 -0
- package/tests/lib/guard-transitions.test.mjs +262 -0
- package/tests/lib/guard.test.mjs +611 -0
- package/tests/lib/hash.test.mjs +138 -0
- package/tests/lib/infer-workflow.test.mjs +161 -0
- package/tests/lib/install-git-hooks.test.mjs +20 -0
- package/tests/lib/marketplace-release-docs.test.mjs +31 -0
- package/tests/lib/minimality-discipline.test.mjs +37 -0
- package/tests/lib/model-profiles-docs.test.mjs +33 -0
- package/tests/lib/node20-compatibility.test.mjs +73 -0
- package/tests/lib/node20-test-entry.test.mjs +17 -0
- package/tests/lib/platform-runtime-distribution.test.mjs +150 -0
- package/tests/lib/raw-mode-smoke.test.mjs +42 -0
- package/tests/lib/sdd-overlay.test.mjs +118 -0
- package/tests/lib/spec-paths.test.mjs +80 -0
- package/tests/lib/state-loader.test.mjs +251 -0
- package/tests/lib/token-baseline.test.mjs +72 -0
- package/tests/lib/token-rules.test.mjs +94 -0
- package/tests/lib/verify-marketplace-release.test.mjs +128 -0
- package/tests/tsconfig.json +12 -0
- package/token-baseline.json +95 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Config loader for team-flow
|
|
2
|
+
// Loads team-flow.config.json and merges with built-in defaults.
|
|
3
|
+
// Lookup order: (1) projectRoot, (2) git root, (3) home directory.
|
|
4
|
+
|
|
5
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const DEFAULTS = {
|
|
10
|
+
artifacts: {
|
|
11
|
+
order: ['proposal', 'specs', 'design', 'tasks'],
|
|
12
|
+
skip: [],
|
|
13
|
+
},
|
|
14
|
+
execution: {
|
|
15
|
+
inlineThreshold: 3,
|
|
16
|
+
abandonmentReasonMinLength: 50,
|
|
17
|
+
defaultLanguage: 'auto',
|
|
18
|
+
},
|
|
19
|
+
verification: {
|
|
20
|
+
language: 'auto',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const MODEL_PROFILES = Object.freeze([
|
|
25
|
+
'mechanical', 'standard', 'strong', 'review',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function resolveModelProfile(config, profile) {
|
|
29
|
+
if (!MODEL_PROFILES.includes(profile)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Unknown model profile '${profile}'. Supported profiles: ${MODEL_PROFILES.join(', ')}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const models = config?.models;
|
|
36
|
+
if (!models || !Object.hasOwn(models, profile)) {
|
|
37
|
+
return { profile, model: null, configured: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const model = models[profile];
|
|
41
|
+
if (typeof model !== 'string' || model.trim().length === 0) {
|
|
42
|
+
throw new Error(`Invalid model profile: models.${profile} must be a non-empty string`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { profile, model, configured: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deepMerge(target, source) {
|
|
49
|
+
const result = { ...target };
|
|
50
|
+
for (const key of Object.keys(source)) {
|
|
51
|
+
if (
|
|
52
|
+
source[key] &&
|
|
53
|
+
typeof source[key] === 'object' &&
|
|
54
|
+
!Array.isArray(source[key]) &&
|
|
55
|
+
target[key] &&
|
|
56
|
+
typeof target[key] === 'object' &&
|
|
57
|
+
!Array.isArray(target[key])
|
|
58
|
+
) {
|
|
59
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
60
|
+
} else {
|
|
61
|
+
result[key] = source[key];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findConfigFile(startDir) {
|
|
68
|
+
// 1. Check startDir
|
|
69
|
+
const direct = join(startDir, 'team-flow.config.json');
|
|
70
|
+
if (existsSync(direct)) return direct;
|
|
71
|
+
|
|
72
|
+
// 2. Check git root
|
|
73
|
+
try {
|
|
74
|
+
const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
75
|
+
cwd: startDir,
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
78
|
+
}).trim();
|
|
79
|
+
const gitPath = join(gitRoot, 'team-flow.config.json');
|
|
80
|
+
if (existsSync(gitPath)) return gitPath;
|
|
81
|
+
} catch {
|
|
82
|
+
// Not a git repo — skip
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Check home directory
|
|
86
|
+
const homePath = join(process.env.HOME || '', 'team-flow.config.json');
|
|
87
|
+
if (existsSync(homePath)) return homePath;
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function loadConfig(projectRoot) {
|
|
93
|
+
const configPath = findConfigFile(projectRoot || process.cwd());
|
|
94
|
+
if (!configPath) return { ...DEFAULTS };
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
98
|
+
const userConfig = JSON.parse(raw);
|
|
99
|
+
return deepMerge(DEFAULTS, userConfig);
|
|
100
|
+
} catch {
|
|
101
|
+
return { ...DEFAULTS };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getDefaults() {
|
|
106
|
+
return JSON.parse(JSON.stringify(DEFAULTS));
|
|
107
|
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { computeArtifactsHash, computeContractHash } from './hash.mjs';
|
|
6
|
+
import { getOverlayPaths } from './sdd-overlay.mjs';
|
|
7
|
+
import { readState } from './state-loader.mjs';
|
|
8
|
+
|
|
9
|
+
export const EXECUTION_MODES = ['inline', 'batch-inline', 'sdd'];
|
|
10
|
+
|
|
11
|
+
const WAVE_STRATEGIES = new Set(['parallel', 'serial']);
|
|
12
|
+
const REVIEW_STATUSES = new Set(['pass', 'fail']);
|
|
13
|
+
|
|
14
|
+
export function createPlan(changeDir, input) {
|
|
15
|
+
const state = readState(changeDir);
|
|
16
|
+
const plan = {
|
|
17
|
+
mode: input?.mode,
|
|
18
|
+
source: input?.source,
|
|
19
|
+
rationale: input?.rationale,
|
|
20
|
+
waves: input?.waves,
|
|
21
|
+
artifacts_hash: computeArtifactsHash(changeDir),
|
|
22
|
+
contract_hash: computeContractHash(changeDir),
|
|
23
|
+
workflow: state.workflow,
|
|
24
|
+
revision: input?.revision ?? state.revision ?? 1,
|
|
25
|
+
};
|
|
26
|
+
if (input?.recommendation !== undefined) plan.recommendation = input.recommendation;
|
|
27
|
+
if (input?.recommendationReceipt !== undefined) plan.recommendation_receipt = input.recommendationReceipt;
|
|
28
|
+
if (input?.selection !== undefined) plan.selection = input.selection;
|
|
29
|
+
const failures = validateStructure(plan);
|
|
30
|
+
if (failures.length > 0) throw new Error(`Invalid execution plan: ${failures.join('; ')}`);
|
|
31
|
+
plan.hash = hashPlan(plan);
|
|
32
|
+
return plan;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readPlan(changeDir) {
|
|
36
|
+
const filePath = getOverlayPaths(changeDir).executionPlan;
|
|
37
|
+
if (!existsSync(filePath)) return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Unable to read execution plan: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function writePlan(changeDir, plan) {
|
|
46
|
+
const failures = validateStructure(plan);
|
|
47
|
+
const expectedHash = tryHashPlan(plan);
|
|
48
|
+
if (expectedHash === null) failures.push('execution plan content cannot be hashed');
|
|
49
|
+
else if (plan?.hash !== expectedHash) failures.push('execution plan content hash mismatch');
|
|
50
|
+
if (failures.length > 0) throw new Error(`Invalid execution plan: ${failures.join('; ')}`);
|
|
51
|
+
|
|
52
|
+
const paths = getOverlayPaths(changeDir);
|
|
53
|
+
mkdirSync(paths.root, { recursive: true });
|
|
54
|
+
const previousPlan = readPlan(changeDir);
|
|
55
|
+
if (previousPlan && (previousPlan.revision !== plan.revision || previousPlan.hash !== plan.hash)) {
|
|
56
|
+
// Review evidence is scoped to exactly one plan revision. Removing it on
|
|
57
|
+
// revision prevents an old wave ID from satisfying a changed plan.
|
|
58
|
+
rmSync(paths.reviews, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
atomicWrite(paths.executionPlan, `${JSON.stringify(plan, null, 2)}\n`);
|
|
61
|
+
writeExecutionPlanSummary(changeDir, plan);
|
|
62
|
+
return readPlan(changeDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validatePlan(changeDir, plan) {
|
|
66
|
+
const failures = validateStructure(plan);
|
|
67
|
+
const actualHash = tryHashPlan(plan);
|
|
68
|
+
if (actualHash === null) failures.push('execution plan content cannot be hashed');
|
|
69
|
+
else if (plan?.hash !== actualHash) failures.push('execution plan content hash mismatch');
|
|
70
|
+
|
|
71
|
+
const state = readState(changeDir);
|
|
72
|
+
if (state.execution_plan_hash !== plan?.hash) {
|
|
73
|
+
failures.push('execution plan summary does not match state');
|
|
74
|
+
}
|
|
75
|
+
if (state.execution_mode !== plan?.mode) {
|
|
76
|
+
failures.push('execution plan mode does not match state');
|
|
77
|
+
}
|
|
78
|
+
if (plan?.artifacts_hash !== computeArtifactsHash(changeDir)) {
|
|
79
|
+
failures.push('execution plan is stale: artifacts hash mismatch');
|
|
80
|
+
}
|
|
81
|
+
if (plan?.contract_hash !== computeContractHash(changeDir)) {
|
|
82
|
+
failures.push('execution plan is stale: contract hash mismatch');
|
|
83
|
+
}
|
|
84
|
+
if (plan?.workflow !== state.workflow) {
|
|
85
|
+
failures.push('execution plan workflow does not match state');
|
|
86
|
+
}
|
|
87
|
+
if (state.execution_plan_revision !== plan?.revision) {
|
|
88
|
+
failures.push('execution plan revision does not match state');
|
|
89
|
+
}
|
|
90
|
+
if (state.revision != null && plan?.revision !== state.revision) {
|
|
91
|
+
failures.push('execution plan revision does not match state');
|
|
92
|
+
}
|
|
93
|
+
if (plan?.workflow !== 'tweak') {
|
|
94
|
+
if (plan?.recommendation === undefined) failures.push('execution plan recommendation is required for full/hotfix');
|
|
95
|
+
if (plan?.recommendation_receipt === undefined) failures.push('execution plan recommendation receipt is required for full/hotfix');
|
|
96
|
+
if (plan?.selection === undefined) failures.push('execution plan selection is required for full/hotfix');
|
|
97
|
+
}
|
|
98
|
+
if (plan?.recommendation_receipt !== undefined) {
|
|
99
|
+
if (plan.recommendation_receipt.artifacts_hash !== plan.artifacts_hash) failures.push('execution plan recommendation receipt artifacts hash does not match plan');
|
|
100
|
+
if (plan.recommendation_receipt.contract_hash !== plan.contract_hash) failures.push('execution plan recommendation receipt contract hash does not match plan');
|
|
101
|
+
if (plan.recommendation_receipt.workflow !== plan.workflow) failures.push('execution plan recommendation receipt workflow does not match plan');
|
|
102
|
+
if (stableJson(plan.recommendation_receipt.waves) !== stableJson(plan.waves)) failures.push('execution plan recommendation receipt waves do not match plan');
|
|
103
|
+
if (stableJson(plan.recommendation_receipt.recommendation) !== stableJson(plan.recommendation)) failures.push('execution plan recommendation receipt does not match plan recommendation');
|
|
104
|
+
const expectedRecommendationPlanRevision = plan.source === 'user-confirmed-revision'
|
|
105
|
+
? plan.revision - 1
|
|
106
|
+
: null;
|
|
107
|
+
if (plan.recommendation_receipt.execution_plan_revision_at_recommendation !== expectedRecommendationPlanRevision) {
|
|
108
|
+
failures.push('execution plan recommendation receipt does not match the immediately prior plan revision');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (plan?.selection !== undefined && plan?.recommendation?.recommendation?.mode) {
|
|
112
|
+
const followedRecommendation = plan.mode === plan.recommendation.recommendation.mode;
|
|
113
|
+
if (plan.selection.followed_recommendation !== followedRecommendation) failures.push('execution plan selection does not match recommended mode');
|
|
114
|
+
if (plan.selection.acknowledged_non_recommendation !== !followedRecommendation) failures.push('execution plan selection acknowledgement does not match recommended mode');
|
|
115
|
+
}
|
|
116
|
+
return { valid: failures.length === 0, failures, plan };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function recordReview(changeDir, waveId, receipt) {
|
|
120
|
+
const plan = readPlan(changeDir);
|
|
121
|
+
const validation = validatePlan(changeDir, plan);
|
|
122
|
+
if (!validation.valid) throw new Error(`Cannot record a review for an invalid execution plan: ${validation.failures.join('; ')}`);
|
|
123
|
+
const wave = Array.isArray(plan?.waves) && plan.waves.find(candidate => candidate?.id === waveId);
|
|
124
|
+
if (!wave) throw new Error(`Review receipt references unknown wave '${waveId}'`);
|
|
125
|
+
const blockedBy = blockedDependencies(changeDir, plan, wave);
|
|
126
|
+
if (blockedBy.length > 0) {
|
|
127
|
+
throw new Error(`Wave '${waveId}' cannot be reviewed before dependencies have passing receipts: ${blockedBy.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
if (!REVIEW_STATUSES.has(receipt?.status)) {
|
|
130
|
+
throw new Error("Review receipt status must be 'pass' or 'fail'");
|
|
131
|
+
}
|
|
132
|
+
for (const field of ['base', 'head']) requireText(receipt?.[field], `receipt.${field}`);
|
|
133
|
+
const report = validateReviewReportEvidence(changeDir, receipt?.report);
|
|
134
|
+
const { base, head } = validateReviewRange(changeDir, receipt.base, receipt.head);
|
|
135
|
+
|
|
136
|
+
const savedReceipt = {
|
|
137
|
+
status: receipt.status,
|
|
138
|
+
base,
|
|
139
|
+
head,
|
|
140
|
+
report,
|
|
141
|
+
plan_hash: plan.hash,
|
|
142
|
+
plan_revision: plan.revision,
|
|
143
|
+
recorded_at: new Date().toISOString(),
|
|
144
|
+
};
|
|
145
|
+
const paths = getOverlayPaths(changeDir);
|
|
146
|
+
mkdirSync(paths.reviews, { recursive: true });
|
|
147
|
+
atomicWrite(join(paths.reviews, `${safeFileName(waveId)}.json`), `${JSON.stringify(savedReceipt, null, 2)}\n`);
|
|
148
|
+
return savedReceipt;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Returns the current plan's receipt for one wave. Receipts from a previous
|
|
153
|
+
* revision/hash are never evidence for the current plan.
|
|
154
|
+
*/
|
|
155
|
+
export function readCurrentReview(changeDir, waveId, plan = readPlan(changeDir)) {
|
|
156
|
+
if (!plan) return null;
|
|
157
|
+
const filePath = join(getOverlayPaths(changeDir).reviews, `${safeFileName(waveId)}.json`);
|
|
158
|
+
if (!existsSync(filePath)) return null;
|
|
159
|
+
try {
|
|
160
|
+
const receipt = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
161
|
+
if (receipt?.plan_hash !== plan.hash || receipt?.plan_revision !== plan.revision) return null;
|
|
162
|
+
const range = validateReviewRange(changeDir, receipt?.base, receipt?.head);
|
|
163
|
+
if (receipt.base !== range.base || receipt.head !== range.head) return null;
|
|
164
|
+
// A passing receipt is current evidence only while its referenced report
|
|
165
|
+
// remains safe and readable. Recheck it here because reports can be
|
|
166
|
+
// deleted or replaced after the receipt was recorded.
|
|
167
|
+
if (receipt?.status === 'pass') validateReviewReportEvidence(changeDir, receipt.report);
|
|
168
|
+
return receipt;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Machine-readable execution status used by `tf execution show`. A wave is
|
|
176
|
+
* eligible when it has no current receipt, or its current receipt failed and
|
|
177
|
+
* is therefore retryable, and all declared dependencies have passing receipts.
|
|
178
|
+
*/
|
|
179
|
+
export function describeWaves(changeDir, plan = readPlan(changeDir)) {
|
|
180
|
+
if (!plan || !Array.isArray(plan.waves)) return [];
|
|
181
|
+
return plan.waves.map(wave => {
|
|
182
|
+
const receipt = readCurrentReview(changeDir, wave.id, plan);
|
|
183
|
+
const blockers = blockedDependencies(changeDir, plan, wave);
|
|
184
|
+
const retryable = receipt?.status === 'fail';
|
|
185
|
+
return {
|
|
186
|
+
id: wave.id,
|
|
187
|
+
strategy: wave.strategy,
|
|
188
|
+
tasks: wave.tasks,
|
|
189
|
+
depends_on: wave.depends_on,
|
|
190
|
+
eligible: (receipt === null || retryable) && blockers.length === 0,
|
|
191
|
+
retryable,
|
|
192
|
+
receipt,
|
|
193
|
+
blockers,
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateReviewReportEvidence(changeDir, report) {
|
|
199
|
+
requireText(report, 'receipt.report');
|
|
200
|
+
if (/[\p{Cc}\p{Zl}\p{Zp}]/u.test(report)) {
|
|
201
|
+
throw new Error('Review report evidence path is unsafe');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { changeRoot, reviewsDir } = getPhysicalReviewsDirectory(changeDir);
|
|
205
|
+
const reportPath = isAbsolute(report) ? resolve(report) : resolve(changeRoot, report);
|
|
206
|
+
|
|
207
|
+
let metadata;
|
|
208
|
+
try {
|
|
209
|
+
metadata = lstatSync(reportPath);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new Error(`Review report evidence cannot be read: ${error.message}`);
|
|
212
|
+
}
|
|
213
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
214
|
+
throw new Error('Review report evidence must be a regular file');
|
|
215
|
+
}
|
|
216
|
+
if (metadata.size === 0) {
|
|
217
|
+
throw new Error('Review report evidence must be non-empty');
|
|
218
|
+
}
|
|
219
|
+
const realReportPath = realpathSync(reportPath);
|
|
220
|
+
const realOverlayRelativePath = relative(reviewsDir, realReportPath);
|
|
221
|
+
if (realOverlayRelativePath === '' || realOverlayRelativePath === '..' || realOverlayRelativePath.startsWith(`..${sep}`) || isAbsolute(realOverlayRelativePath)) {
|
|
222
|
+
throw new Error('Review report evidence must resolve inside the change review overlay');
|
|
223
|
+
}
|
|
224
|
+
return relative(changeRoot, realReportPath);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getPhysicalReviewsDirectory(changeDir) {
|
|
228
|
+
let changeRoot;
|
|
229
|
+
try {
|
|
230
|
+
changeRoot = realpathSync(changeDir);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw new Error(`Review report evidence cannot resolve the change directory: ${error.message}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let directory = changeRoot;
|
|
236
|
+
for (const component of ['.superpowers', 'sdd', 'reviews']) {
|
|
237
|
+
directory = join(directory, component);
|
|
238
|
+
let metadata;
|
|
239
|
+
try {
|
|
240
|
+
metadata = lstatSync(directory);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw new Error(`Review report evidence cannot read the ${component} overlay directory: ${error.message}`);
|
|
243
|
+
}
|
|
244
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
245
|
+
throw new Error('Review report evidence requires physical .superpowers/sdd/reviews overlay directories');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { changeRoot, reviewsDir: directory };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function validateReviewRange(changeDir, base, head) {
|
|
252
|
+
const gitRoot = getGitRoot(changeDir);
|
|
253
|
+
const resolvedBase = resolveGitCommit(gitRoot, base, 'base');
|
|
254
|
+
const resolvedHead = resolveGitCommit(gitRoot, head, 'head');
|
|
255
|
+
try {
|
|
256
|
+
execFileSync('git', ['-C', gitRoot, 'merge-base', '--is-ancestor', resolvedBase, resolvedHead], {
|
|
257
|
+
stdio: 'ignore',
|
|
258
|
+
});
|
|
259
|
+
} catch {
|
|
260
|
+
throw new Error('Review receipt base must be an ancestor of head');
|
|
261
|
+
}
|
|
262
|
+
return { base: resolvedBase, head: resolvedHead };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getGitRoot(changeDir) {
|
|
266
|
+
try {
|
|
267
|
+
return execFileSync('git', ['-C', changeDir, 'rev-parse', '--show-toplevel'], {
|
|
268
|
+
encoding: 'utf8',
|
|
269
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
270
|
+
}).trim();
|
|
271
|
+
} catch {
|
|
272
|
+
throw new Error('Review receipts require the change directory to be inside a Git work tree');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resolveGitCommit(gitRoot, revision, field) {
|
|
277
|
+
try {
|
|
278
|
+
return execFileSync('git', ['-C', gitRoot, 'rev-parse', '--verify', `${revision}^{commit}`], {
|
|
279
|
+
encoding: 'utf8',
|
|
280
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
281
|
+
}).trim();
|
|
282
|
+
} catch {
|
|
283
|
+
throw new Error(`Review receipt ${field} must name an existing Git commit`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function blockedDependencies(changeDir, plan, wave) {
|
|
288
|
+
if (!Array.isArray(wave?.depends_on)) return [];
|
|
289
|
+
return wave.depends_on.filter(dependency => readCurrentReview(changeDir, dependency, plan)?.status !== 'pass');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function validateStructure(plan) {
|
|
293
|
+
const failures = [];
|
|
294
|
+
if (!isObject(plan)) return ['execution plan must be an object'];
|
|
295
|
+
if (!EXECUTION_MODES.includes(plan.mode)) failures.push('execution plan mode is invalid');
|
|
296
|
+
if (typeof plan.source !== 'string' || !plan.source.trim()) failures.push('execution plan source is required');
|
|
297
|
+
if (!isNonEmptyText(plan.rationale)) failures.push('execution plan rationale is required');
|
|
298
|
+
if (plan.recommendation !== undefined) {
|
|
299
|
+
if (!isObject(plan.recommendation)) {
|
|
300
|
+
failures.push('execution plan recommendation must be an object');
|
|
301
|
+
} else {
|
|
302
|
+
if (!isObject(plan.recommendation.recommendation) || !EXECUTION_MODES.includes(plan.recommendation.recommendation.mode)) {
|
|
303
|
+
failures.push('execution plan recommendation mode is invalid');
|
|
304
|
+
}
|
|
305
|
+
if (!Array.isArray(plan.recommendation.recommendation?.reasons) || plan.recommendation.recommendation.reasons.some(reason => !isNonEmptyText(reason))) {
|
|
306
|
+
failures.push('execution plan recommendation reasons must be non-empty strings');
|
|
307
|
+
}
|
|
308
|
+
if (!isObject(plan.recommendation.facts)) failures.push('execution plan recommendation facts must be an object');
|
|
309
|
+
if (!Array.isArray(plan.recommendation.available_modes) || plan.recommendation.available_modes.some(mode => !EXECUTION_MODES.includes(mode))) {
|
|
310
|
+
failures.push('execution plan recommendation available_modes are invalid');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (plan.recommendation_receipt !== undefined) {
|
|
315
|
+
if (!isObject(plan.recommendation_receipt)) {
|
|
316
|
+
failures.push('execution plan recommendation receipt must be an object');
|
|
317
|
+
} else {
|
|
318
|
+
if (!isObject(plan.recommendation_receipt.recommendation)) failures.push('execution plan recommendation receipt payload is required');
|
|
319
|
+
if (!Array.isArray(plan.recommendation_receipt.waves)) failures.push('execution plan recommendation receipt waves must be an array');
|
|
320
|
+
if (!isNullableHash(plan.recommendation_receipt.artifacts_hash)) failures.push('execution plan recommendation receipt artifacts hash is invalid');
|
|
321
|
+
if (!isNullableHash(plan.recommendation_receipt.contract_hash)) failures.push('execution plan recommendation receipt contract hash is invalid');
|
|
322
|
+
if (!isNonEmptyText(plan.recommendation_receipt.workflow)) failures.push('execution plan recommendation receipt workflow is invalid');
|
|
323
|
+
if (plan.recommendation_receipt.execution_plan_revision_at_recommendation !== null
|
|
324
|
+
&& (!Number.isInteger(plan.recommendation_receipt.execution_plan_revision_at_recommendation)
|
|
325
|
+
|| plan.recommendation_receipt.execution_plan_revision_at_recommendation < 1)) {
|
|
326
|
+
failures.push('execution plan recommendation receipt plan revision is invalid');
|
|
327
|
+
}
|
|
328
|
+
if (!isNonEmptyText(plan.recommendation_receipt.created_at)) failures.push('execution plan recommendation receipt timestamp is invalid');
|
|
329
|
+
if (typeof plan.recommendation_receipt.hash !== 'string' || !plan.recommendation_receipt.hash.startsWith('sha256:')) failures.push('execution plan recommendation receipt hash is invalid');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (plan.selection !== undefined) {
|
|
333
|
+
if (!isObject(plan.selection)) {
|
|
334
|
+
failures.push('execution plan selection must be an object');
|
|
335
|
+
} else {
|
|
336
|
+
if (plan.selection.confirmed !== true) failures.push('execution plan selection must be confirmed');
|
|
337
|
+
if (typeof plan.selection.followed_recommendation !== 'boolean') failures.push('execution plan selection must record recommendation alignment');
|
|
338
|
+
if (typeof plan.selection.acknowledged_non_recommendation !== 'boolean') failures.push('execution plan selection must record non-recommendation acknowledgement');
|
|
339
|
+
if (plan.selection.followed_recommendation === plan.selection.acknowledged_non_recommendation) {
|
|
340
|
+
failures.push('execution plan selection acknowledgement conflicts with recommendation alignment');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (!Array.isArray(plan.waves)) {
|
|
345
|
+
failures.push('execution plan waves must be an array');
|
|
346
|
+
return failures;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const ids = new Set();
|
|
350
|
+
const taskOwners = new Map();
|
|
351
|
+
for (const [index, wave] of plan.waves.entries()) {
|
|
352
|
+
const label = `wave ${index + 1}`;
|
|
353
|
+
if (!isObject(wave)) {
|
|
354
|
+
failures.push(`${label} must be an object`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (!isNonEmptyText(wave.id)) failures.push(`${label} id is required`);
|
|
358
|
+
else if (ids.has(wave.id)) failures.push(`duplicate wave id '${wave.id}'`);
|
|
359
|
+
else ids.add(wave.id);
|
|
360
|
+
if (!WAVE_STRATEGIES.has(wave.strategy)) failures.push(`${label} strategy is invalid`);
|
|
361
|
+
if (!Array.isArray(wave.tasks) || wave.tasks.length === 0) {
|
|
362
|
+
failures.push(`${label} must include at least one task`);
|
|
363
|
+
} else {
|
|
364
|
+
if (wave.tasks.some(task => !isNonEmptyText(task))) failures.push(`${label} tasks must be non-empty strings`);
|
|
365
|
+
for (const task of wave.tasks.filter(isNonEmptyText)) {
|
|
366
|
+
const priorWaveId = taskOwners.get(task);
|
|
367
|
+
if (priorWaveId !== undefined) {
|
|
368
|
+
failures.push(`duplicate task id '${task}' appears in waves '${priorWaveId}' and '${wave.id}'`);
|
|
369
|
+
} else {
|
|
370
|
+
taskOwners.set(task, wave.id);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (wave.strategy === 'parallel' && new Set(wave.tasks).size !== wave.tasks.length) {
|
|
374
|
+
failures.push(`parallel wave '${wave.id}' contains duplicate tasks`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (!Array.isArray(wave.depends_on)) failures.push(`${label} depends_on must be an array`);
|
|
378
|
+
else if (wave.depends_on.some(id => !isNonEmptyText(id))) failures.push(`${label} dependencies must be non-empty strings`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (const wave of plan.waves.filter(isObject)) {
|
|
382
|
+
if (!Array.isArray(wave.depends_on) || !isNonEmptyText(wave.id)) continue;
|
|
383
|
+
for (const dependency of wave.depends_on) {
|
|
384
|
+
if (dependency === wave.id) failures.push(`wave '${wave.id}' cannot depend on itself`);
|
|
385
|
+
else if (isNonEmptyText(dependency) && !ids.has(dependency)) {
|
|
386
|
+
failures.push(`wave '${wave.id}' depends on unknown wave '${dependency}'`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const canCheckCycles = plan.waves.every(wave => isObject(wave)
|
|
391
|
+
&& isNonEmptyText(wave.id) && Array.isArray(wave.depends_on));
|
|
392
|
+
if (canCheckCycles && !failures.some(failure => /duplicate wave id|unknown wave|cannot depend on itself/.test(failure))
|
|
393
|
+
&& hasDependencyCycle(plan.waves)) {
|
|
394
|
+
failures.push('execution plan waves contain a dependency cycle');
|
|
395
|
+
}
|
|
396
|
+
return failures;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function hasDependencyCycle(waves) {
|
|
400
|
+
const dependencies = new Map(waves.map(wave => [wave.id, wave.depends_on]));
|
|
401
|
+
const visiting = new Set();
|
|
402
|
+
const visited = new Set();
|
|
403
|
+
const visit = id => {
|
|
404
|
+
if (visiting.has(id)) return true;
|
|
405
|
+
if (visited.has(id)) return false;
|
|
406
|
+
visiting.add(id);
|
|
407
|
+
for (const dependency of dependencies.get(id) || []) {
|
|
408
|
+
if (visit(dependency)) return true;
|
|
409
|
+
}
|
|
410
|
+
visiting.delete(id);
|
|
411
|
+
visited.add(id);
|
|
412
|
+
return false;
|
|
413
|
+
};
|
|
414
|
+
return [...dependencies.keys()].some(visit);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function hashPlan(plan) {
|
|
418
|
+
const { hash, ...content } = plan;
|
|
419
|
+
return `sha256:${createHash('sha256').update(stableJson(content)).digest('hex')}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function tryHashPlan(plan) {
|
|
423
|
+
if (!isObject(plan)) return null;
|
|
424
|
+
try {
|
|
425
|
+
return hashPlan(plan);
|
|
426
|
+
} catch {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function stableJson(value, seen = new WeakSet()) {
|
|
432
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
433
|
+
if (seen.has(value)) throw new Error('circular plan data');
|
|
434
|
+
seen.add(value);
|
|
435
|
+
if (Array.isArray(value)) {
|
|
436
|
+
const result = `[${value.map(item => stableJson(item, seen)).join(',')}]`;
|
|
437
|
+
seen.delete(value);
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
const result = `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key], seen)}`).join(',')}}`;
|
|
441
|
+
seen.delete(value);
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function writeExecutionPlanSummary(changeDir, plan) {
|
|
446
|
+
const statePath = join(changeDir, '.team-flow.yaml');
|
|
447
|
+
const state = readState(changeDir);
|
|
448
|
+
const original = existsSync(statePath)
|
|
449
|
+
? readFileSync(statePath, 'utf8')
|
|
450
|
+
: `state: ${state.state}\nworkflow: ${state.workflow}\n`;
|
|
451
|
+
const content = [
|
|
452
|
+
['revision', plan.revision],
|
|
453
|
+
['execution_mode', plan.mode],
|
|
454
|
+
['execution_plan_hash', plan.hash],
|
|
455
|
+
['execution_plan_revision', plan.revision],
|
|
456
|
+
].reduce((current, [field, value]) => setStateField(current, field, value), original);
|
|
457
|
+
atomicWrite(statePath, content);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function setStateField(content, field, value) {
|
|
461
|
+
const line = `${field}: ${value}`;
|
|
462
|
+
const expression = new RegExp(`^${field}:\\s*.*$`, 'm');
|
|
463
|
+
return expression.test(content)
|
|
464
|
+
? content.replace(expression, line)
|
|
465
|
+
: `${content.replace(/\n*$/, '\n')}\n${line}\n`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function atomicWrite(targetPath, content) {
|
|
469
|
+
const tempPath = `${targetPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
470
|
+
writeFileSync(tempPath, content, 'utf8');
|
|
471
|
+
renameSync(tempPath, targetPath);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isObject(value) {
|
|
475
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function isNonEmptyText(value) {
|
|
479
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function isNullableHash(value) {
|
|
483
|
+
return value === null || (typeof value === 'string' && value.startsWith('sha256:'));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function requireText(value, field) {
|
|
487
|
+
if (!isNonEmptyText(value)) throw new Error(`${field} is required`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function safeFileName(value) {
|
|
491
|
+
return Buffer.from(value, 'utf8').toString('base64url');
|
|
492
|
+
}
|