@bonesofspring/ai-rules 0.2.22 → 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.
Files changed (213) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +6 -2
  3. package/bin/cli.js +32 -8
  4. package/bin/mavis-install.js +441 -0
  5. package/package.json +9 -2
  6. package/presets/_shared/core/meta/preset-pr-checklist.md +1 -0
  7. package/presets/_shared/core/meta/preset-twin-sync.md +16 -0
  8. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  9. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +16 -0
  10. package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  11. package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +16 -0
  12. package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  13. package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +16 -0
  14. package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  15. package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +16 -0
  16. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  17. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +16 -0
  18. package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  19. package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +16 -0
  20. package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  21. package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +16 -0
  22. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  23. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +16 -0
  24. package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  25. package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +16 -0
  26. package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  27. package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +16 -0
  28. package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +1 -0
  29. package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +16 -0
  30. package/presets/cursor/go/rules/preset-pr-checklist.mdc +1 -0
  31. package/presets/cursor/go/rules/preset-twin-sync.mdc +16 -0
  32. package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +1 -0
  33. package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +16 -0
  34. package/presets/cursor/java/rules/preset-pr-checklist.mdc +1 -0
  35. package/presets/cursor/java/rules/preset-twin-sync.mdc +16 -0
  36. package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +1 -0
  37. package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +16 -0
  38. package/presets/cursor/next/rules/preset-pr-checklist.mdc +1 -0
  39. package/presets/cursor/next/rules/preset-twin-sync.mdc +16 -0
  40. package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +1 -0
  41. package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +16 -0
  42. package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +1 -0
  43. package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +16 -0
  44. package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +1 -0
  45. package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +16 -0
  46. package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +1 -0
  47. package/presets/cursor/svelte/rules/preset-twin-sync.mdc +16 -0
  48. package/presets/mavis/README.md +60 -0
  49. package/presets/mavis/next/MAVIS.md +67 -0
  50. package/presets/mavis/next/MCP.md +36 -0
  51. package/presets/mavis/next/README.md +15 -0
  52. package/presets/mavis/next/REPO_AGENTS.md +94 -0
  53. package/presets/mavis/next/adapter-map.json +959 -0
  54. package/presets/mavis/next/agents/README.md +206 -0
  55. package/presets/mavis/next/agents/accessibility-reviewer.md +61 -0
  56. package/presets/mavis/next/agents/api-contract-reviewer.md +57 -0
  57. package/presets/mavis/next/agents/build-verifier.md +110 -0
  58. package/presets/mavis/next/agents/ci-investigator.md +63 -0
  59. package/presets/mavis/next/agents/code-reviewer.md +61 -0
  60. package/presets/mavis/next/agents/codebase-analyzer.md +21 -0
  61. package/presets/mavis/next/agents/debugger.md +80 -0
  62. package/presets/mavis/next/agents/feature-developer.md +101 -0
  63. package/presets/mavis/next/agents/migration-specialist.md +67 -0
  64. package/presets/mavis/next/agents/mockup-reviewer.md +53 -0
  65. package/presets/mavis/next/agents/openspec-author.md +157 -0
  66. package/presets/mavis/next/agents/performance-auditor.md +68 -0
  67. package/presets/mavis/next/agents/playwright-test-generator.md +28 -0
  68. package/presets/mavis/next/agents/playwright-test-healer.md +28 -0
  69. package/presets/mavis/next/agents/playwright-test-planner.md +21 -0
  70. package/presets/mavis/next/agents/qa-tester.md +52 -0
  71. package/presets/mavis/next/agents/security-reviewer.md +54 -0
  72. package/presets/mavis/next/agents/solution-architect.md +68 -0
  73. package/presets/mavis/next/agents/task-analyst.md +166 -0
  74. package/presets/mavis/next/agents/task-router.md +178 -0
  75. package/presets/mavis/next/agents/tech-writer.md +57 -0
  76. package/presets/mavis/next/agents/unit-test-generator.md +31 -0
  77. package/presets/mavis/next/agents/unit-test-healer.md +32 -0
  78. package/presets/mavis/next/agents/unit-test-planner.md +54 -0
  79. package/presets/mavis/next/commands/README.md +49 -0
  80. package/presets/mavis/next/commands/spec-start.md +24 -0
  81. package/presets/mavis/next/commands/task-continue.md +50 -0
  82. package/presets/mavis/next/commands/task.md +50 -0
  83. package/presets/mavis/next/commands/technical-retro.md +80 -0
  84. package/presets/mavis/next/hooks/README.md +22 -0
  85. package/presets/mavis/next/hooks/chain-team-phases.sh +845 -0
  86. package/presets/mavis/next/mcp.json +19 -0
  87. package/presets/mavis/next/rules/README.md +16 -0
  88. package/presets/mavis/next/rules/agent-team-intake.md +11 -0
  89. package/presets/mavis/next/rules/agent-team-orchestrator.md +210 -0
  90. package/presets/mavis/next/rules/ai-local-overlay.md +37 -0
  91. package/presets/mavis/next/rules/anti-sycophancy-discipline.md +23 -0
  92. package/presets/mavis/next/rules/api-services.md +72 -0
  93. package/presets/mavis/next/rules/architecture-boundaries-ui.md +9 -0
  94. package/presets/mavis/next/rules/architecture-boundaries.md +86 -0
  95. package/presets/mavis/next/rules/arrow-functions.md +25 -0
  96. package/presets/mavis/next/rules/code-quality-and-refactoring.md +40 -0
  97. package/presets/mavis/next/rules/code-review-mr.md +40 -0
  98. package/presets/mavis/next/rules/css-property-order-stylelint.md +5 -0
  99. package/presets/mavis/next/rules/design-guidance.md +99 -0
  100. package/presets/mavis/next/rules/feature-delivery-workflow.md +49 -0
  101. package/presets/mavis/next/rules/http-client.md +55 -0
  102. package/presets/mavis/next/rules/layer-barrel-exports.md +68 -0
  103. package/presets/mavis/next/rules/mcp-usage.md +86 -0
  104. package/presets/mavis/next/rules/mockup-review.md +107 -0
  105. package/presets/mavis/next/rules/navigation-router-stack.md +56 -0
  106. package/presets/mavis/next/rules/navigation-router-ui.md +10 -0
  107. package/presets/mavis/next/rules/next-app-core.md +29 -0
  108. package/presets/mavis/next/rules/next-app-router.md +44 -0
  109. package/presets/mavis/next/rules/no-props-spread.md +56 -0
  110. package/presets/mavis/next/rules/no-type-assertion-as-import-export.md +51 -0
  111. package/presets/mavis/next/rules/package-manager.md +11 -0
  112. package/presets/mavis/next/rules/playwright-agents.md +67 -0
  113. package/presets/mavis/next/rules/post-change-lint.md +35 -0
  114. package/presets/mavis/next/rules/preset-layering.md +27 -0
  115. package/presets/mavis/next/rules/preset-no-cross-stack-leakage.md +90 -0
  116. package/presets/mavis/next/rules/preset-pr-checklist.md +35 -0
  117. package/presets/mavis/next/rules/preset-token-budget.md +36 -0
  118. package/presets/mavis/next/rules/preset-twin-sync.md +65 -0
  119. package/presets/mavis/next/rules/product-specs-authoring.md +112 -0
  120. package/presets/mavis/next/rules/product-specs.md +56 -0
  121. package/presets/mavis/next/rules/public-imports.md +50 -0
  122. package/presets/mavis/next/rules/react-a11y-coding.md +31 -0
  123. package/presets/mavis/next/rules/react-next-anti-hallucination.md +60 -0
  124. package/presets/mavis/next/rules/react-ui.md +105 -0
  125. package/presets/mavis/next/rules/reference-features.md +45 -0
  126. package/presets/mavis/next/rules/security-next.md +45 -0
  127. package/presets/mavis/next/rules/store-rtk.md +78 -0
  128. package/presets/mavis/next/rules/technical-retro.md +11 -0
  129. package/presets/mavis/next/rules/tests-e2e-structure.md +72 -0
  130. package/presets/mavis/next/rules/tests-unit.md +72 -0
  131. package/presets/mavis/next/rules/types-jsdoc.md +43 -0
  132. package/presets/mavis/next/rules/ux-design.md +99 -0
  133. package/presets/mavis/next/skills/README.md +21 -0
  134. package/presets/mavis/next/skills/ci-investigation/SKILL.md +39 -0
  135. package/presets/mavis/next/skills/code-review/SKILL.md +39 -0
  136. package/presets/mavis/next/skills/debug-investigation/SKILL.md +26 -0
  137. package/presets/mavis/next/skills/feature-delivery/SKILL.md +34 -0
  138. package/presets/mavis/next/skills/mockup-review/SKILL.md +24 -0
  139. package/presets/mavis/next/skills/playwright-e2e/SKILL.md +28 -0
  140. package/presets/mavis/next/skills/react-performance/SKILL.md +39 -0
  141. package/presets/mavis/next/skills/spec-authoring/SKILL.md +21 -0
  142. package/presets/mavis/next/skills/spec-discovery/SKILL.md +53 -0
  143. package/presets/mavis/next/skills/technical-retro/SKILL.md +49 -0
  144. package/presets/mavis/next/skills/unit-testing/SKILL.md +29 -0
  145. package/presets/mavis/next/skills/ux-design/SKILL.md +31 -0
  146. package/presets/mavis/next/skills/write-adr/SKILL.md +36 -0
  147. package/presets/mavis/next/team/README.md +148 -0
  148. package/presets/mavis/next/team/conventions.md +21 -0
  149. package/presets/mavis/next/team/fixtures/artifact-manifest.example.json +79 -0
  150. package/presets/mavis/next/team/fixtures/bugfix-standard.json +25 -0
  151. package/presets/mavis/next/team/fixtures/feature-full.json +29 -0
  152. package/presets/mavis/next/team/fixtures/feature-light.json +27 -0
  153. package/presets/mavis/next/team/fixtures/feature-with-figma-url.json +17 -0
  154. package/presets/mavis/next/team/fixtures/feature-with-ux-no-figma.json +17 -0
  155. package/presets/mavis/next/team/fixtures/memlog.example.md +9 -0
  156. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/human-gate/metrics.json +69 -0
  157. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/missing-ledger/status.json +5 -0
  158. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/parallel-receipts/metrics.json +79 -0
  159. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/serial-retry/metrics.json +124 -0
  160. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/validation-failed/metrics.json +124 -0
  161. package/presets/mavis/next/team/fixtures/metrics.example.json +54 -0
  162. package/presets/mavis/next/team/fixtures/mockup-review-fail-embedded-status.json +8 -0
  163. package/presets/mavis/next/team/fixtures/mockup-review-standalone.json +14 -0
  164. package/presets/mavis/next/team/fixtures/ux-design-standalone.json +15 -0
  165. package/presets/mavis/next/team/tasks/.gitkeep +0 -0
  166. package/presets/mavis/platform-contract.json +131 -0
  167. package/scripts/capture-cli-golden.mjs +44 -0
  168. package/scripts/check-chain-hook-coverage.sh +1 -0
  169. package/scripts/check-mavis-adapter-map.mjs +294 -0
  170. package/scripts/check-mavis-local-overlay.mjs +56 -0
  171. package/scripts/check-mavis-platform-contract.mjs +388 -0
  172. package/scripts/check-mavis-token-budget.mjs +48 -0
  173. package/scripts/check-preset-leakage.sh +35 -3
  174. package/scripts/check-preset-structure.sh +63 -0
  175. package/scripts/check-shared-core-drift.sh +18 -0
  176. package/scripts/check-task-router-intents.sh +15 -2
  177. package/scripts/fixtures/cli-golden/claude/next.json +130 -0
  178. package/scripts/fixtures/cli-golden/cursor/next.json +132 -0
  179. package/scripts/fixtures/cli-golden/mavis/next.json +125 -0
  180. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/AGENTS.md +1 -0
  181. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/extra.mdc +1 -0
  182. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/sample.mdc +2 -0
  183. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/MAVIS.md +1 -0
  184. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/adapter-map.json +26 -0
  185. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/rules/sample.md +2 -0
  186. package/scripts/fixtures/mavis-adapter-map/valid/cursor/AGENTS.md +1 -0
  187. package/scripts/fixtures/mavis-adapter-map/valid/cursor/rules/sample.mdc +2 -0
  188. package/scripts/fixtures/mavis-adapter-map/valid/mavis/MAVIS.md +1 -0
  189. package/scripts/fixtures/mavis-adapter-map/valid/mavis/adapter-map.json +26 -0
  190. package/scripts/fixtures/mavis-adapter-map/valid/mavis/rules/sample.md +2 -0
  191. package/scripts/fixtures/mavis-platform-contract/invalid-minimax-destination.json +131 -0
  192. package/scripts/fixtures/mavis-platform-contract/invalid-missing-capability.json +126 -0
  193. package/scripts/fixtures/mavis-platform-contract/invalid-released-unknown-stack.json +133 -0
  194. package/scripts/fixtures/mavis-platform-contract/invalid-unknown-native-path.json +131 -0
  195. package/scripts/fixtures/mavis-platform-contract/invalid-version-pin.json +132 -0
  196. package/scripts/fixtures/mavis-platform-contract/valid.json +131 -0
  197. package/scripts/fixtures/mavis-probe/AGENTS.md +6 -0
  198. package/scripts/fixtures/mavis-probe/agents/probe-agent.md +6 -0
  199. package/scripts/fixtures/mavis-probe/fake-mcode.mjs +98 -0
  200. package/scripts/fixtures/mavis-probe/output.schema.json +28 -0
  201. package/scripts/fixtures/mavis-probe/rules/probe.md +5 -0
  202. package/scripts/fixtures/mavis-probe/skills/probe/SKILL.md +11 -0
  203. package/scripts/generate-mavis-next.mjs +704 -0
  204. package/scripts/lib/cli-golden.mjs +50 -0
  205. package/scripts/lib/discover-hooks.mjs +72 -0
  206. package/scripts/probe-mavis-capabilities.mjs +480 -0
  207. package/scripts/test-agent-task-metrics-hooks.mjs +22 -16
  208. package/scripts/test-chain-team-phases-coverage.mjs +10 -7
  209. package/scripts/test-cli-init-clean.mjs +256 -0
  210. package/scripts/test-cli-mcp-merge.mjs +23 -0
  211. package/scripts/test-mavis-context-budget.mjs +55 -0
  212. package/scripts/test-mavis-orchestration.mjs +649 -0
  213. package/scripts/test-probe-mavis-capabilities.mjs +205 -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
- const hooks = [
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
- assert.equal(hooks.length, 20, 'expected every packaged Cursor and Claude hook (10 stacks × 2 platforms)');
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
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';
@@ -903,7 +909,7 @@ for (const hook of hooks) {
903
909
  for (const hook of hooks) {
904
910
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-lock-race-'));
905
911
  try {
906
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
912
+ const platform = platformFromHook(hook);
907
913
  const runId = 'lock-race-run';
908
914
  const attemptId = `${runId}:step-0:feature-developer:attempt-1`;
909
915
  const taskDirectory = writeTask(
@@ -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
- 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
- ];
35
+ import { discoverChainHooks, expectedHookCount, platformFromHook } from './lib/discover-hooks.mjs';
39
36
 
40
- assert.equal(hooks.length, 20, `expected 20 packaged hooks (10 stacks × 2 platforms), got ${hooks.length}`);
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, 18, 'expected 18 hooks to cover openspec-author');
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.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
66
+ return platformFromHook(hook);
64
67
  }
65
68
 
66
69
  function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {}) {
@@ -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 });
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * P1 context budget: root AGENTS + .mavis/AGENTS combined nonempty lines ≤ 170.
4
+ * Critical-policy sentinels must appear in the pair.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { loadReleasedMavisStacks } from './lib/discover-hooks.mjs';
10
+
11
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
12
+ const presetsRoot = path.join(scriptDirectory, '..', 'presets');
13
+ const LINE_CAP = 170;
14
+ const SENTINELS = [
15
+ 'next-app-core',
16
+ 'package-manager',
17
+ 'code-quality-and-refactoring',
18
+ 'ai-local-overlay',
19
+ '.mavis/AGENTS.md',
20
+ 'status.json',
21
+ '~/.minimax',
22
+ ];
23
+
24
+ function nonemptyLines(text) {
25
+ return text.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
26
+ }
27
+
28
+ const stacks = loadReleasedMavisStacks(presetsRoot);
29
+ if (stacks.length === 0) {
30
+ console.log('ok mavis context-budget: no released stacks');
31
+ process.exit(0);
32
+ }
33
+
34
+ let failed = 0;
35
+ for (const stack of stacks) {
36
+ const root = path.join(presetsRoot, 'mavis', stack);
37
+ const repoAgents = fs.readFileSync(path.join(root, 'REPO_AGENTS.md'), 'utf8');
38
+ const mavisAgents = fs.readFileSync(path.join(root, 'MAVIS.md'), 'utf8');
39
+ const combined = `${repoAgents}\n${mavisAgents}`;
40
+ const count = nonemptyLines(repoAgents) + nonemptyLines(mavisAgents);
41
+ if (count > LINE_CAP) {
42
+ console.error(`FAIL mavis/${stack}: entry nonempty lines ${count} > ${LINE_CAP}`);
43
+ failed += 1;
44
+ } else {
45
+ console.log(`ok mavis/${stack}: entry nonempty lines ${count} (cap ${LINE_CAP})`);
46
+ }
47
+ for (const sentinel of SENTINELS) {
48
+ if (!combined.includes(sentinel)) {
49
+ console.error(`FAIL mavis/${stack}: missing critical sentinel ${sentinel}`);
50
+ failed += 1;
51
+ }
52
+ }
53
+ }
54
+
55
+ process.exit(failed === 0 ? 0 : 1);