@bonesofspring/ai-rules 0.2.20 → 0.2.21
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/CHANGELOG.md +28 -0
- package/README.md +17 -2
- package/bin/cli.js +83 -6
- package/package.json +5 -2
- package/presets/_shared/README.md +2 -1
- package/presets/_shared/assets/docs-specs/INDEX.md +4 -1
- package/presets/_shared/core/agent-team/agent-artifact-contracts.md +13 -0
- package/presets/_shared/core/agent-team/agent-task-metrics.md +1 -1
- package/presets/_shared/core/agent-team/agent-team-orchestrator.md +1 -3
- package/presets/_shared/core/meta/preset-pr-checklist.md +5 -1
- package/presets/_shared/core/meta/preset-token-budget.md +7 -3
- package/presets/_shared/core/meta/preset-twin-sync.md +2 -0
- package/presets/_shared/core/testing/testing-principles.md +70 -0
- package/presets/claude/android-kotlin/agents/README.md +2 -1
- package/presets/claude/android-kotlin/agents/feature-developer.md +1 -1
- package/presets/claude/android-kotlin/agents/openspec-author.md +163 -0
- package/presets/claude/android-kotlin/agents/task-analyst.md +1 -1
- package/presets/claude/android-kotlin/agents/task-router.md +5 -1
- package/presets/claude/android-kotlin/agents/tech-writer.md +4 -0
- package/presets/claude/android-kotlin/commands/README.md +0 -2
- package/presets/claude/android-kotlin/commands/task-continue.md +1 -1
- package/presets/claude/android-kotlin/commands/task.md +1 -5
- package/presets/claude/android-kotlin/commands/technical-retro.md +2 -2
- package/presets/claude/android-kotlin/hooks/chain-team-phases.sh +54 -2
- package/presets/claude/android-kotlin/rules/README.md +1 -0
- package/presets/claude/android-kotlin/rules/tooling-and-review/agent-team-orchestrator.md +7 -4
- package/presets/claude/android-kotlin/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/android-kotlin/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/go/README.md +1 -1
- package/presets/claude/go/agents/README.md +3 -2
- package/presets/claude/go/agents/openspec-author.md +163 -0
- package/presets/claude/go/agents/task-router.md +5 -1
- package/presets/claude/go/agents/tech-writer.md +4 -0
- package/presets/claude/go/commands/README.md +0 -2
- package/presets/claude/go/commands/task-continue.md +1 -1
- package/presets/claude/go/commands/task.md +1 -4
- package/presets/claude/go/hooks/chain-team-phases.sh +54 -3
- package/presets/claude/go/rules/README.md +1 -0
- package/presets/claude/go/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
- package/presets/claude/go/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/go/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/ios-swift/agents/README.md +3 -2
- package/presets/claude/ios-swift/agents/feature-developer.md +1 -1
- package/presets/claude/ios-swift/agents/openspec-author.md +163 -0
- package/presets/claude/ios-swift/agents/task-analyst.md +1 -1
- package/presets/claude/ios-swift/agents/task-router.md +5 -1
- package/presets/claude/ios-swift/agents/tech-writer.md +4 -0
- package/presets/claude/ios-swift/commands/README.md +0 -2
- package/presets/claude/ios-swift/commands/task-continue.md +1 -1
- package/presets/claude/ios-swift/commands/task.md +1 -5
- package/presets/claude/ios-swift/commands/technical-retro.md +2 -2
- package/presets/claude/ios-swift/hooks/chain-team-phases.sh +54 -2
- package/presets/claude/ios-swift/rules/README.md +1 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/agent-team-orchestrator.md +7 -4
- package/presets/claude/ios-swift/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/ios-swift/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/java/CLAUDE.md +1 -1
- package/presets/claude/java/agents/README.md +3 -2
- package/presets/claude/java/agents/openspec-author.md +163 -0
- package/presets/claude/java/agents/task-router.md +5 -1
- package/presets/claude/java/agents/tech-writer.md +4 -0
- package/presets/claude/java/commands/README.md +0 -2
- package/presets/claude/java/commands/task-continue.md +1 -1
- package/presets/claude/java/commands/task.md +1 -4
- package/presets/claude/java/hooks/chain-team-phases.sh +54 -3
- package/presets/claude/java/rules/README.md +1 -0
- package/presets/claude/java/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
- package/presets/claude/java/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/java/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/mcp-ts/agents/task-analyst.md +1 -1
- package/presets/claude/mcp-ts/commands/README.md +0 -2
- package/presets/claude/mcp-ts/commands/task-continue.md +1 -1
- package/presets/claude/mcp-ts/commands/task.md +1 -4
- package/presets/claude/mcp-ts/commands/technical-retro.md +2 -2
- package/presets/claude/mcp-ts/hooks/chain-team-phases.sh +40 -9
- package/presets/claude/mcp-ts/rules/README.md +1 -0
- package/presets/claude/mcp-ts/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
- package/presets/claude/mcp-ts/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/mcp-ts/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/next/README.md +1 -0
- package/presets/claude/next/agents/README.md +2 -2
- package/presets/claude/next/agents/build-verifier.md +11 -1
- package/presets/claude/next/agents/feature-developer.md +1 -1
- package/presets/claude/next/agents/openspec-author.md +163 -0
- package/presets/claude/next/agents/task-analyst.md +1 -1
- package/presets/claude/next/agents/task-router.md +5 -1
- package/presets/claude/next/agents/tech-writer.md +4 -0
- package/presets/claude/next/commands/README.md +0 -2
- package/presets/claude/next/commands/task-continue.md +1 -1
- package/presets/claude/next/commands/task.md +1 -4
- package/presets/claude/next/hooks/chain-team-phases.sh +54 -2
- package/presets/claude/next/rules/README.md +1 -0
- package/presets/claude/next/rules/testing/README.md +6 -0
- package/presets/claude/next/rules/testing/tests-e2e-structure.md +84 -48
- package/presets/claude/next/rules/testing/tests-unit.md +79 -50
- package/presets/claude/next/rules/tooling-and-review/agent-team-orchestrator.md +3 -4
- package/presets/claude/next/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/next/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/next/team/README.md +0 -3
- package/presets/claude/nuxt/agents/README.md +1 -1
- package/presets/claude/nuxt/agents/feature-developer.md +1 -1
- package/presets/claude/nuxt/agents/openspec-author.md +163 -0
- package/presets/claude/nuxt/agents/task-analyst.md +1 -1
- package/presets/claude/nuxt/agents/task-router.md +5 -1
- package/presets/claude/nuxt/agents/tech-writer.md +4 -0
- package/presets/claude/nuxt/commands/README.md +0 -2
- package/presets/claude/nuxt/commands/task-continue.md +1 -1
- package/presets/claude/nuxt/commands/task.md +1 -5
- package/presets/claude/nuxt/commands/technical-retro.md +2 -2
- package/presets/claude/nuxt/hooks/chain-team-phases.sh +54 -2
- package/presets/claude/nuxt/rules/README.md +1 -0
- package/presets/claude/nuxt/rules/testing/README.md +6 -0
- package/presets/claude/nuxt/rules/testing/tests-e2e-structure.md +86 -43
- package/presets/claude/nuxt/rules/testing/tests-unit.md +58 -45
- package/presets/claude/nuxt/rules/tooling-and-review/agent-team-orchestrator.md +8 -5
- package/presets/claude/nuxt/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/nuxt/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/nuxt/team/README.md +0 -3
- package/presets/claude/php-hexagonal/CLAUDE.md +1 -1
- package/presets/claude/php-hexagonal/agents/README.md +3 -2
- package/presets/claude/php-hexagonal/agents/openspec-author.md +163 -0
- package/presets/claude/php-hexagonal/agents/task-router.md +5 -1
- package/presets/claude/php-hexagonal/agents/tech-writer.md +4 -0
- package/presets/claude/php-hexagonal/commands/README.md +0 -2
- package/presets/claude/php-hexagonal/commands/task-continue.md +1 -1
- package/presets/claude/php-hexagonal/commands/task.md +1 -4
- package/presets/claude/php-hexagonal/hooks/chain-team-phases.sh +54 -3
- package/presets/claude/php-hexagonal/rules/README.md +1 -0
- package/presets/claude/php-hexagonal/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
- package/presets/claude/php-hexagonal/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/php-laravel/agents/README.md +3 -2
- package/presets/claude/php-laravel/agents/openspec-author.md +163 -0
- package/presets/claude/php-laravel/agents/task-router.md +5 -1
- package/presets/claude/php-laravel/agents/tech-writer.md +4 -0
- package/presets/claude/php-laravel/commands/README.md +0 -2
- package/presets/claude/php-laravel/commands/task-continue.md +1 -1
- package/presets/claude/php-laravel/commands/task.md +1 -4
- package/presets/claude/php-laravel/hooks/chain-team-phases.sh +54 -3
- package/presets/claude/php-laravel/rules/README.md +1 -0
- package/presets/claude/php-laravel/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
- package/presets/claude/php-laravel/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/php-laravel/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/svelte/agents/README.md +1 -1
- package/presets/claude/svelte/agents/feature-developer.md +1 -1
- package/presets/claude/svelte/agents/openspec-author.md +163 -0
- package/presets/claude/svelte/agents/task-analyst.md +1 -1
- package/presets/claude/svelte/agents/task-router.md +5 -1
- package/presets/claude/svelte/agents/tech-writer.md +4 -0
- package/presets/claude/svelte/commands/README.md +0 -2
- package/presets/claude/svelte/commands/task-continue.md +1 -1
- package/presets/claude/svelte/commands/task.md +1 -5
- package/presets/claude/svelte/commands/technical-retro.md +2 -2
- package/presets/claude/svelte/hooks/chain-team-phases.sh +54 -2
- package/presets/claude/svelte/rules/README.md +1 -0
- package/presets/claude/svelte/rules/testing/README.md +6 -0
- package/presets/claude/svelte/rules/testing/tests-e2e-structure.md +87 -45
- package/presets/claude/svelte/rules/testing/tests-unit.md +70 -23
- package/presets/claude/svelte/rules/tooling-and-review/agent-team-orchestrator.md +8 -5
- package/presets/claude/svelte/rules/tooling-and-review/ai-local-overlay.md +48 -0
- package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +5 -1
- package/presets/claude/svelte/rules/tooling-and-review/preset-token-budget.md +7 -3
- package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +2 -0
- package/presets/claude/svelte/team/README.md +0 -3
- package/presets/cursor/android-kotlin/agents/README.md +2 -1
- package/presets/cursor/android-kotlin/agents/feature-developer.md +1 -1
- package/presets/cursor/android-kotlin/agents/openspec-author.md +164 -0
- package/presets/cursor/android-kotlin/agents/task-analyst.md +1 -1
- package/presets/cursor/android-kotlin/agents/task-router.md +5 -1
- package/presets/cursor/android-kotlin/agents/tech-writer.md +4 -0
- package/presets/cursor/android-kotlin/commands/README.md +0 -2
- package/presets/cursor/android-kotlin/commands/task-continue.md +1 -1
- package/presets/cursor/android-kotlin/commands/task.md +1 -5
- package/presets/cursor/android-kotlin/commands/technical-retro.md +2 -2
- package/presets/cursor/android-kotlin/hooks/chain-team-phases.sh +54 -2
- package/presets/cursor/android-kotlin/rules/agent-team-orchestrator.mdc +7 -4
- package/presets/cursor/android-kotlin/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/android-kotlin/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/go/README.md +6 -6
- package/presets/cursor/go/agents/README.md +3 -2
- package/presets/cursor/go/agents/openspec-author.md +164 -0
- package/presets/cursor/go/agents/task-router.md +5 -1
- package/presets/cursor/go/agents/tech-writer.md +4 -0
- package/presets/cursor/go/commands/README.md +0 -2
- package/presets/cursor/go/commands/task-continue.md +1 -1
- package/presets/cursor/go/commands/task.md +1 -4
- package/presets/cursor/go/commands/technical-retro.md +2 -2
- package/presets/cursor/go/hooks/chain-team-phases.sh +54 -3
- package/presets/cursor/go/rules/agent-team-orchestrator.mdc +2 -4
- package/presets/cursor/go/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/go/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/go/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/go/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/ios-swift/README.md +6 -6
- package/presets/cursor/ios-swift/agents/README.md +3 -2
- package/presets/cursor/ios-swift/agents/feature-developer.md +1 -1
- package/presets/cursor/ios-swift/agents/openspec-author.md +164 -0
- package/presets/cursor/ios-swift/agents/task-analyst.md +1 -1
- package/presets/cursor/ios-swift/agents/task-router.md +5 -1
- package/presets/cursor/ios-swift/agents/tech-writer.md +4 -0
- package/presets/cursor/ios-swift/commands/README.md +1 -3
- package/presets/cursor/ios-swift/commands/task-continue.md +1 -1
- package/presets/cursor/ios-swift/commands/task.md +1 -5
- package/presets/cursor/ios-swift/commands/technical-retro.md +2 -2
- package/presets/cursor/ios-swift/hooks/chain-team-phases.sh +54 -2
- package/presets/cursor/ios-swift/rules/agent-team-orchestrator.mdc +7 -4
- package/presets/cursor/ios-swift/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/ios-swift/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/java/AGENTS.md +1 -1
- package/presets/cursor/java/agents/README.md +3 -2
- package/presets/cursor/java/agents/openspec-author.md +164 -0
- package/presets/cursor/java/agents/task-router.md +5 -1
- package/presets/cursor/java/agents/tech-writer.md +4 -0
- package/presets/cursor/java/commands/README.md +0 -2
- package/presets/cursor/java/commands/task-continue.md +1 -1
- package/presets/cursor/java/commands/task.md +1 -4
- package/presets/cursor/java/commands/technical-retro.md +2 -2
- package/presets/cursor/java/hooks/chain-team-phases.sh +54 -3
- package/presets/cursor/java/rules/agent-team-orchestrator.mdc +2 -4
- package/presets/cursor/java/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/java/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/java/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/java/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/mcp-ts/agents/task-analyst.md +1 -1
- package/presets/cursor/mcp-ts/commands/README.md +0 -2
- package/presets/cursor/mcp-ts/commands/task-continue.md +1 -1
- package/presets/cursor/mcp-ts/commands/task.md +1 -4
- package/presets/cursor/mcp-ts/commands/technical-retro.md +2 -2
- package/presets/cursor/mcp-ts/hooks/chain-team-phases.sh +40 -9
- package/presets/cursor/mcp-ts/rules/agent-team-orchestrator.mdc +2 -4
- package/presets/cursor/mcp-ts/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/mcp-ts/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/next/README.md +8 -6
- package/presets/cursor/next/REPO_AGENTS.md +8 -0
- package/presets/cursor/next/agents/README.md +2 -2
- package/presets/cursor/next/agents/build-verifier.md +11 -1
- package/presets/cursor/next/agents/feature-developer.md +1 -1
- package/presets/cursor/next/agents/openspec-author.md +164 -0
- package/presets/cursor/next/agents/task-analyst.md +1 -1
- package/presets/cursor/next/agents/task-router.md +5 -1
- package/presets/cursor/next/agents/tech-writer.md +4 -0
- package/presets/cursor/next/commands/README.md +0 -2
- package/presets/cursor/next/commands/task-continue.md +1 -1
- package/presets/cursor/next/commands/task.md +1 -4
- package/presets/cursor/next/commands/technical-retro.md +2 -2
- package/presets/cursor/next/hooks/chain-team-phases.sh +54 -2
- package/presets/cursor/next/rules/README.md +6 -3
- package/presets/cursor/next/rules/agent-team-orchestrator.mdc +7 -5
- package/presets/cursor/next/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/next/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/next/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/next/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/next/rules/tests-e2e-structure.mdc +83 -43
- package/presets/cursor/next/rules/tests-unit.mdc +78 -45
- package/presets/cursor/next/team/README.md +0 -3
- package/presets/cursor/nuxt/README.md +6 -6
- package/presets/cursor/nuxt/agents/README.md +1 -1
- package/presets/cursor/nuxt/agents/feature-developer.md +1 -1
- package/presets/cursor/nuxt/agents/openspec-author.md +164 -0
- package/presets/cursor/nuxt/agents/task-analyst.md +1 -1
- package/presets/cursor/nuxt/agents/task-router.md +5 -1
- package/presets/cursor/nuxt/agents/tech-writer.md +4 -0
- package/presets/cursor/nuxt/commands/README.md +0 -2
- package/presets/cursor/nuxt/commands/task-continue.md +1 -1
- package/presets/cursor/nuxt/commands/task.md +1 -5
- package/presets/cursor/nuxt/commands/technical-retro.md +2 -2
- package/presets/cursor/nuxt/hooks/chain-team-phases.sh +54 -2
- package/presets/cursor/nuxt/rules/README.md +3 -1
- package/presets/cursor/nuxt/rules/agent-team-orchestrator.mdc +8 -5
- package/presets/cursor/nuxt/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/nuxt/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/nuxt/rules/tests-e2e-structure.mdc +87 -43
- package/presets/cursor/nuxt/rules/tests-unit.mdc +62 -47
- package/presets/cursor/nuxt/team/README.md +0 -3
- package/presets/cursor/php-hexagonal/AGENTS.md +1 -1
- package/presets/cursor/php-hexagonal/agents/README.md +3 -2
- package/presets/cursor/php-hexagonal/agents/openspec-author.md +164 -0
- package/presets/cursor/php-hexagonal/agents/task-router.md +5 -1
- package/presets/cursor/php-hexagonal/agents/tech-writer.md +4 -0
- package/presets/cursor/php-hexagonal/commands/README.md +0 -2
- package/presets/cursor/php-hexagonal/commands/task-continue.md +1 -1
- package/presets/cursor/php-hexagonal/commands/task.md +1 -4
- package/presets/cursor/php-hexagonal/commands/technical-retro.md +2 -2
- package/presets/cursor/php-hexagonal/hooks/chain-team-phases.sh +54 -3
- package/presets/cursor/php-hexagonal/rules/agent-team-orchestrator.mdc +2 -4
- package/presets/cursor/php-hexagonal/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/php-hexagonal/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/php-laravel/AGENTS.md +1 -1
- package/presets/cursor/php-laravel/agents/README.md +3 -2
- package/presets/cursor/php-laravel/agents/openspec-author.md +164 -0
- package/presets/cursor/php-laravel/agents/task-router.md +5 -1
- package/presets/cursor/php-laravel/agents/tech-writer.md +4 -0
- package/presets/cursor/php-laravel/commands/README.md +0 -2
- package/presets/cursor/php-laravel/commands/task-continue.md +1 -1
- package/presets/cursor/php-laravel/commands/task.md +1 -4
- package/presets/cursor/php-laravel/commands/technical-retro.md +2 -2
- package/presets/cursor/php-laravel/hooks/chain-team-phases.sh +54 -3
- package/presets/cursor/php-laravel/rules/agent-team-orchestrator.mdc +2 -4
- package/presets/cursor/php-laravel/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/php-laravel/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/svelte/README.md +6 -6
- package/presets/cursor/svelte/agents/README.md +1 -1
- package/presets/cursor/svelte/agents/feature-developer.md +1 -1
- package/presets/cursor/svelte/agents/openspec-author.md +164 -0
- package/presets/cursor/svelte/agents/task-analyst.md +1 -1
- package/presets/cursor/svelte/agents/task-router.md +5 -1
- package/presets/cursor/svelte/agents/tech-writer.md +4 -0
- package/presets/cursor/svelte/commands/README.md +0 -2
- package/presets/cursor/svelte/commands/task-continue.md +1 -1
- package/presets/cursor/svelte/commands/task.md +1 -5
- package/presets/cursor/svelte/commands/technical-retro.md +2 -2
- package/presets/cursor/svelte/hooks/chain-team-phases.sh +54 -2
- package/presets/cursor/svelte/rules/README.md +3 -1
- package/presets/cursor/svelte/rules/agent-team-orchestrator.mdc +8 -5
- package/presets/cursor/svelte/rules/ai-local-overlay.mdc +43 -0
- package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +5 -1
- package/presets/cursor/svelte/rules/preset-token-budget.mdc +7 -3
- package/presets/cursor/svelte/rules/preset-twin-sync.mdc +2 -0
- package/presets/cursor/svelte/rules/tests-e2e-structure.mdc +87 -43
- package/presets/cursor/svelte/rules/tests-unit.mdc +72 -23
- package/presets/cursor/svelte/team/README.md +0 -3
- package/scripts/_lib/embed-stems.json +31 -0
- package/scripts/aggregate-agent-task-metrics.mjs +23 -0
- package/scripts/check-chain-hook-coverage.sh +109 -0
- package/scripts/check-local-overlay.sh +136 -0
- package/scripts/check-preset-i18n.sh +2 -2
- package/scripts/check-preset-leakage.sh +4 -1
- package/scripts/check-preset-structure.sh +21 -1
- package/scripts/check-preset-token-budget.sh +20 -10
- package/scripts/check-rule-eval.mjs +16 -0
- package/scripts/check-shared-core-drift.sh +10 -32
- package/scripts/close-agent-task-human-gate.mjs +22 -5
- package/scripts/sync-shared-core-embeds.sh +9 -20
- package/scripts/test-agent-task-metrics-hooks.mjs +1 -1
- package/scripts/test-chain-team-phases-coverage.mjs +411 -0
- package/presets/claude/android-kotlin/commands/feature-continue.md +0 -23
- package/presets/claude/android-kotlin/commands/feature-start.md +0 -41
- package/presets/claude/go/commands/feature-continue.md +0 -23
- package/presets/claude/go/commands/feature-start.md +0 -38
- package/presets/claude/ios-swift/commands/feature-continue.md +0 -23
- package/presets/claude/ios-swift/commands/feature-start.md +0 -41
- package/presets/claude/java/commands/feature-continue.md +0 -23
- package/presets/claude/java/commands/feature-start.md +0 -38
- package/presets/claude/mcp-ts/commands/feature-continue.md +0 -23
- package/presets/claude/mcp-ts/commands/feature-start.md +0 -41
- package/presets/claude/next/commands/feature-continue.md +0 -23
- package/presets/claude/next/commands/feature-start.md +0 -38
- package/presets/claude/nuxt/commands/feature-continue.md +0 -23
- package/presets/claude/nuxt/commands/feature-start.md +0 -41
- package/presets/claude/php-hexagonal/commands/feature-continue.md +0 -23
- package/presets/claude/php-hexagonal/commands/feature-start.md +0 -38
- package/presets/claude/php-laravel/commands/feature-continue.md +0 -23
- package/presets/claude/php-laravel/commands/feature-start.md +0 -38
- package/presets/claude/svelte/commands/feature-continue.md +0 -23
- package/presets/claude/svelte/commands/feature-start.md +0 -41
- package/presets/cursor/android-kotlin/commands/feature-continue.md +0 -23
- package/presets/cursor/android-kotlin/commands/feature-start.md +0 -41
- package/presets/cursor/go/commands/feature-continue.md +0 -23
- package/presets/cursor/go/commands/feature-start.md +0 -41
- package/presets/cursor/ios-swift/commands/feature-continue.md +0 -23
- package/presets/cursor/ios-swift/commands/feature-start.md +0 -41
- package/presets/cursor/java/commands/feature-continue.md +0 -23
- package/presets/cursor/java/commands/feature-start.md +0 -41
- package/presets/cursor/mcp-ts/commands/feature-continue.md +0 -23
- package/presets/cursor/mcp-ts/commands/feature-start.md +0 -41
- package/presets/cursor/next/commands/feature-continue.md +0 -23
- package/presets/cursor/next/commands/feature-start.md +0 -41
- package/presets/cursor/nuxt/commands/feature-continue.md +0 -23
- package/presets/cursor/nuxt/commands/feature-start.md +0 -41
- package/presets/cursor/php-hexagonal/commands/feature-continue.md +0 -23
- package/presets/cursor/php-hexagonal/commands/feature-start.md +0 -41
- package/presets/cursor/php-laravel/commands/feature-continue.md +0 -23
- package/presets/cursor/php-laravel/commands/feature-start.md +0 -41
- package/presets/cursor/svelte/commands/feature-continue.md +0 -23
- package/presets/cursor/svelte/commands/feature-start.md +0 -41
|
@@ -16,7 +16,8 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
|
|
18
18
|
STACKS = ("next", "nuxt", "svelte", "ios-swift", "android-kotlin", "go", "java", "mcp-ts", "php-hexagonal", "php-laravel")
|
|
19
|
-
BODY_CAP = 140
|
|
19
|
+
BODY_CAP = 140 # applied to 3-rule trios (Claude session-start, Cursor trio when len==3)
|
|
20
|
+
BODY_CAP_CURSOR_4 = 170 # Cursor now allows 4 always-on (next-app-core, package-manager, code-quality, ai-local-overlay)
|
|
20
21
|
MARKER_LINE_RE = re.compile(r"^<!--\s*shared-core:\s*[^>]+?\s*-->\s*$")
|
|
21
22
|
ALWAYS_APPLY_RE = re.compile(r"(?m)^alwaysApply:\s*true\s*$")
|
|
22
23
|
PATHS_RE = re.compile(r"(?m)^paths:\s*")
|
|
@@ -70,7 +71,7 @@ failed = 0
|
|
|
70
71
|
|
|
71
72
|
print("Preset token / session-start budget (Option A)")
|
|
72
73
|
print(f"Presets: {presets}")
|
|
73
|
-
print(f"Body line cap (trio
|
|
74
|
+
print(f"Body line cap (Claude trio): {BODY_CAP}; (Cursor 4-set): {BODY_CAP_CURSOR_4}")
|
|
74
75
|
print()
|
|
75
76
|
|
|
76
77
|
for stack in STACKS:
|
|
@@ -87,9 +88,15 @@ for stack in STACKS:
|
|
|
87
88
|
print(f"FAIL: meta preset-* must not be alwaysApply: {p.name}")
|
|
88
89
|
failed += 1
|
|
89
90
|
|
|
90
|
-
if len(always)
|
|
91
|
-
print(f"FAIL: Cursor alwaysApply count is {len(always)}, expected
|
|
91
|
+
if len(always) not in (3, 4):
|
|
92
|
+
print(f"FAIL: Cursor alwaysApply count is {len(always)}, expected 3 or 4 (4th slot reserved for ai-local-overlay)")
|
|
92
93
|
failed += 1
|
|
94
|
+
elif len(always) == 4:
|
|
95
|
+
# 4-set: ensure the 4th rule is ai-local-overlay (or declares itself as such).
|
|
96
|
+
extra = [p for p in always if p.stem not in {"code-quality-and-refactoring", "package-manager", "next-app-core", "nuxt-app-core", "svelte-app-core", "ios-app-core", "android-app-core", "go-app-core", "java-app-core", "mcp-ts-app-core", "php-app-core", "laravel-app-core", "architecture-boundaries", "mcp-server-boundaries"}]
|
|
97
|
+
if not any(p.stem == "ai-local-overlay" for p in always) and extra:
|
|
98
|
+
print(f"FAIL: Cursor alwaysApply=4 but no ai-local-overlay.mdc; the 4th slot is reserved for the local overlay rule")
|
|
99
|
+
failed += 1
|
|
93
100
|
|
|
94
101
|
# --- Claude no-paths (excl README) ---
|
|
95
102
|
session = claude_session_start(claude_rules)
|
|
@@ -108,10 +115,12 @@ for stack in STACKS:
|
|
|
108
115
|
)
|
|
109
116
|
failed += 1
|
|
110
117
|
|
|
111
|
-
# --- Body sum of intended
|
|
112
|
-
#
|
|
118
|
+
# --- Body sum of intended session-start set (Cursor alwaysApply + Claude session) ---
|
|
119
|
+
# Cursor allows 3 or 4 rules (4th slot reserved for ai-local-overlay); Claude is exactly 3.
|
|
113
120
|
for label, files in (("Cursor", always), ("Claude", session)):
|
|
114
|
-
if len(files)
|
|
121
|
+
if label == "Cursor" and len(files) not in (3, 4):
|
|
122
|
+
continue
|
|
123
|
+
if label == "Claude" and len(files) != 3:
|
|
115
124
|
continue
|
|
116
125
|
total = 0
|
|
117
126
|
for path in files:
|
|
@@ -121,10 +130,11 @@ for stack in STACKS:
|
|
|
121
130
|
total += n
|
|
122
131
|
name = path.name if label == "Cursor" else path.relative_to(claude_rules).as_posix()
|
|
123
132
|
print(f" {label} body lines {name}: {n}")
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
cap = BODY_CAP_CURSOR_4 if label == "Cursor" and len(always) == 4 else BODY_CAP
|
|
134
|
+
print(f" {label} session-start body sum: {total} (cap {cap}, files={len(files)})")
|
|
135
|
+
if total > cap:
|
|
126
136
|
print(
|
|
127
|
-
f"FAIL: {label} session-start body sum {total} > {
|
|
137
|
+
f"FAIL: {label} session-start body sum {total} > {cap} ({stack})"
|
|
128
138
|
)
|
|
129
139
|
failed += 1
|
|
130
140
|
|
|
@@ -99,6 +99,22 @@ function checkFile(relPath, minPairs, requireImpact) {
|
|
|
99
99
|
) && ok;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Order check: each Incorrect heading must precede its matching Correct heading.
|
|
103
|
+
// Without this, an agent reading the rule may learn a "correct" pattern before
|
|
104
|
+
// seeing the "incorrect" example it contrasts with.
|
|
105
|
+
if (incorrectCount > 0 && correctCount > 0) {
|
|
106
|
+
const incRe = /^#{2,3}\s+Incorrect\s*$/gim;
|
|
107
|
+
const corRe = /^#{2,3}\s+Correct\s*$/gim;
|
|
108
|
+
const firstInc = text.search(incRe);
|
|
109
|
+
const firstCor = text.search(corRe);
|
|
110
|
+
if (firstInc >= 0 && firstCor >= 0 && firstInc >= firstCor) {
|
|
111
|
+
ok =
|
|
112
|
+
fail(
|
|
113
|
+
`${relPath}: first "Incorrect" heading (offset ${firstInc}) must precede first "Correct" heading (offset ${firstCor})`,
|
|
114
|
+
) && ok;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
const incorrectBodies = sectionBodies(text, 'Incorrect');
|
|
103
119
|
const correctBodies = sectionBodies(text, 'Correct');
|
|
104
120
|
for (let i = 0; i < Math.min(incorrectBodies.length, correctBodies.length); i++) {
|
|
@@ -7,48 +7,26 @@ set -euo pipefail
|
|
|
7
7
|
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
8
8
|
export AI_RULES_PRESETS_ROOT="$ROOT/packages/ai-rules/presets"
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
EMBED_LIST_FILE="$SCRIPT_DIR/_lib/embed-stems.json"
|
|
12
|
+
|
|
13
|
+
python3 - "$EMBED_LIST_FILE" <<'PY'
|
|
11
14
|
from __future__ import annotations
|
|
12
15
|
|
|
16
|
+
import json
|
|
13
17
|
import os
|
|
14
18
|
import re
|
|
15
19
|
import sys
|
|
16
20
|
from pathlib import Path
|
|
17
21
|
|
|
22
|
+
embed_list_file = Path(sys.argv[1])
|
|
23
|
+
_stems = json.loads(embed_list_file.read_text(encoding="utf-8"))
|
|
24
|
+
EMBED = _stems["embed"]
|
|
25
|
+
LINEAGE = _stems["lineage"]
|
|
26
|
+
|
|
18
27
|
presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
|
|
19
28
|
core_root = presets / "_shared" / "core"
|
|
20
29
|
|
|
21
|
-
EMBED = [
|
|
22
|
-
"quality/code-quality-and-refactoring.md",
|
|
23
|
-
"quality/anti-sycophancy-discipline.md",
|
|
24
|
-
"quality/design-guidance.md",
|
|
25
|
-
"quality/mcp-usage.md",
|
|
26
|
-
"meta/preset-layering.md",
|
|
27
|
-
"meta/preset-twin-sync.md",
|
|
28
|
-
"meta/preset-token-budget.md",
|
|
29
|
-
"meta/preset-pr-checklist.md",
|
|
30
|
-
"meta/preset-no-cross-stack-leakage.md",
|
|
31
|
-
"review/code-review-mr.md",
|
|
32
|
-
"review/technical-retro.md",
|
|
33
|
-
"review/mockup-review.md",
|
|
34
|
-
"review/ux-design.md",
|
|
35
|
-
"agent-team/agent-team-intake.md",
|
|
36
|
-
"architecture/product-specs.md",
|
|
37
|
-
"architecture/product-specs-authoring.md",
|
|
38
|
-
# Whole-file embed: skill debug-investigation = core + ## Stack notes only.
|
|
39
|
-
"quality/debug-investigation.md",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
LINEAGE = [
|
|
43
|
-
"architecture/feature-delivery-workflow.md",
|
|
44
|
-
"architecture/reference-features.template.md",
|
|
45
|
-
"agent-team/agent-team-orchestrator.md",
|
|
46
|
-
# Section snippets in fat agents/skills — body is not the whole file.
|
|
47
|
-
"quality/verification-before-completion.md",
|
|
48
|
-
"review/receiving-code-review.md",
|
|
49
|
-
"agent-team/task-classification.md",
|
|
50
|
-
]
|
|
51
|
-
|
|
52
30
|
# Full-line sync marker only (do not strip prose that *mentions* the marker).
|
|
53
31
|
MARKER_LINE_RE = re.compile(r"^<!--\s*shared-core:\s*[^>]+?\s*-->\s*$")
|
|
54
32
|
|
|
@@ -13,18 +13,35 @@ import path from 'node:path';
|
|
|
13
13
|
|
|
14
14
|
function parseArgs(argv) {
|
|
15
15
|
const options = { cwd: process.cwd(), platform: 'cursor', slug: null };
|
|
16
|
+
// Read the next token from argv for value-taking flags. Returns the value or null
|
|
17
|
+
// when the flag is missing its value; the caller exits with a friendly error.
|
|
18
|
+
function nextValue(flag, i) {
|
|
19
|
+
const next = argv[i + 1];
|
|
20
|
+
if (next === undefined || next.startsWith('-')) {
|
|
21
|
+
console.error(`Missing value for ${flag} (use ${flag} <value>)`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
16
26
|
for (let i = 2; i < argv.length; i += 1) {
|
|
17
27
|
const arg = argv[i];
|
|
18
|
-
if (arg === '--cwd')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
else if (arg === '--
|
|
28
|
+
if (arg === '--cwd') {
|
|
29
|
+
options.cwd = path.resolve(nextValue(arg, i));
|
|
30
|
+
i += 1;
|
|
31
|
+
} else if (arg === '--platform') {
|
|
32
|
+
options.platform = nextValue(arg, i);
|
|
33
|
+
i += 1;
|
|
34
|
+
} else if (arg === '--slug') {
|
|
35
|
+
options.slug = nextValue(arg, i);
|
|
36
|
+
i += 1;
|
|
37
|
+
} else if (arg === '--activate-next') {
|
|
22
38
|
/* always activate via hook payload */
|
|
23
39
|
} else if (arg === '--team-root') {
|
|
24
40
|
// Compatibility with earlier docs: derive platform + cwd from team root.
|
|
25
|
-
const teamRoot = path.resolve(
|
|
41
|
+
const teamRoot = path.resolve(nextValue(arg, i));
|
|
26
42
|
options.cwd = path.resolve(teamRoot, '..', '..');
|
|
27
43
|
options.platform = path.basename(path.dirname(teamRoot)) === '.claude' ? 'claude' : 'cursor';
|
|
44
|
+
i += 1;
|
|
28
45
|
} else {
|
|
29
46
|
console.error(`Unknown argument: ${arg}`);
|
|
30
47
|
process.exit(2);
|
|
@@ -8,35 +8,24 @@ set -euo pipefail
|
|
|
8
8
|
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
9
9
|
export AI_RULES_PRESETS_ROOT="$ROOT/packages/ai-rules/presets"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
EMBED_LIST_FILE="$SCRIPT_DIR/_lib/embed-stems.json"
|
|
13
|
+
|
|
14
|
+
python3 - "$EMBED_LIST_FILE" <<'PY'
|
|
12
15
|
from __future__ import annotations
|
|
13
16
|
|
|
17
|
+
import json
|
|
14
18
|
import os
|
|
15
19
|
import re
|
|
20
|
+
import sys
|
|
16
21
|
from pathlib import Path
|
|
17
22
|
|
|
23
|
+
embed_list_file = Path(sys.argv[1])
|
|
24
|
+
EMBED = json.loads(embed_list_file.read_text(encoding="utf-8"))["embed"]
|
|
25
|
+
|
|
18
26
|
presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
|
|
19
27
|
core_root = presets / "_shared" / "core"
|
|
20
28
|
|
|
21
|
-
EMBED = [
|
|
22
|
-
"quality/code-quality-and-refactoring.md",
|
|
23
|
-
"quality/anti-sycophancy-discipline.md",
|
|
24
|
-
"quality/design-guidance.md",
|
|
25
|
-
"quality/mcp-usage.md",
|
|
26
|
-
"meta/preset-layering.md",
|
|
27
|
-
"meta/preset-twin-sync.md",
|
|
28
|
-
"meta/preset-token-budget.md",
|
|
29
|
-
"meta/preset-pr-checklist.md",
|
|
30
|
-
"meta/preset-no-cross-stack-leakage.md",
|
|
31
|
-
"review/code-review-mr.md",
|
|
32
|
-
"review/technical-retro.md",
|
|
33
|
-
"review/mockup-review.md",
|
|
34
|
-
"review/ux-design.md",
|
|
35
|
-
"agent-team/agent-team-intake.md",
|
|
36
|
-
"architecture/product-specs.md",
|
|
37
|
-
"architecture/product-specs-authoring.md",
|
|
38
|
-
]
|
|
39
|
-
|
|
40
29
|
MARKER_LINE_RE = re.compile(r"^<!--\s*shared-core:\s*([^>]+?)\s*-->\s*$")
|
|
41
30
|
|
|
42
31
|
|
|
@@ -14,7 +14,7 @@ const hooks = [
|
|
|
14
14
|
...fs.readdirSync(path.join(presetsDirectory, 'claude')).map((stack) => path.join(presetsDirectory, 'claude', stack, 'hooks', 'chain-team-phases.sh')),
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
-
assert.equal(hooks.length,
|
|
17
|
+
assert.equal(hooks.length, 20, 'expected every packaged Cursor and Claude hook (10 stacks × 2 platforms)');
|
|
18
18
|
|
|
19
19
|
function readJson(filePath) {
|
|
20
20
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Targeted tests for chain-team-phases.sh fixes that prevent `/task` pipelines
|
|
3
|
+
// from silently skipping or stalling on modern roles.
|
|
4
|
+
//
|
|
5
|
+
// Coverage:
|
|
6
|
+
// 1. openspec-author handoff path: the AGENT_HANDOFF['openspec-author']
|
|
7
|
+
// entry must be emitted so the orchestrator invokes the agent; the
|
|
8
|
+
// DEFAULT_ARTIFACT_OUTPUT_IDS['openspec-author'] entry must match the
|
|
9
|
+
// manifest's `openspec-change` receipt so the hook advances instead of
|
|
10
|
+
// stalling in `awaiting_artifact_receipt`.
|
|
11
|
+
// 2. QA / unit / playwright / instrumentation / integration / xcuitest
|
|
12
|
+
// agents must have both AGENT_HANDOFF and DEFAULT_ARTIFACT_OUTPUT_IDS
|
|
13
|
+
// entries (the symmetric check is in check-chain-hook-coverage.sh; here
|
|
14
|
+
// we drive a full pipeline step to make sure receipts reconcile).
|
|
15
|
+
// 3. Pipeline.json parse error: a malformed pipeline.json that still
|
|
16
|
+
// carries an `intent` field must NOT fall through to legacyFlow; the
|
|
17
|
+
// hook must write `state: blocked` and emit a followup_message that
|
|
18
|
+
// mentions `pipeline.json` so the orchestrator surfaces the error.
|
|
19
|
+
// 4. Legacy flow fallback: a missing pipeline.json must still invoke
|
|
20
|
+
// legacyFlow() (analysis → development → review → testing chain).
|
|
21
|
+
// 5. writeStatus durability: status.json must be durable (fsync) before the
|
|
22
|
+
// hook returns. We assert that a second reader immediately after the
|
|
23
|
+
// hook returns sees the new state.
|
|
24
|
+
|
|
25
|
+
import assert from 'node:assert/strict';
|
|
26
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const packageDirectory = path.resolve(scriptDirectory, '..');
|
|
33
|
+
const presetsDirectory = path.join(packageDirectory, 'presets');
|
|
34
|
+
|
|
35
|
+
const hooks = [
|
|
36
|
+
...fs.readdirSync(path.join(presetsDirectory, 'cursor')).map((stack) => path.join(presetsDirectory, 'cursor', stack, 'hooks', 'chain-team-phases.sh')),
|
|
37
|
+
...fs.readdirSync(path.join(presetsDirectory, 'claude')).map((stack) => path.join(presetsDirectory, 'claude', stack, 'hooks', 'chain-team-phases.sh')),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
assert.equal(hooks.length, 20, `expected 20 packaged hooks (10 stacks × 2 platforms), got ${hooks.length}`);
|
|
41
|
+
|
|
42
|
+
// mcp-ts does not ship openspec-author (per preset-no-cross-stack-leakage).
|
|
43
|
+
const HOOKS_WITH_OPENSPEC = hooks.filter((hook) => !hook.includes(`${path.sep}mcp-ts${path.sep}`));
|
|
44
|
+
assert.equal(HOOKS_WITH_OPENSPEC.length, 18, 'expected 18 hooks to cover openspec-author');
|
|
45
|
+
|
|
46
|
+
function readJson(filePath) {
|
|
47
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeJson(filePath, value) {
|
|
51
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function invokeHook(hook, workingDirectory, attemptId, payload = {}) {
|
|
55
|
+
return execFileSync('bash', [hook], {
|
|
56
|
+
cwd: workingDirectory,
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
input: JSON.stringify({ status: 'completed', attemptId, ...payload }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function platformFor(hook) {
|
|
63
|
+
return hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {}) {
|
|
67
|
+
const platform = platformFor(hook);
|
|
68
|
+
const teamRoot = path.join(root, `.${platform}`, 'team');
|
|
69
|
+
const taskDirectory = path.join(teamRoot, 'tasks', slug);
|
|
70
|
+
fs.mkdirSync(taskDirectory, { recursive: true });
|
|
71
|
+
writeJson(path.join(teamRoot, 'active-task.json'), { slug });
|
|
72
|
+
writeJson(path.join(taskDirectory, 'pipeline.json'), pipeline);
|
|
73
|
+
writeJson(path.join(taskDirectory, 'status.json'), status);
|
|
74
|
+
writeJson(path.join(taskDirectory, 'metrics.json'), metrics);
|
|
75
|
+
for (const [name, content] of Object.entries(artifacts)) {
|
|
76
|
+
fs.writeFileSync(path.join(taskDirectory, name), content);
|
|
77
|
+
}
|
|
78
|
+
return { taskDirectory, platform };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const todayIso = '2026-08-21T00:00:00.000Z';
|
|
82
|
+
const earlierIso = '2026-08-21T00:00:01.000Z';
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Test 1 — openspec-author handoff: the hook must emit a followup_message that
|
|
86
|
+
// references openspec-author and the openspec-change scope, and the default
|
|
87
|
+
// artifact id must match the manifest receipt for `state` to advance.
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
for (const hook of HOOKS_WITH_OPENSPEC) {
|
|
90
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.openspec-handoff-'));
|
|
91
|
+
try {
|
|
92
|
+
const runId = 'openspec-handoff-run';
|
|
93
|
+
const platform = platformFor(hook);
|
|
94
|
+
const analystAttemptId = `${runId}:step-0:task-analyst:attempt-1`;
|
|
95
|
+
const { taskDirectory } = writeTask(
|
|
96
|
+
root,
|
|
97
|
+
hook,
|
|
98
|
+
'openspec-handoff',
|
|
99
|
+
{
|
|
100
|
+
intent: 'openspec-change',
|
|
101
|
+
profile: 'standard',
|
|
102
|
+
steps: [
|
|
103
|
+
{ agent: 'task-analyst', label: 'Analyze' },
|
|
104
|
+
{ agent: 'openspec-author', label: 'Author spec change', scope: 'openspec:MEDCARD-11762' },
|
|
105
|
+
],
|
|
106
|
+
humanGates: [],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
slug: 'openspec-handoff',
|
|
110
|
+
intent: 'openspec-change',
|
|
111
|
+
pipelineIndex: 0,
|
|
112
|
+
currentAgent: 'task-analyst',
|
|
113
|
+
phase: 'executing',
|
|
114
|
+
state: 'completed',
|
|
115
|
+
activeAttempts: {
|
|
116
|
+
[analystAttemptId]: {
|
|
117
|
+
agent: 'task-analyst',
|
|
118
|
+
stepIndex: 0,
|
|
119
|
+
attempt: 1,
|
|
120
|
+
startedAt: todayIso,
|
|
121
|
+
parallelGroupId: null,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
metricsRunId: runId,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
schemaVersion: 1,
|
|
128
|
+
task: { runId, slug: 'openspec-handoff', preset: 'ai-rules', stack: 'test', platform, intent: 'openspec-change', profile: 'standard' },
|
|
129
|
+
events: [
|
|
130
|
+
{ eventId: `${runId}:task_started`, type: 'task_started', at: todayIso },
|
|
131
|
+
{ eventId: `${runId}:analyst-start`, type: 'attempt_started', attemptId: analystAttemptId, agent: 'task-analyst', stepIndex: 0, attempt: 1, at: todayIso },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const handoffRaw = invokeHook(hook, root, analystAttemptId);
|
|
137
|
+
const handoff = JSON.parse(handoffRaw).followup_message;
|
|
138
|
+
assert.match(handoff, /openspec-author/, `${hook}: handoff did not mention openspec-author`);
|
|
139
|
+
assert.match(handoff, /openspec:MEDCARD-11762/, `${hook}: handoff did not echo the openspec scope`);
|
|
140
|
+
|
|
141
|
+
const statusAfter = readJson(path.join(taskDirectory, 'status.json'));
|
|
142
|
+
assert.equal(statusAfter.pipelineIndex, 1, `${hook}: hook did not advance past the analyst step`);
|
|
143
|
+
assert.equal(statusAfter.currentAgent, 'openspec-author', `${hook}: next agent was not openspec-author`);
|
|
144
|
+
const activeAttempts = Object.values(statusAfter.activeAttempts || {});
|
|
145
|
+
assert.ok(activeAttempts.some((entry) => entry.agent === 'openspec-author'), `${hook}: openspec-author attempt was not activated`);
|
|
146
|
+
|
|
147
|
+
// Now finish the openspec-author attempt with a properly bound receipt
|
|
148
|
+
// (id: openspec-change) and assert the hook advances to the next step.
|
|
149
|
+
const openspecAttempt = Object.entries(statusAfter.activeAttempts).find(([, entry]) => entry.agent === 'openspec-author');
|
|
150
|
+
const openspecAttemptId = openspecAttempt[0];
|
|
151
|
+
// Use a timestamp after the attempt's actual startedAt — the hook filters
|
|
152
|
+
// out manifest receipts whose completedAt predates the attempt's start.
|
|
153
|
+
const freshIso = new Date().toISOString();
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
path.join(taskDirectory, 'artifact-manifest.json'),
|
|
156
|
+
JSON.stringify(
|
|
157
|
+
{
|
|
158
|
+
schemaVersion: 1,
|
|
159
|
+
slug: 'openspec-handoff',
|
|
160
|
+
updatedAt: freshIso,
|
|
161
|
+
artifacts: [
|
|
162
|
+
{
|
|
163
|
+
id: 'openspec-change',
|
|
164
|
+
producer: 'openspec-author',
|
|
165
|
+
status: 'completed',
|
|
166
|
+
authoritativePaths: ['openspec/changes/MEDCARD-11762/proposal.md'],
|
|
167
|
+
acceptanceCriteriaIds: ['AC-1'],
|
|
168
|
+
taskIds: ['T1'],
|
|
169
|
+
timestamps: { createdAt: freshIso, updatedAt: freshIso, completedAt: freshIso },
|
|
170
|
+
receipt: {
|
|
171
|
+
outcome: 'completed',
|
|
172
|
+
attemptId: openspecAttemptId,
|
|
173
|
+
summary: 'Proposal + design + tasks emitted; openspec validate --strict PASS',
|
|
174
|
+
evidencePaths: ['openspec/changes/MEDCARD-11762/proposal.md'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
null,
|
|
180
|
+
2,
|
|
181
|
+
) + '\n',
|
|
182
|
+
);
|
|
183
|
+
writeJson(path.join(taskDirectory, 'status.json'), { ...statusAfter, state: 'completed' });
|
|
184
|
+
const finishRaw = invokeHook(hook, root, openspecAttemptId);
|
|
185
|
+
const finishHandoff = JSON.parse(finishRaw).followup_message;
|
|
186
|
+
assert.match(finishHandoff, /Pipeline complete/, `${hook}: pipeline should complete after openspec-author finishes`);
|
|
187
|
+
const finalStatus = readJson(path.join(taskDirectory, 'status.json'));
|
|
188
|
+
assert.equal(finalStatus.phase, 'done', `${hook}: phase did not transition to done`);
|
|
189
|
+
assert.equal(finalStatus.state, 'completed', `${hook}: state did not reach completed after openspec-author`);
|
|
190
|
+
} finally {
|
|
191
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Test 2 — qa-tester + unit-test-* handoff: a QA step in a pipeline must
|
|
197
|
+
// advance. Before the fix, DEFAULT_ARTIFACT_OUTPUT_IDS lacked the QA entries
|
|
198
|
+
// so `artifactOutputIdFor` returned the agent name itself; manifest receipts
|
|
199
|
+
// landed under the right id (qa / unit-test-spec) and the hook stalled in
|
|
200
|
+
// awaiting_artifact_receipt.
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
for (const hook of hooks) {
|
|
203
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.qa-tester-handoff-'));
|
|
204
|
+
try {
|
|
205
|
+
const platform = platformFor(hook);
|
|
206
|
+
const runId = 'qa-tester-run';
|
|
207
|
+
const devAttemptId = `${runId}:step-0:feature-developer:attempt-1`;
|
|
208
|
+
const { taskDirectory } = writeTask(
|
|
209
|
+
root,
|
|
210
|
+
hook,
|
|
211
|
+
'qa-tester-handoff',
|
|
212
|
+
{
|
|
213
|
+
intent: 'test-only',
|
|
214
|
+
profile: 'standard',
|
|
215
|
+
steps: [
|
|
216
|
+
{ agent: 'feature-developer', label: 'Implement' },
|
|
217
|
+
{ agent: 'qa-tester', label: 'QA' },
|
|
218
|
+
],
|
|
219
|
+
humanGates: [],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
slug: 'qa-tester-handoff',
|
|
223
|
+
intent: 'test-only',
|
|
224
|
+
pipelineIndex: 0,
|
|
225
|
+
currentAgent: 'feature-developer',
|
|
226
|
+
phase: 'executing',
|
|
227
|
+
state: 'completed',
|
|
228
|
+
activeAttempts: {
|
|
229
|
+
[devAttemptId]: {
|
|
230
|
+
agent: 'feature-developer',
|
|
231
|
+
stepIndex: 0,
|
|
232
|
+
attempt: 1,
|
|
233
|
+
startedAt: todayIso,
|
|
234
|
+
parallelGroupId: null,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
metricsRunId: runId,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
schemaVersion: 1,
|
|
241
|
+
task: { runId, slug: 'qa-tester-handoff', preset: 'ai-rules', stack: 'test', platform, intent: 'test-only', profile: 'standard' },
|
|
242
|
+
events: [
|
|
243
|
+
{ eventId: `${runId}:task_started`, type: 'task_started', at: todayIso },
|
|
244
|
+
{ eventId: `${runId}:dev-start`, type: 'attempt_started', attemptId: devAttemptId, agent: 'feature-developer', stepIndex: 0, attempt: 1, at: todayIso },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
const handoff = JSON.parse(invokeHook(hook, root, devAttemptId)).followup_message;
|
|
249
|
+
assert.match(handoff, /qa-tester/, `${hook}: handoff did not mention qa-tester`);
|
|
250
|
+
const statusAfter = readJson(path.join(taskDirectory, 'status.json'));
|
|
251
|
+
assert.equal(statusAfter.pipelineIndex, 1, `${hook}: hook did not advance to qa-tester step`);
|
|
252
|
+
assert.equal(statusAfter.currentAgent, 'qa-tester', `${hook}: currentAgent was not qa-tester`);
|
|
253
|
+
} finally {
|
|
254
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Test 3 — pipeline.json parse error with intent: the hook must surface the
|
|
260
|
+
// error and write state: blocked; legacyFlow must NOT be silently used.
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
for (const hook of hooks) {
|
|
263
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.pipeline-misparse-'));
|
|
264
|
+
try {
|
|
265
|
+
const platform = platformFor(hook);
|
|
266
|
+
const runId = 'misparse-run';
|
|
267
|
+
const devAttemptId = `${runId}:step-0:feature-developer:attempt-1`;
|
|
268
|
+
const taskDirectory = path.join(root, `.${platform}`, 'team', 'tasks', 'misparse');
|
|
269
|
+
fs.mkdirSync(taskDirectory, { recursive: true });
|
|
270
|
+
fs.writeFileSync(path.join(root, `.${platform}`, 'team', 'active-task.json'), JSON.stringify({ slug: 'misparse' }));
|
|
271
|
+
// Malformed pipeline.json with `intent` field — must NOT fall back to legacy.
|
|
272
|
+
fs.writeFileSync(
|
|
273
|
+
path.join(taskDirectory, 'pipeline.json'),
|
|
274
|
+
'{ "intent": "openspec-change", "steps": [\n { "agent": "openspec-author" }\n', // truncated JSON
|
|
275
|
+
);
|
|
276
|
+
writeJson(path.join(taskDirectory, 'status.json'), {
|
|
277
|
+
slug: 'misparse',
|
|
278
|
+
intent: 'openspec-change',
|
|
279
|
+
pipelineIndex: 0,
|
|
280
|
+
currentAgent: 'feature-developer',
|
|
281
|
+
phase: 'executing',
|
|
282
|
+
state: 'completed',
|
|
283
|
+
activeAttempts: {
|
|
284
|
+
[devAttemptId]: {
|
|
285
|
+
agent: 'feature-developer',
|
|
286
|
+
stepIndex: 0,
|
|
287
|
+
attempt: 1,
|
|
288
|
+
startedAt: todayIso,
|
|
289
|
+
parallelGroupId: null,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
metricsRunId: runId,
|
|
293
|
+
});
|
|
294
|
+
writeJson(path.join(taskDirectory, 'metrics.json'), {
|
|
295
|
+
schemaVersion: 1,
|
|
296
|
+
task: { runId, slug: 'misparse', preset: 'ai-rules', stack: 'test', platform, intent: 'openspec-change', profile: 'standard' },
|
|
297
|
+
events: [
|
|
298
|
+
{ eventId: `${runId}:task_started`, type: 'task_started', at: todayIso },
|
|
299
|
+
{ eventId: `${runId}:start`, type: 'attempt_started', attemptId: devAttemptId, agent: 'feature-developer', stepIndex: 0, attempt: 1, at: todayIso },
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const raw = invokeHook(hook, root, devAttemptId);
|
|
304
|
+
const parsed = JSON.parse(raw);
|
|
305
|
+
const followup = parsed.followup_message || '';
|
|
306
|
+
assert.match(followup, /pipeline\.json is malformed/, `${hook}: hook did not surface parse error in followup_message (got: ${followup})`);
|
|
307
|
+
const status = readJson(path.join(taskDirectory, 'status.json'));
|
|
308
|
+
assert.equal(status.state, 'blocked', `${hook}: malformed pipeline with intent did not block the pipeline (got: ${status.state})`);
|
|
309
|
+
assert.equal(status.phase, 'blocked', `${hook}: phase did not transition to blocked`);
|
|
310
|
+
const metrics = readJson(path.join(taskDirectory, 'metrics.json'));
|
|
311
|
+
assert.ok(
|
|
312
|
+
metrics.events.some((event) => event.type === 'pipeline_parse_error'),
|
|
313
|
+
`${hook}: pipeline_parse_error metric event was not emitted`,
|
|
314
|
+
);
|
|
315
|
+
} finally {
|
|
316
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Test 4 — legacyFlow fallback: when pipeline.json is absent, the hook must
|
|
322
|
+
// continue to use legacyFlow (analysis → development → review → testing).
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
for (const hook of hooks) {
|
|
325
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.legacy-fallback-'));
|
|
326
|
+
try {
|
|
327
|
+
const platform = platformFor(hook);
|
|
328
|
+
const taskDirectory = path.join(root, `.${platform}`, 'team', 'tasks', 'legacy-slug');
|
|
329
|
+
fs.mkdirSync(taskDirectory, { recursive: true });
|
|
330
|
+
fs.writeFileSync(path.join(root, `.${platform}`, 'team', 'active-task.json'), JSON.stringify({ slug: 'legacy-slug' }));
|
|
331
|
+
// No pipeline.json — legacy flow triggers.
|
|
332
|
+
writeJson(path.join(taskDirectory, 'status.json'), {
|
|
333
|
+
slug: 'legacy-slug',
|
|
334
|
+
intent: 'feature',
|
|
335
|
+
phase: 'development',
|
|
336
|
+
state: 'completed',
|
|
337
|
+
currentAgent: 'feature-developer',
|
|
338
|
+
pipelineIndex: 0,
|
|
339
|
+
});
|
|
340
|
+
const raw = invokeHook(hook, root, null);
|
|
341
|
+
const parsed = JSON.parse(raw);
|
|
342
|
+
const followup = parsed.followup_message || '';
|
|
343
|
+
assert.match(followup, /build-verifier/, `${hook}: legacy fallback did not invoke build-verifier (got: ${followup})`);
|
|
344
|
+
} finally {
|
|
345
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// Test 5 — writeStatus durability: after the hook returns, the new
|
|
351
|
+
// status.json must already be on disk and reflect the post-step state. This
|
|
352
|
+
// is the property that lets the orchestrator's next read see the human-gate
|
|
353
|
+
// transition (no stale-read race).
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
for (const hook of HOOKS_WITH_OPENSPEC) {
|
|
356
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.write-status-durability-'));
|
|
357
|
+
try {
|
|
358
|
+
const runId = 'durability-run';
|
|
359
|
+
const platform = platformFor(hook);
|
|
360
|
+
const analystAttemptId = `${runId}:step-0:task-analyst:attempt-1`;
|
|
361
|
+
const { taskDirectory } = writeTask(
|
|
362
|
+
root,
|
|
363
|
+
hook,
|
|
364
|
+
'durability-slug',
|
|
365
|
+
{
|
|
366
|
+
intent: 'feature',
|
|
367
|
+
profile: 'standard',
|
|
368
|
+
steps: [
|
|
369
|
+
{ agent: 'task-analyst', label: 'Analyze' },
|
|
370
|
+
{ agent: 'openspec-author', label: 'Spec change' },
|
|
371
|
+
],
|
|
372
|
+
humanGates: ['after:task-analyst'],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
slug: 'durability-slug',
|
|
376
|
+
intent: 'feature',
|
|
377
|
+
pipelineIndex: 0,
|
|
378
|
+
currentAgent: 'task-analyst',
|
|
379
|
+
phase: 'executing',
|
|
380
|
+
state: 'completed',
|
|
381
|
+
activeAttempts: {
|
|
382
|
+
[analystAttemptId]: {
|
|
383
|
+
agent: 'task-analyst',
|
|
384
|
+
stepIndex: 0,
|
|
385
|
+
attempt: 1,
|
|
386
|
+
startedAt: todayIso,
|
|
387
|
+
parallelGroupId: null,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
metricsRunId: runId,
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
schemaVersion: 1,
|
|
394
|
+
task: { runId, slug: 'durability-slug', preset: 'ai-rules', stack: 'test', platform, intent: 'feature', profile: 'standard' },
|
|
395
|
+
events: [
|
|
396
|
+
{ eventId: `${runId}:task_started`, type: 'task_started', at: todayIso },
|
|
397
|
+
{ eventId: `${runId}:start`, type: 'attempt_started', attemptId: analystAttemptId, agent: 'task-analyst', stepIndex: 0, attempt: 1, at: todayIso },
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
// Sync + invoke hook; immediately read status.json without fsync of our own.
|
|
402
|
+
invokeHook(hook, root, analystAttemptId);
|
|
403
|
+
const onDisk = JSON.parse(fs.readFileSync(path.join(taskDirectory, 'status.json'), 'utf8'));
|
|
404
|
+
assert.equal(onDisk.awaitingHumanGate, true, `${hook}: human gate was not durably written to status.json`);
|
|
405
|
+
assert.equal(onDisk.state, 'awaiting_approval', `${hook}: state did not durably transition to awaiting_approval`);
|
|
406
|
+
} finally {
|
|
407
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
console.log(`Verified openspec-author handoff, qa-tester handoff, pipeline misparse blocking, legacy fallback, and writeStatus durability in ${hooks.length} hooks.`);
|