@bonesofspring/ai-rules 0.2.21 → 0.2.23
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 +8 -0
- package/README.md +6 -2
- package/bin/cli.js +32 -8
- package/bin/mavis-install.js +441 -0
- package/package.json +9 -2
- package/presets/_shared/core/agent-team/agent-artifact-contracts.md +2 -0
- package/presets/_shared/core/meta/preset-pr-checklist.md +1 -0
- package/presets/_shared/core/meta/preset-twin-sync.md +17 -1
- package/presets/claude/android-kotlin/agents/build-verifier.md +2 -0
- package/presets/claude/android-kotlin/agents/code-reviewer.md +2 -0
- package/presets/claude/android-kotlin/agents/debugger.md +1 -0
- package/presets/claude/android-kotlin/agents/feature-developer.md +3 -0
- package/presets/claude/android-kotlin/agents/qa-tester.md +2 -0
- package/presets/claude/android-kotlin/agents/task-router.md +4 -0
- package/presets/claude/android-kotlin/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/go/agents/build-verifier.md +4 -0
- package/presets/claude/go/agents/code-reviewer.md +4 -0
- package/presets/claude/go/agents/debugger.md +4 -0
- package/presets/claude/go/agents/feature-developer.md +5 -0
- package/presets/claude/go/agents/qa-tester.md +4 -0
- package/presets/claude/go/agents/task-router.md +4 -0
- package/presets/claude/go/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/ios-swift/agents/build-verifier.md +2 -0
- package/presets/claude/ios-swift/agents/code-reviewer.md +2 -0
- package/presets/claude/ios-swift/agents/debugger.md +1 -0
- package/presets/claude/ios-swift/agents/feature-developer.md +3 -0
- package/presets/claude/ios-swift/agents/qa-tester.md +2 -0
- package/presets/claude/ios-swift/agents/task-router.md +4 -0
- package/presets/claude/ios-swift/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/java/agents/build-verifier.md +4 -0
- package/presets/claude/java/agents/code-reviewer.md +4 -0
- package/presets/claude/java/agents/debugger.md +4 -0
- package/presets/claude/java/agents/feature-developer.md +5 -0
- package/presets/claude/java/agents/qa-tester.md +4 -0
- package/presets/claude/java/agents/task-router.md +4 -0
- package/presets/claude/java/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/mcp-ts/agents/build-verifier.md +4 -0
- package/presets/claude/mcp-ts/agents/feature-developer.md +5 -0
- package/presets/claude/mcp-ts/agents/task-router.md +1 -1
- package/presets/claude/mcp-ts/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/next/agents/build-verifier.md +2 -0
- package/presets/claude/next/agents/code-reviewer.md +2 -0
- package/presets/claude/next/agents/debugger.md +4 -0
- package/presets/claude/next/agents/feature-developer.md +3 -0
- package/presets/claude/next/agents/qa-tester.md +4 -0
- package/presets/claude/next/agents/unit-test-generator.md +1 -1
- package/presets/claude/next/agents/unit-test-planner.md +1 -1
- package/presets/claude/next/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/next/rules/api-and-data/http-client.md +1 -1
- package/presets/claude/next/rules/architecture/reference-features.md +1 -1
- package/presets/claude/next/rules/testing/README.md +1 -1
- package/presets/claude/next/rules/testing/tests-e2e-structure.md +2 -0
- package/presets/claude/next/rules/testing/tests-unit.md +3 -5
- package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/next/rules/ui-and-accessibility/react-ui.md +1 -1
- package/presets/claude/next/skills/playwright-e2e/SKILL.md +5 -3
- package/presets/claude/next/skills/unit-testing/SKILL.md +6 -4
- package/presets/claude/nuxt/agents/build-verifier.md +2 -0
- package/presets/claude/nuxt/agents/code-reviewer.md +2 -0
- package/presets/claude/nuxt/agents/debugger.md +4 -0
- package/presets/claude/nuxt/agents/feature-developer.md +3 -0
- package/presets/claude/nuxt/agents/qa-tester.md +4 -0
- package/presets/claude/nuxt/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/php-hexagonal/agents/build-verifier.md +4 -0
- package/presets/claude/php-hexagonal/agents/code-reviewer.md +4 -0
- package/presets/claude/php-hexagonal/agents/debugger.md +4 -0
- package/presets/claude/php-hexagonal/agents/feature-developer.md +5 -0
- package/presets/claude/php-hexagonal/agents/qa-tester.md +4 -0
- package/presets/claude/php-hexagonal/agents/task-router.md +4 -0
- package/presets/claude/php-hexagonal/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/php-laravel/agents/build-verifier.md +4 -0
- package/presets/claude/php-laravel/agents/code-reviewer.md +4 -0
- package/presets/claude/php-laravel/agents/debugger.md +4 -0
- package/presets/claude/php-laravel/agents/feature-developer.md +5 -0
- package/presets/claude/php-laravel/agents/qa-tester.md +4 -0
- package/presets/claude/php-laravel/agents/task-router.md +1 -0
- package/presets/claude/php-laravel/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/claude/svelte/agents/build-verifier.md +2 -0
- package/presets/claude/svelte/agents/code-reviewer.md +2 -0
- package/presets/claude/svelte/agents/debugger.md +4 -0
- package/presets/claude/svelte/agents/feature-developer.md +3 -0
- package/presets/claude/svelte/agents/qa-tester.md +4 -0
- package/presets/claude/svelte/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +17 -1
- package/presets/cursor/android-kotlin/agents/build-verifier.md +2 -0
- package/presets/cursor/android-kotlin/agents/code-reviewer.md +2 -0
- package/presets/cursor/android-kotlin/agents/debugger.md +1 -0
- package/presets/cursor/android-kotlin/agents/feature-developer.md +3 -0
- package/presets/cursor/android-kotlin/agents/qa-tester.md +2 -0
- package/presets/cursor/android-kotlin/agents/task-router.md +4 -0
- package/presets/cursor/android-kotlin/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/go/agents/build-verifier.md +4 -0
- package/presets/cursor/go/agents/code-reviewer.md +4 -0
- package/presets/cursor/go/agents/debugger.md +4 -0
- package/presets/cursor/go/agents/feature-developer.md +5 -0
- package/presets/cursor/go/agents/qa-tester.md +4 -0
- package/presets/cursor/go/agents/task-router.md +4 -0
- package/presets/cursor/go/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/go/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/go/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/ios-swift/agents/build-verifier.md +2 -0
- package/presets/cursor/ios-swift/agents/code-reviewer.md +2 -0
- package/presets/cursor/ios-swift/agents/debugger.md +1 -0
- package/presets/cursor/ios-swift/agents/feature-developer.md +3 -0
- package/presets/cursor/ios-swift/agents/qa-tester.md +2 -0
- package/presets/cursor/ios-swift/agents/task-router.md +4 -0
- package/presets/cursor/ios-swift/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/java/agents/build-verifier.md +4 -0
- package/presets/cursor/java/agents/code-reviewer.md +4 -0
- package/presets/cursor/java/agents/debugger.md +4 -0
- package/presets/cursor/java/agents/feature-developer.md +5 -0
- package/presets/cursor/java/agents/qa-tester.md +4 -0
- package/presets/cursor/java/agents/task-router.md +4 -0
- package/presets/cursor/java/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/java/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/java/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/mcp-ts/agents/build-verifier.md +4 -0
- package/presets/cursor/mcp-ts/agents/feature-developer.md +5 -0
- package/presets/cursor/mcp-ts/agents/task-router.md +1 -1
- package/presets/cursor/mcp-ts/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/next/agents/build-verifier.md +2 -0
- package/presets/cursor/next/agents/code-reviewer.md +2 -0
- package/presets/cursor/next/agents/debugger.md +4 -0
- package/presets/cursor/next/agents/feature-developer.md +3 -0
- package/presets/cursor/next/agents/qa-tester.md +4 -0
- package/presets/cursor/next/agents/unit-test-generator.md +1 -1
- package/presets/cursor/next/agents/unit-test-planner.md +1 -1
- package/presets/cursor/next/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/next/rules/http-client.mdc +1 -1
- package/presets/cursor/next/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/next/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/next/rules/react-ui.mdc +1 -1
- package/presets/cursor/next/rules/reference-features.mdc +1 -1
- package/presets/cursor/next/rules/tests-e2e-structure.mdc +2 -0
- package/presets/cursor/next/rules/tests-unit.mdc +3 -5
- package/presets/cursor/next/skills/playwright-e2e/SKILL.md +5 -3
- package/presets/cursor/next/skills/unit-testing/SKILL.md +6 -4
- package/presets/cursor/nuxt/agents/build-verifier.md +2 -0
- package/presets/cursor/nuxt/agents/code-reviewer.md +2 -0
- package/presets/cursor/nuxt/agents/debugger.md +4 -0
- package/presets/cursor/nuxt/agents/feature-developer.md +3 -0
- package/presets/cursor/nuxt/agents/qa-tester.md +4 -0
- package/presets/cursor/nuxt/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/php-hexagonal/agents/build-verifier.md +4 -0
- package/presets/cursor/php-hexagonal/agents/code-reviewer.md +4 -0
- package/presets/cursor/php-hexagonal/agents/debugger.md +4 -0
- package/presets/cursor/php-hexagonal/agents/feature-developer.md +5 -0
- package/presets/cursor/php-hexagonal/agents/qa-tester.md +4 -0
- package/presets/cursor/php-hexagonal/agents/task-router.md +4 -0
- package/presets/cursor/php-hexagonal/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/php-laravel/agents/build-verifier.md +4 -0
- package/presets/cursor/php-laravel/agents/code-reviewer.md +4 -0
- package/presets/cursor/php-laravel/agents/debugger.md +4 -0
- package/presets/cursor/php-laravel/agents/feature-developer.md +5 -0
- package/presets/cursor/php-laravel/agents/qa-tester.md +4 -0
- package/presets/cursor/php-laravel/agents/task-router.md +1 -0
- package/presets/cursor/php-laravel/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +17 -1
- package/presets/cursor/svelte/agents/build-verifier.md +2 -0
- package/presets/cursor/svelte/agents/code-reviewer.md +2 -0
- package/presets/cursor/svelte/agents/debugger.md +4 -0
- package/presets/cursor/svelte/agents/feature-developer.md +3 -0
- package/presets/cursor/svelte/agents/qa-tester.md +4 -0
- package/presets/cursor/svelte/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/svelte/rules/preset-twin-sync.mdc +17 -1
- package/presets/mavis/README.md +60 -0
- package/presets/mavis/next/MAVIS.md +67 -0
- package/presets/mavis/next/MCP.md +36 -0
- package/presets/mavis/next/README.md +15 -0
- package/presets/mavis/next/REPO_AGENTS.md +94 -0
- package/presets/mavis/next/adapter-map.json +959 -0
- package/presets/mavis/next/agents/README.md +206 -0
- package/presets/mavis/next/agents/accessibility-reviewer.md +61 -0
- package/presets/mavis/next/agents/api-contract-reviewer.md +57 -0
- package/presets/mavis/next/agents/build-verifier.md +110 -0
- package/presets/mavis/next/agents/ci-investigator.md +63 -0
- package/presets/mavis/next/agents/code-reviewer.md +61 -0
- package/presets/mavis/next/agents/codebase-analyzer.md +21 -0
- package/presets/mavis/next/agents/debugger.md +80 -0
- package/presets/mavis/next/agents/feature-developer.md +101 -0
- package/presets/mavis/next/agents/migration-specialist.md +67 -0
- package/presets/mavis/next/agents/mockup-reviewer.md +53 -0
- package/presets/mavis/next/agents/openspec-author.md +157 -0
- package/presets/mavis/next/agents/performance-auditor.md +68 -0
- package/presets/mavis/next/agents/playwright-test-generator.md +28 -0
- package/presets/mavis/next/agents/playwright-test-healer.md +28 -0
- package/presets/mavis/next/agents/playwright-test-planner.md +21 -0
- package/presets/mavis/next/agents/qa-tester.md +52 -0
- package/presets/mavis/next/agents/security-reviewer.md +54 -0
- package/presets/mavis/next/agents/solution-architect.md +68 -0
- package/presets/mavis/next/agents/task-analyst.md +166 -0
- package/presets/mavis/next/agents/task-router.md +178 -0
- package/presets/mavis/next/agents/tech-writer.md +57 -0
- package/presets/mavis/next/agents/unit-test-generator.md +31 -0
- package/presets/mavis/next/agents/unit-test-healer.md +32 -0
- package/presets/mavis/next/agents/unit-test-planner.md +54 -0
- package/presets/mavis/next/commands/README.md +49 -0
- package/presets/mavis/next/commands/spec-start.md +24 -0
- package/presets/mavis/next/commands/task-continue.md +50 -0
- package/presets/mavis/next/commands/task.md +50 -0
- package/presets/mavis/next/commands/technical-retro.md +80 -0
- package/presets/mavis/next/hooks/README.md +22 -0
- package/presets/mavis/next/hooks/chain-team-phases.sh +845 -0
- package/presets/mavis/next/mcp.json +19 -0
- package/presets/mavis/next/rules/README.md +16 -0
- package/presets/mavis/next/rules/agent-team-intake.md +11 -0
- package/presets/mavis/next/rules/agent-team-orchestrator.md +210 -0
- package/presets/mavis/next/rules/ai-local-overlay.md +37 -0
- package/presets/mavis/next/rules/anti-sycophancy-discipline.md +23 -0
- package/presets/mavis/next/rules/api-services.md +72 -0
- package/presets/mavis/next/rules/architecture-boundaries-ui.md +9 -0
- package/presets/mavis/next/rules/architecture-boundaries.md +86 -0
- package/presets/mavis/next/rules/arrow-functions.md +25 -0
- package/presets/mavis/next/rules/code-quality-and-refactoring.md +40 -0
- package/presets/mavis/next/rules/code-review-mr.md +40 -0
- package/presets/mavis/next/rules/css-property-order-stylelint.md +5 -0
- package/presets/mavis/next/rules/design-guidance.md +99 -0
- package/presets/mavis/next/rules/feature-delivery-workflow.md +49 -0
- package/presets/mavis/next/rules/http-client.md +55 -0
- package/presets/mavis/next/rules/layer-barrel-exports.md +68 -0
- package/presets/mavis/next/rules/mcp-usage.md +86 -0
- package/presets/mavis/next/rules/mockup-review.md +107 -0
- package/presets/mavis/next/rules/navigation-router-stack.md +56 -0
- package/presets/mavis/next/rules/navigation-router-ui.md +10 -0
- package/presets/mavis/next/rules/next-app-core.md +29 -0
- package/presets/mavis/next/rules/next-app-router.md +44 -0
- package/presets/mavis/next/rules/no-props-spread.md +56 -0
- package/presets/mavis/next/rules/no-type-assertion-as-import-export.md +51 -0
- package/presets/mavis/next/rules/package-manager.md +11 -0
- package/presets/mavis/next/rules/playwright-agents.md +67 -0
- package/presets/mavis/next/rules/post-change-lint.md +35 -0
- package/presets/mavis/next/rules/preset-layering.md +27 -0
- package/presets/mavis/next/rules/preset-no-cross-stack-leakage.md +90 -0
- package/presets/mavis/next/rules/preset-pr-checklist.md +35 -0
- package/presets/mavis/next/rules/preset-token-budget.md +36 -0
- package/presets/mavis/next/rules/preset-twin-sync.md +65 -0
- package/presets/mavis/next/rules/product-specs-authoring.md +112 -0
- package/presets/mavis/next/rules/product-specs.md +56 -0
- package/presets/mavis/next/rules/public-imports.md +50 -0
- package/presets/mavis/next/rules/react-a11y-coding.md +31 -0
- package/presets/mavis/next/rules/react-next-anti-hallucination.md +60 -0
- package/presets/mavis/next/rules/react-ui.md +105 -0
- package/presets/mavis/next/rules/reference-features.md +45 -0
- package/presets/mavis/next/rules/security-next.md +45 -0
- package/presets/mavis/next/rules/store-rtk.md +78 -0
- package/presets/mavis/next/rules/technical-retro.md +11 -0
- package/presets/mavis/next/rules/tests-e2e-structure.md +72 -0
- package/presets/mavis/next/rules/tests-unit.md +72 -0
- package/presets/mavis/next/rules/types-jsdoc.md +43 -0
- package/presets/mavis/next/rules/ux-design.md +99 -0
- package/presets/mavis/next/skills/README.md +21 -0
- package/presets/mavis/next/skills/ci-investigation/SKILL.md +39 -0
- package/presets/mavis/next/skills/code-review/SKILL.md +39 -0
- package/presets/mavis/next/skills/debug-investigation/SKILL.md +26 -0
- package/presets/mavis/next/skills/feature-delivery/SKILL.md +34 -0
- package/presets/mavis/next/skills/mockup-review/SKILL.md +24 -0
- package/presets/mavis/next/skills/playwright-e2e/SKILL.md +28 -0
- package/presets/mavis/next/skills/react-performance/SKILL.md +39 -0
- package/presets/mavis/next/skills/spec-authoring/SKILL.md +21 -0
- package/presets/mavis/next/skills/spec-discovery/SKILL.md +53 -0
- package/presets/mavis/next/skills/technical-retro/SKILL.md +49 -0
- package/presets/mavis/next/skills/unit-testing/SKILL.md +29 -0
- package/presets/mavis/next/skills/ux-design/SKILL.md +31 -0
- package/presets/mavis/next/skills/write-adr/SKILL.md +36 -0
- package/presets/mavis/next/team/README.md +148 -0
- package/presets/mavis/next/team/conventions.md +21 -0
- package/presets/mavis/next/team/fixtures/artifact-manifest.example.json +79 -0
- package/presets/mavis/next/team/fixtures/bugfix-standard.json +25 -0
- package/presets/mavis/next/team/fixtures/feature-full.json +29 -0
- package/presets/mavis/next/team/fixtures/feature-light.json +27 -0
- package/presets/mavis/next/team/fixtures/feature-with-figma-url.json +17 -0
- package/presets/mavis/next/team/fixtures/feature-with-ux-no-figma.json +17 -0
- package/presets/mavis/next/team/fixtures/memlog.example.md +9 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/human-gate/metrics.json +69 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/missing-ledger/status.json +5 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/parallel-receipts/metrics.json +79 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/serial-retry/metrics.json +124 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/validation-failed/metrics.json +124 -0
- package/presets/mavis/next/team/fixtures/metrics.example.json +54 -0
- package/presets/mavis/next/team/fixtures/mockup-review-fail-embedded-status.json +8 -0
- package/presets/mavis/next/team/fixtures/mockup-review-standalone.json +14 -0
- package/presets/mavis/next/team/fixtures/ux-design-standalone.json +15 -0
- package/presets/mavis/next/team/tasks/.gitkeep +0 -0
- package/presets/mavis/platform-contract.json +131 -0
- package/scripts/capture-cli-golden.mjs +44 -0
- package/scripts/check-chain-hook-coverage.sh +1 -0
- package/scripts/check-mavis-adapter-map.mjs +294 -0
- package/scripts/check-mavis-local-overlay.mjs +56 -0
- package/scripts/check-mavis-platform-contract.mjs +388 -0
- package/scripts/check-mavis-token-budget.mjs +48 -0
- package/scripts/check-preset-leakage.sh +35 -3
- package/scripts/check-preset-structure.sh +73 -0
- package/scripts/check-preset-token-budget.sh +100 -0
- package/scripts/check-shared-core-drift.sh +18 -0
- package/scripts/check-task-router-intents.sh +154 -0
- package/scripts/fixtures/cli-golden/claude/next.json +130 -0
- package/scripts/fixtures/cli-golden/cursor/next.json +132 -0
- package/scripts/fixtures/cli-golden/mavis/next.json +125 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/AGENTS.md +1 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/extra.mdc +1 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/sample.mdc +2 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/MAVIS.md +1 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/adapter-map.json +26 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/rules/sample.md +2 -0
- package/scripts/fixtures/mavis-adapter-map/valid/cursor/AGENTS.md +1 -0
- package/scripts/fixtures/mavis-adapter-map/valid/cursor/rules/sample.mdc +2 -0
- package/scripts/fixtures/mavis-adapter-map/valid/mavis/MAVIS.md +1 -0
- package/scripts/fixtures/mavis-adapter-map/valid/mavis/adapter-map.json +26 -0
- package/scripts/fixtures/mavis-adapter-map/valid/mavis/rules/sample.md +2 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-minimax-destination.json +131 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-missing-capability.json +126 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-released-unknown-stack.json +133 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-unknown-native-path.json +131 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-version-pin.json +132 -0
- package/scripts/fixtures/mavis-platform-contract/valid.json +131 -0
- package/scripts/fixtures/mavis-probe/AGENTS.md +6 -0
- package/scripts/fixtures/mavis-probe/agents/probe-agent.md +6 -0
- package/scripts/fixtures/mavis-probe/fake-mcode.mjs +98 -0
- package/scripts/fixtures/mavis-probe/output.schema.json +28 -0
- package/scripts/fixtures/mavis-probe/rules/probe.md +5 -0
- package/scripts/fixtures/mavis-probe/skills/probe/SKILL.md +11 -0
- package/scripts/generate-mavis-next.mjs +704 -0
- package/scripts/lib/cli-golden.mjs +50 -0
- package/scripts/lib/discover-hooks.mjs +72 -0
- package/scripts/probe-mavis-capabilities.mjs +480 -0
- package/scripts/test-agent-task-metrics-hooks.mjs +120 -16
- package/scripts/test-chain-team-phases-coverage.mjs +110 -8
- package/scripts/test-cli-init-clean.mjs +256 -0
- package/scripts/test-cli-mcp-merge.mjs +23 -0
- package/scripts/test-mavis-context-budget.mjs +55 -0
- package/scripts/test-mavis-orchestration.mjs +649 -0
- package/scripts/test-probe-mavis-capabilities.mjs +205 -0
- package/scripts/test-task-router-intents-fixtures.mjs +109 -0
|
@@ -9,12 +9,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const packageDirectory = path.resolve(scriptDirectory, '..');
|
|
11
11
|
const presetsDirectory = path.join(packageDirectory, 'presets');
|
|
12
|
-
|
|
13
|
-
...fs.readdirSync(path.join(presetsDirectory, 'cursor')).map((stack) => path.join(presetsDirectory, 'cursor', stack, 'hooks', 'chain-team-phases.sh')),
|
|
14
|
-
...fs.readdirSync(path.join(presetsDirectory, 'claude')).map((stack) => path.join(presetsDirectory, 'claude', stack, 'hooks', 'chain-team-phases.sh')),
|
|
15
|
-
];
|
|
12
|
+
import { discoverChainHooks, expectedHookCount, loadReleasedMavisStacks, platformFromHook } from './lib/discover-hooks.mjs';
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
const hooks = discoverChainHooks(presetsDirectory);
|
|
15
|
+
|
|
16
|
+
assert.equal(
|
|
17
|
+
hooks.length,
|
|
18
|
+
expectedHookCount(presetsDirectory),
|
|
19
|
+
`expected ${expectedHookCount(presetsDirectory)} packaged hooks`,
|
|
20
|
+
);
|
|
18
21
|
|
|
19
22
|
function readJson(filePath) {
|
|
20
23
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -55,7 +58,7 @@ function invokeHookAsync(hook, workingDirectory, attemptId) {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {}) {
|
|
58
|
-
const platform = hook
|
|
61
|
+
const platform = platformFromHook(hook);
|
|
59
62
|
const teamRoot = path.join(root, `.${platform}`, 'team');
|
|
60
63
|
const taskDirectory = path.join(teamRoot, 'tasks', slug);
|
|
61
64
|
fs.mkdirSync(taskDirectory, { recursive: true });
|
|
@@ -70,7 +73,7 @@ function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {})
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
function prepareTask(root, hook) {
|
|
73
|
-
const platform = hook
|
|
76
|
+
const platform = platformFromHook(hook);
|
|
74
77
|
const teamRoot = path.join(root, `.${platform}`, 'team');
|
|
75
78
|
const taskDirectory = path.join(teamRoot, 'tasks', 'reverse-parallel');
|
|
76
79
|
fs.mkdirSync(taskDirectory, { recursive: true });
|
|
@@ -162,7 +165,7 @@ for (const hook of hooks) {
|
|
|
162
165
|
for (const hook of hooks) {
|
|
163
166
|
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-retry-'));
|
|
164
167
|
try {
|
|
165
|
-
const platform = hook
|
|
168
|
+
const platform = platformFromHook(hook);
|
|
166
169
|
const runId = 'serial-retry-run';
|
|
167
170
|
const reviewerAttemptId = `${runId}:step-5:security-reviewer:attempt-1`;
|
|
168
171
|
const taskDirectory = writeTask(
|
|
@@ -242,7 +245,7 @@ for (const hook of hooks) {
|
|
|
242
245
|
for (const hook of hooks) {
|
|
243
246
|
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-no-chain-'));
|
|
244
247
|
try {
|
|
245
|
-
const platform = hook
|
|
248
|
+
const platform = platformFromHook(hook);
|
|
246
249
|
const runId = 'no-chain-run';
|
|
247
250
|
const attemptId = `${runId}:step-0:feature-developer:attempt-1`;
|
|
248
251
|
const taskDirectory = writeTask(
|
|
@@ -293,7 +296,7 @@ for (const hook of hooks) {
|
|
|
293
296
|
for (const hook of hooks) {
|
|
294
297
|
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-mixed-parallel-'));
|
|
295
298
|
try {
|
|
296
|
-
const platform = hook
|
|
299
|
+
const platform = platformFromHook(hook);
|
|
297
300
|
const runId = 'mixed-parallel-run';
|
|
298
301
|
const alphaAttemptId = `${runId}:step-1:code-reviewer:attempt-1`;
|
|
299
302
|
const betaAttemptId = `${runId}:step-1:security-reviewer:attempt-1`;
|
|
@@ -360,7 +363,7 @@ for (const hook of hooks) {
|
|
|
360
363
|
for (const hook of hooks) {
|
|
361
364
|
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-skip-'));
|
|
362
365
|
try {
|
|
363
|
-
const platform = hook
|
|
366
|
+
const platform = platformFromHook(hook);
|
|
364
367
|
const runId = 'skip-run';
|
|
365
368
|
const starterAttemptId = `${runId}:step-0:starter:attempt-1`;
|
|
366
369
|
const taskDirectory = writeTask(
|
|
@@ -418,6 +421,9 @@ for (const hook of hooks) {
|
|
|
418
421
|
for (const command of [
|
|
419
422
|
...fs.readdirSync(path.join(presetsDirectory, 'cursor')).map((stack) => path.join(presetsDirectory, 'cursor', stack, 'commands', 'task-continue.md')),
|
|
420
423
|
...fs.readdirSync(path.join(presetsDirectory, 'claude')).map((stack) => path.join(presetsDirectory, 'claude', stack, 'commands', 'task-continue.md')),
|
|
424
|
+
...loadReleasedMavisStacks(presetsDirectory).map((stack) =>
|
|
425
|
+
path.join(presetsDirectory, 'mavis', stack, 'commands', 'task-continue.md'),
|
|
426
|
+
),
|
|
421
427
|
]) {
|
|
422
428
|
const source = fs.readFileSync(command, 'utf8');
|
|
423
429
|
assert.match(source, /action":"end_human_gate"/, `${command}: must invoke installed hook end_human_gate action`);
|
|
@@ -436,7 +442,7 @@ for (const hook of hooks) {
|
|
|
436
442
|
// pre-resume status → hook end_human_gate (no manual pipelineIndex++) → invoke from status.currentAgent.
|
|
437
443
|
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-gate-'));
|
|
438
444
|
try {
|
|
439
|
-
const platform = hook
|
|
445
|
+
const platform = platformFromHook(hook);
|
|
440
446
|
const stack = path.basename(path.dirname(path.dirname(hook)));
|
|
441
447
|
const presetCommand = path.join(path.dirname(path.dirname(hook)), 'commands', 'task-continue.md');
|
|
442
448
|
const installedHook = path.join(root, `.${platform}`, 'hooks', 'chain-team-phases.sh');
|
|
@@ -510,7 +516,7 @@ for (const hook of hooks) {
|
|
|
510
516
|
for (const hook of hooks) {
|
|
511
517
|
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-manifest-reconcile-'));
|
|
512
518
|
try {
|
|
513
|
-
const platform = hook
|
|
519
|
+
const platform = platformFromHook(hook);
|
|
514
520
|
const runId = 'manifest-reconcile-run';
|
|
515
521
|
const reviewerAttemptId = `${runId}:step-1:code-reviewer:attempt-1`;
|
|
516
522
|
const nowIso = '2026-01-01T00:00:00.000Z';
|
|
@@ -617,7 +623,7 @@ for (const hook of hooks) {
|
|
|
617
623
|
for (const hook of hooks) {
|
|
618
624
|
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-blocked-receipt-'));
|
|
619
625
|
try {
|
|
620
|
-
const platform = hook
|
|
626
|
+
const platform = platformFromHook(hook);
|
|
621
627
|
const runId = 'blocked-receipt-run';
|
|
622
628
|
const attemptId = `${runId}:step-0:feature-developer:attempt-1`;
|
|
623
629
|
const startedAt = '2026-02-01T00:00:00.000Z';
|
|
@@ -708,7 +714,7 @@ for (const hook of hooks) {
|
|
|
708
714
|
for (const hook of hooks) {
|
|
709
715
|
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-stale-receipt-'));
|
|
710
716
|
try {
|
|
711
|
-
const platform = hook
|
|
717
|
+
const platform = platformFromHook(hook);
|
|
712
718
|
const runId = 'stale-receipt-run';
|
|
713
719
|
const attemptId = `${runId}:step-2:code-reviewer:attempt-2`;
|
|
714
720
|
const startedAt = '2026-02-01T00:00:00.000Z';
|
|
@@ -892,4 +898,102 @@ for (const hook of hooks) {
|
|
|
892
898
|
}
|
|
893
899
|
}
|
|
894
900
|
|
|
895
|
-
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
// Test — OD-1: parallel-stop lock race. The hook's waitForMetricsLock must
|
|
903
|
+
// actually block a second writer when the lock file is held. We pre-acquire
|
|
904
|
+
// `metrics.json.lock` (fs.openSync(_, 'wx') — same primitive the hook uses)
|
|
905
|
+
// in the test, then invoke the hook concurrently; the hook must NOT acquire
|
|
906
|
+
// the lock until the test releases it. Without the real lock primitive the
|
|
907
|
+
// hook would race through, run the metrics write, and leave a corrupt ledger.
|
|
908
|
+
// ---------------------------------------------------------------------------
|
|
909
|
+
for (const hook of hooks) {
|
|
910
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-lock-race-'));
|
|
911
|
+
try {
|
|
912
|
+
const platform = platformFromHook(hook);
|
|
913
|
+
const runId = 'lock-race-run';
|
|
914
|
+
const attemptId = `${runId}:step-0:feature-developer:attempt-1`;
|
|
915
|
+
const taskDirectory = writeTask(
|
|
916
|
+
root,
|
|
917
|
+
hook,
|
|
918
|
+
'lock-race',
|
|
919
|
+
{ autoChain: true, steps: [{ agent: 'feature-developer' }] },
|
|
920
|
+
{
|
|
921
|
+
slug: 'lock-race',
|
|
922
|
+
intent: 'feature',
|
|
923
|
+
pipelineIndex: 0,
|
|
924
|
+
currentAgent: 'feature-developer',
|
|
925
|
+
phase: 'executing',
|
|
926
|
+
state: 'completed',
|
|
927
|
+
activeAttempts: {
|
|
928
|
+
[attemptId]: {
|
|
929
|
+
agent: 'feature-developer',
|
|
930
|
+
stepIndex: 0,
|
|
931
|
+
attempt: 1,
|
|
932
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
933
|
+
parallelGroupId: null,
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
metricsRunId: runId,
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
schemaVersion: 1,
|
|
940
|
+
task: { runId, slug: 'lock-race', preset: 'ai-rules', stack: 'test', platform, intent: 'feature', profile: 'test' },
|
|
941
|
+
events: [
|
|
942
|
+
{ eventId: `${runId}:task_started`, type: 'task_started', at: '2026-01-01T00:00:00.000Z' },
|
|
943
|
+
{ eventId: `${runId}:start`, type: 'attempt_started', attemptId, agent: 'feature-developer', stepIndex: 0, attempt: 1, at: '2026-01-01T00:00:00.000Z' },
|
|
944
|
+
],
|
|
945
|
+
},
|
|
946
|
+
);
|
|
947
|
+
const metricsPath = path.join(taskDirectory, 'metrics.json');
|
|
948
|
+
const lockPath = `${metricsPath}.lock`;
|
|
949
|
+
|
|
950
|
+
// Pre-acquire the lock the same way the hook does (fs.openSync 'wx').
|
|
951
|
+
const holderFd = fs.openSync(lockPath, 'wx');
|
|
952
|
+
|
|
953
|
+
// Spawn the hook while the lock is held. The hook must wait.
|
|
954
|
+
const child = spawn('bash', [hook], { cwd: root, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
955
|
+
let stderr = '';
|
|
956
|
+
let stdout = '';
|
|
957
|
+
child.stderr.setEncoding('utf8');
|
|
958
|
+
child.stdout.setEncoding('utf8');
|
|
959
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
960
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
961
|
+
child.stdin.end(JSON.stringify({ status: 'completed', attemptId }));
|
|
962
|
+
|
|
963
|
+
// Give the child time to enter waitForMetricsLock and hit the EEXIST retry path.
|
|
964
|
+
const holdMs = 60;
|
|
965
|
+
await new Promise((resolve) => setTimeout(resolve, holdMs));
|
|
966
|
+
assert.ok(
|
|
967
|
+
child.exitCode === null,
|
|
968
|
+
`${hook}: hook exited prematurely while lock was held (stdout=${stdout.slice(0, 200)} stderr=${stderr.slice(0, 200)})`,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// Release the lock — child should now proceed.
|
|
972
|
+
const releaseAt = Date.now();
|
|
973
|
+
fs.closeSync(holderFd);
|
|
974
|
+
fs.unlinkSync(lockPath);
|
|
975
|
+
|
|
976
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
977
|
+
child.on('error', reject);
|
|
978
|
+
child.on('close', resolve);
|
|
979
|
+
});
|
|
980
|
+
const elapsed = Date.now() - releaseAt;
|
|
981
|
+
assert.equal(exitCode, 0, `${hook}: hook did not exit 0 after lock release (stderr=${stderr.slice(0, 400)})`);
|
|
982
|
+
|
|
983
|
+
// The hook's waitForMetricsLock busy-waits on EEXIST in 10 ms slices; with a
|
|
984
|
+
// 60 ms hold the child should have been blocked for >= holdMs and run to
|
|
985
|
+
// completion in well under the 2 s ceiling. Both assertions guard against
|
|
986
|
+
// regressions to the fake Atomics.wait impl (which never blocked).
|
|
987
|
+
assert.ok(elapsed >= 1, `${hook}: hook finished suspiciously fast after lock release (${elapsed} ms)`);
|
|
988
|
+
assert.ok(elapsed < 1500, `${hook}: hook took ${elapsed} ms after lock release — lock impl may be stuck`);
|
|
989
|
+
const ledger = readJson(metricsPath);
|
|
990
|
+
const finished = ledger.events.find(
|
|
991
|
+
(event) => event.type === 'attempt_finished' && event.attemptId === attemptId,
|
|
992
|
+
);
|
|
993
|
+
assert.ok(finished, `${hook}: lock-race did not emit attempt_finished; ledger=${JSON.stringify(ledger.events.map((e) => e.type))}`);
|
|
994
|
+
} finally {
|
|
995
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
console.log(`Verified retry, parallel, skip, gate, manifest-reconcile, lock-race, and aggregation lifecycle in ${hooks.length} hooks.`);
|
|
@@ -32,16 +32,19 @@ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
|
32
32
|
const packageDirectory = path.resolve(scriptDirectory, '..');
|
|
33
33
|
const presetsDirectory = path.join(packageDirectory, 'presets');
|
|
34
34
|
|
|
35
|
-
|
|
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
|
-
];
|
|
35
|
+
import { discoverChainHooks, expectedHookCount, platformFromHook } from './lib/discover-hooks.mjs';
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
const hooks = discoverChainHooks(presetsDirectory);
|
|
38
|
+
|
|
39
|
+
assert.equal(
|
|
40
|
+
hooks.length,
|
|
41
|
+
expectedHookCount(presetsDirectory),
|
|
42
|
+
`expected ${expectedHookCount(presetsDirectory)} packaged hooks, got ${hooks.length}`,
|
|
43
|
+
);
|
|
41
44
|
|
|
42
45
|
// mcp-ts does not ship openspec-author (per preset-no-cross-stack-leakage).
|
|
43
46
|
const HOOKS_WITH_OPENSPEC = hooks.filter((hook) => !hook.includes(`${path.sep}mcp-ts${path.sep}`));
|
|
44
|
-
assert.equal(HOOKS_WITH_OPENSPEC.length,
|
|
47
|
+
assert.equal(HOOKS_WITH_OPENSPEC.length, 19, 'expected 19 hooks to cover openspec-author');
|
|
45
48
|
|
|
46
49
|
function readJson(filePath) {
|
|
47
50
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -60,7 +63,7 @@ function invokeHook(hook, workingDirectory, attemptId, payload = {}) {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
function platformFor(hook) {
|
|
63
|
-
return hook
|
|
66
|
+
return platformFromHook(hook);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {}) {
|
|
@@ -408,4 +411,103 @@ for (const hook of HOOKS_WITH_OPENSPEC) {
|
|
|
408
411
|
}
|
|
409
412
|
}
|
|
410
413
|
|
|
411
|
-
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Test 6 — stale `attemptId` rejection (WP-1 receipt-binding callout):
|
|
416
|
+
// the hook's `readBoundReceiptOutcome` rejects entries whose `receipt.attemptId`
|
|
417
|
+
// does not match the active attempt, even when `completedAt` is fresh. A
|
|
418
|
+
// receipt bound to a prior attempt ID (e.g. attempt-1 reused after a retry
|
|
419
|
+
// recycled the slot) must not advance the pipeline.
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
for (const hook of hooks) {
|
|
422
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.stale-attempt-id-'));
|
|
423
|
+
try {
|
|
424
|
+
const platform = platformFor(hook);
|
|
425
|
+
const runId = 'stale-attempt-id-run';
|
|
426
|
+
const devAttemptId = `${runId}:step-0:feature-developer:attempt-2`;
|
|
427
|
+
const freshIso = new Date().toISOString();
|
|
428
|
+
writeTask(
|
|
429
|
+
root,
|
|
430
|
+
hook,
|
|
431
|
+
'stale-attempt-id',
|
|
432
|
+
{
|
|
433
|
+
autoChain: true,
|
|
434
|
+
steps: [
|
|
435
|
+
{ agent: 'feature-developer', artifactOutputId: 'implementation' },
|
|
436
|
+
{ agent: 'build-verifier', artifactInputIds: ['implementation'], artifactOutputId: 'validation' },
|
|
437
|
+
],
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
slug: 'stale-attempt-id',
|
|
441
|
+
intent: 'feature',
|
|
442
|
+
pipelineIndex: 0,
|
|
443
|
+
currentAgent: 'feature-developer',
|
|
444
|
+
phase: 'executing',
|
|
445
|
+
state: 'in_progress',
|
|
446
|
+
activeAttempts: {
|
|
447
|
+
[devAttemptId]: {
|
|
448
|
+
agent: 'feature-developer',
|
|
449
|
+
stepIndex: 0,
|
|
450
|
+
attempt: 2,
|
|
451
|
+
startedAt: freshIso,
|
|
452
|
+
parallelGroupId: null,
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
metricsRunId: runId,
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
schemaVersion: 1,
|
|
459
|
+
task: { runId, slug: 'stale-attempt-id', preset: 'ai-rules', stack: 'test', platform, intent: 'feature', profile: 'standard' },
|
|
460
|
+
events: [
|
|
461
|
+
{ eventId: `${runId}:task_started`, type: 'task_started', at: freshIso },
|
|
462
|
+
{ eventId: `${runId}:dev-start`, type: 'attempt_started', attemptId: devAttemptId, agent: 'feature-developer', stepIndex: 0, attempt: 2, at: freshIso },
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
'artifact-manifest.json': `${JSON.stringify(
|
|
467
|
+
{
|
|
468
|
+
schemaVersion: 1,
|
|
469
|
+
slug: 'stale-attempt-id',
|
|
470
|
+
updatedAt: freshIso,
|
|
471
|
+
artifacts: [
|
|
472
|
+
{
|
|
473
|
+
id: 'implementation',
|
|
474
|
+
producer: 'feature-developer',
|
|
475
|
+
status: 'completed',
|
|
476
|
+
authoritativePaths: ['implementation.md'],
|
|
477
|
+
acceptanceCriteriaIds: ['AC-1'],
|
|
478
|
+
taskIds: ['T1'],
|
|
479
|
+
timestamps: { createdAt: freshIso, updatedAt: freshIso, completedAt: freshIso },
|
|
480
|
+
receipt: {
|
|
481
|
+
outcome: 'completed',
|
|
482
|
+
// Stale: belongs to attempt-1 of the same (step, agent); the active attempt is attempt-2.
|
|
483
|
+
attemptId: `${runId}:step-0:feature-developer:attempt-1`,
|
|
484
|
+
summary: 'Receipt bound to a prior attempt; should be rejected.',
|
|
485
|
+
evidencePaths: ['implementation.md'],
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
null,
|
|
491
|
+
2,
|
|
492
|
+
)}\n`,
|
|
493
|
+
},
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const taskDirectory = path.join(root, `.${platform}`, 'team', 'tasks', 'stale-attempt-id');
|
|
497
|
+
const parsed = JSON.parse(invokeHook(hook, root, devAttemptId));
|
|
498
|
+
assert.deepEqual(parsed, {}, `${hook}: stale-attemptId receipt must not hand off`);
|
|
499
|
+
const status = readJson(path.join(taskDirectory, 'status.json'));
|
|
500
|
+
assert.equal(status.pipelineIndex, 0, `${hook}: stale-attemptId receipt advanced the pipeline`);
|
|
501
|
+
assert.equal(status.currentAgent, 'feature-developer', `${hook}: stale-attemptId receipt changed the active producer`);
|
|
502
|
+
assert.equal(status.state, 'awaiting_artifact_receipt', `${hook}: stale-attemptId did not enter receipt-wait state (got: ${status.state})`);
|
|
503
|
+
assert.ok(status.activeAttempts[devAttemptId], `${hook}: stale-attemptId consumed the active attempt`);
|
|
504
|
+
const finished = readJson(path.join(taskDirectory, 'metrics.json')).events.filter(
|
|
505
|
+
(event) => event.type === 'attempt_finished' && event.attemptId === devAttemptId,
|
|
506
|
+
);
|
|
507
|
+
assert.equal(finished.length, 0, `${hook}: stale-attemptId emitted terminal metrics`);
|
|
508
|
+
} finally {
|
|
509
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
console.log(`Verified openspec-author handoff, qa-tester handoff, pipeline misparse blocking, legacy fallback, writeStatus durability, and stale attemptId rejection in ${hooks.length} hooks.`);
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI init/clean goldens and Mavis P0 scenarios.
|
|
4
|
+
*/
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { runInit, snapshotInstall } from './lib/cli-golden.mjs';
|
|
12
|
+
|
|
13
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const packageDirectory = path.resolve(scriptDirectory, '..');
|
|
15
|
+
const cli = path.join(packageDirectory, 'bin', 'cli.js');
|
|
16
|
+
const goldenRoot = path.join(scriptDirectory, 'fixtures', 'cli-golden');
|
|
17
|
+
|
|
18
|
+
function readJson(filePath) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeJson(filePath, value) {
|
|
23
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
24
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runClean(cwd, tool) {
|
|
28
|
+
return execFileSync(process.execPath, [cli, 'clean', tool, '--cwd', cwd], {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runCli(cwd, args, env = {}) {
|
|
34
|
+
try {
|
|
35
|
+
return {
|
|
36
|
+
status: 0,
|
|
37
|
+
stdout: execFileSync(process.execPath, [cli, ...args, '--cwd', cwd], {
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
env: { ...process.env, ...env },
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
status: error.status ?? 1,
|
|
45
|
+
stdout: `${error.stdout || ''}`,
|
|
46
|
+
stderr: `${error.stderr || ''}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertGolden(tool, preset) {
|
|
52
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `ai-rules-golden-check-${tool}-`));
|
|
53
|
+
try {
|
|
54
|
+
runInit(cli, tmp, tool, preset);
|
|
55
|
+
const expected = readJson(path.join(goldenRoot, tool, `${preset}.json`));
|
|
56
|
+
const actual = snapshotInstall(tmp);
|
|
57
|
+
if (tool === 'mavis') {
|
|
58
|
+
delete actual['.mavis/.ai-rules-install.json'];
|
|
59
|
+
delete expected.files['.mavis/.ai-rules-install.json'];
|
|
60
|
+
}
|
|
61
|
+
assert.deepEqual(actual, expected.files, `${tool}/${preset} golden drifted`);
|
|
62
|
+
} finally {
|
|
63
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listHomeMinimax(home) {
|
|
68
|
+
const root = path.join(home, '.minimax');
|
|
69
|
+
if (!fs.existsSync(root)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const out = [];
|
|
73
|
+
const visit = (dir) => {
|
|
74
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
+
const full = path.join(dir, entry.name);
|
|
76
|
+
out.push(path.relative(root, full));
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
visit(full);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
visit(root);
|
|
83
|
+
return out.sort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
assertGolden('cursor', 'next');
|
|
87
|
+
assertGolden('claude', 'next');
|
|
88
|
+
|
|
89
|
+
const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-home-'));
|
|
90
|
+
fs.mkdirSync(path.join(isolatedHome, '.minimax'));
|
|
91
|
+
const homeBefore = listHomeMinimax(isolatedHome);
|
|
92
|
+
|
|
93
|
+
const fresh = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-fresh-'));
|
|
94
|
+
const initOut = execFileSync(
|
|
95
|
+
process.execPath,
|
|
96
|
+
[cli, 'init', 'mavis', '--preset', 'next', '--cwd', fresh],
|
|
97
|
+
{ encoding: 'utf8', env: { ...process.env, HOME: isolatedHome } },
|
|
98
|
+
);
|
|
99
|
+
assert.match(initOut, /\.mavis installed/);
|
|
100
|
+
assert.match(initOut, /do not write ~\/\.minimax\/mcp\.json/i);
|
|
101
|
+
assert.equal(fs.existsSync(path.join(fresh, 'AGENTS.md')), true);
|
|
102
|
+
assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'AGENTS.md')), true);
|
|
103
|
+
assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'rules', 'next-app-core.md')), true);
|
|
104
|
+
assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'hooks', 'chain-team-phases.sh')), true);
|
|
105
|
+
assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'mcp.json')), true);
|
|
106
|
+
assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'adapter-map.json')), false);
|
|
107
|
+
assert.equal(fs.existsSync(path.join(fresh, 'docs', 'specs', 'INDEX.md')), true);
|
|
108
|
+
const manifest = readJson(path.join(fresh, '.mavis', '.ai-rules-install.json'));
|
|
109
|
+
assert.equal(manifest.schemaVersion, 1);
|
|
110
|
+
assert.equal(manifest.platform, 'mavis');
|
|
111
|
+
assert.equal(manifest.preset, 'next');
|
|
112
|
+
assert.ok(manifest.managedFiles['AGENTS.md']);
|
|
113
|
+
assert.ok(manifest.shippedMcpServerIds.includes('context7'));
|
|
114
|
+
assert.equal(manifest.docsSeed.seeded, true);
|
|
115
|
+
|
|
116
|
+
const existingAgents = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-agents-'));
|
|
117
|
+
fs.writeFileSync(path.join(existingAgents, 'AGENTS.md'), '# consumer agents\n');
|
|
118
|
+
runInit(cli, existingAgents, 'mavis', 'next');
|
|
119
|
+
assert.match(fs.readFileSync(path.join(existingAgents, 'AGENTS.md'), 'utf8'), /Mavis compatibility entry/);
|
|
120
|
+
|
|
121
|
+
const specsExist = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-specs-'));
|
|
122
|
+
fs.mkdirSync(path.join(specsExist, 'docs', 'specs'), { recursive: true });
|
|
123
|
+
runInit(cli, specsExist, 'mavis', 'next');
|
|
124
|
+
assert.equal(fs.existsSync(path.join(specsExist, 'docs', 'specs', 'INDEX.md')), false);
|
|
125
|
+
assert.equal(
|
|
126
|
+
readJson(path.join(specsExist, '.mavis', '.ai-rules-install.json')).docsSeed.reason,
|
|
127
|
+
'directory-exists',
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
writeJson(path.join(fresh, '.mavis', 'mcp.json'), {
|
|
131
|
+
mcpServers: {
|
|
132
|
+
context7: {
|
|
133
|
+
command: 'npx',
|
|
134
|
+
args: ['-y', '@upstash/context7-mcp@old'],
|
|
135
|
+
env: { CONTEXT7_API_KEY: 'keep-me' },
|
|
136
|
+
},
|
|
137
|
+
storybook: { url: 'http://localhost:6006/mcp' },
|
|
138
|
+
},
|
|
139
|
+
localOnly: true,
|
|
140
|
+
});
|
|
141
|
+
const repeat = runInit(cli, fresh, 'mavis', 'next');
|
|
142
|
+
assert.match(repeat, /kept extra servers: storybook/);
|
|
143
|
+
const merged = readJson(path.join(fresh, '.mavis', 'mcp.json'));
|
|
144
|
+
assert.equal(merged.localOnly, true);
|
|
145
|
+
assert.ok(merged.mcpServers.storybook);
|
|
146
|
+
assert.equal(merged.mcpServers.context7.env.CONTEXT7_API_KEY, 'keep-me');
|
|
147
|
+
|
|
148
|
+
merged.localNote = true;
|
|
149
|
+
writeJson(path.join(fresh, '.mavis', 'mcp.json'), merged);
|
|
150
|
+
const modifiedRel = path.join(fresh, '.mavis', 'rules', 'next-app-core.md');
|
|
151
|
+
fs.appendFileSync(modifiedRel, '\n# local overlay note\n');
|
|
152
|
+
runClean(fresh, 'mavis');
|
|
153
|
+
assert.equal(fs.existsSync(modifiedRel), true);
|
|
154
|
+
assert.equal(fs.existsSync(path.join(fresh, '.mavis', '.ai-rules-install.json')), false);
|
|
155
|
+
assert.equal(fs.existsSync(path.join(fresh, 'AGENTS.md')), false);
|
|
156
|
+
assert.ok(readJson(path.join(fresh, '.mavis', 'mcp.json')).mcpServers.storybook);
|
|
157
|
+
assert.equal(readJson(path.join(fresh, '.mavis', 'mcp.json')).mcpServers.context7, undefined);
|
|
158
|
+
assert.equal(readJson(path.join(fresh, '.mavis', 'mcp.json')).localNote, true);
|
|
159
|
+
|
|
160
|
+
const missing = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-missing-'));
|
|
161
|
+
fs.writeFileSync(path.join(missing, 'keep.txt'), 'safe\n');
|
|
162
|
+
const missingClean = runCli(missing, ['clean', 'mavis']);
|
|
163
|
+
assert.equal(missingClean.status, 1);
|
|
164
|
+
assert.match(`${missingClean.stderr}${missingClean.stdout}`, /nothing removed/);
|
|
165
|
+
assert.equal(fs.readFileSync(path.join(missing, 'keep.txt'), 'utf8'), 'safe\n');
|
|
166
|
+
|
|
167
|
+
const corrupt = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-corrupt-'));
|
|
168
|
+
fs.mkdirSync(path.join(corrupt, '.mavis'), { recursive: true });
|
|
169
|
+
fs.writeFileSync(path.join(corrupt, '.mavis', '.ai-rules-install.json'), '{not json');
|
|
170
|
+
fs.writeFileSync(path.join(corrupt, '.mavis', 'keep.md'), 'keep\n');
|
|
171
|
+
const corruptClean = runCli(corrupt, ['clean', 'mavis']);
|
|
172
|
+
assert.equal(corruptClean.status, 1);
|
|
173
|
+
assert.equal(fs.readFileSync(path.join(corrupt, '.mavis', 'keep.md'), 'utf8'), 'keep\n');
|
|
174
|
+
|
|
175
|
+
const twice = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-twice-'));
|
|
176
|
+
runInit(cli, twice, 'mavis', 'next');
|
|
177
|
+
runClean(twice, 'mavis');
|
|
178
|
+
const secondClean = runCli(twice, ['clean', 'mavis']);
|
|
179
|
+
assert.equal(secondClean.status, 1);
|
|
180
|
+
|
|
181
|
+
const rollback = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-rollback-'));
|
|
182
|
+
fs.writeFileSync(path.join(rollback, 'AGENTS.md'), '# prior\n');
|
|
183
|
+
const fail = runCli(rollback, ['init', 'mavis', '--preset', 'next'], {
|
|
184
|
+
AI_RULES_MAVIS_FAIL_PHASE: 'write',
|
|
185
|
+
});
|
|
186
|
+
assert.equal(fail.status, 1);
|
|
187
|
+
assert.equal(fs.readFileSync(path.join(rollback, 'AGENTS.md'), 'utf8'), '# prior\n');
|
|
188
|
+
assert.equal(fs.existsSync(path.join(rollback, '.mavis')), false);
|
|
189
|
+
|
|
190
|
+
const failMcp = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-fail-mcp-'));
|
|
191
|
+
fs.writeFileSync(path.join(failMcp, 'AGENTS.md'), '# prior mcp\n');
|
|
192
|
+
const failMcpRun = runCli(failMcp, ['init', 'mavis', '--preset', 'next'], {
|
|
193
|
+
AI_RULES_MAVIS_FAIL_PHASE: 'mcp',
|
|
194
|
+
});
|
|
195
|
+
assert.equal(failMcpRun.status, 1);
|
|
196
|
+
assert.equal(fs.readFileSync(path.join(failMcp, 'AGENTS.md'), 'utf8'), '# prior mcp\n');
|
|
197
|
+
assert.equal(fs.existsSync(path.join(failMcp, '.mavis')), false);
|
|
198
|
+
|
|
199
|
+
const failManifest = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-fail-manifest-'));
|
|
200
|
+
fs.writeFileSync(path.join(failManifest, 'AGENTS.md'), '# prior manifest\n');
|
|
201
|
+
const failManifestRun = runCli(failManifest, ['init', 'mavis', '--preset', 'next'], {
|
|
202
|
+
AI_RULES_MAVIS_FAIL_PHASE: 'manifest',
|
|
203
|
+
});
|
|
204
|
+
assert.equal(failManifestRun.status, 1);
|
|
205
|
+
assert.equal(fs.readFileSync(path.join(failManifest, 'AGENTS.md'), 'utf8'), '# prior manifest\n');
|
|
206
|
+
assert.equal(fs.existsSync(path.join(failManifest, '.mavis')), false);
|
|
207
|
+
|
|
208
|
+
const toolFirst = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-tool-first-'));
|
|
209
|
+
execFileSync(process.execPath, [cli, 'mavis', 'init', '--preset', 'next', '--cwd', toolFirst], {
|
|
210
|
+
encoding: 'utf8',
|
|
211
|
+
});
|
|
212
|
+
assert.equal(fs.existsSync(path.join(toolFirst, '.mavis', 'AGENTS.md')), true);
|
|
213
|
+
|
|
214
|
+
const unreleased = runCli(fresh, ['init', 'mavis', '--preset', 'nuxt']);
|
|
215
|
+
assert.equal(unreleased.status, 1);
|
|
216
|
+
assert.match(`${unreleased.stderr}${unreleased.stdout}`, /not released yet/i);
|
|
217
|
+
assert.match(`${unreleased.stderr}${unreleased.stdout}`, /Released stacks: next/);
|
|
218
|
+
|
|
219
|
+
assert.deepEqual(listHomeMinimax(isolatedHome), homeBefore);
|
|
220
|
+
|
|
221
|
+
const mavisGoldenDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-golden-'));
|
|
222
|
+
runInit(cli, mavisGoldenDir, 'mavis', 'next');
|
|
223
|
+
const mavisFiles = snapshotInstall(mavisGoldenDir);
|
|
224
|
+
delete mavisFiles['.mavis/.ai-rules-install.json'];
|
|
225
|
+
const mavisGoldenPath = path.join(goldenRoot, 'mavis', 'next.json');
|
|
226
|
+
if (!fs.existsSync(mavisGoldenPath)) {
|
|
227
|
+
writeJson(mavisGoldenPath, {
|
|
228
|
+
schemaVersion: 1,
|
|
229
|
+
tool: 'mavis',
|
|
230
|
+
preset: 'next',
|
|
231
|
+
files: mavisFiles,
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
const expectedMavis = readJson(mavisGoldenPath);
|
|
235
|
+
delete expectedMavis.files['.mavis/.ai-rules-install.json'];
|
|
236
|
+
assert.deepEqual(mavisFiles, expectedMavis.files, 'mavis/next golden drifted');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const dir of [
|
|
240
|
+
isolatedHome,
|
|
241
|
+
fresh,
|
|
242
|
+
existingAgents,
|
|
243
|
+
specsExist,
|
|
244
|
+
missing,
|
|
245
|
+
corrupt,
|
|
246
|
+
twice,
|
|
247
|
+
rollback,
|
|
248
|
+
failMcp,
|
|
249
|
+
failManifest,
|
|
250
|
+
toolFirst,
|
|
251
|
+
mavisGoldenDir,
|
|
252
|
+
]) {
|
|
253
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log('test-cli-init-clean: ok');
|
|
@@ -131,6 +131,29 @@ try {
|
|
|
131
131
|
assert.ok(restored.mcpServers.playwright);
|
|
132
132
|
fs.rmSync(deletedDir, { recursive: true, force: true });
|
|
133
133
|
|
|
134
|
+
const mavisDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mcp-mavis-'));
|
|
135
|
+
const mavisMcp = path.join(mavisDir, '.mavis', 'mcp.json');
|
|
136
|
+
writeJson(mavisMcp, {
|
|
137
|
+
mcpServers: {
|
|
138
|
+
context7: {
|
|
139
|
+
command: 'npx',
|
|
140
|
+
args: ['-y', '@upstash/context7-mcp@old'],
|
|
141
|
+
env: { CONTEXT7_API_KEY: 'keep-mavis' },
|
|
142
|
+
},
|
|
143
|
+
storybook: { url: 'http://localhost:6006/mcp' },
|
|
144
|
+
},
|
|
145
|
+
localOnly: true,
|
|
146
|
+
});
|
|
147
|
+
const mavisOut = init(mavisDir, 'mavis', 'next');
|
|
148
|
+
assert.match(mavisOut, /kept extra servers: storybook/);
|
|
149
|
+
const mavisMerged = readJson(mavisMcp);
|
|
150
|
+
assert.equal(mavisMerged.localOnly, true);
|
|
151
|
+
assert.ok(mavisMerged.mcpServers.storybook);
|
|
152
|
+
assert.equal(mavisMerged.mcpServers.context7.env.CONTEXT7_API_KEY, 'keep-mavis');
|
|
153
|
+
assert.ok(mavisMerged.mcpServers.playwright);
|
|
154
|
+
assert.equal(fs.existsSync(path.join(mavisDir, '.minimax')), false);
|
|
155
|
+
fs.rmSync(mavisDir, { recursive: true, force: true });
|
|
156
|
+
|
|
134
157
|
console.log('test-cli-mcp-merge: ok');
|
|
135
158
|
} finally {
|
|
136
159
|
fs.rmSync(tmp, { recursive: true, force: true });
|