@next-friday/create-app 9.1.1 → 9.2.1

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 (324) hide show
  1. package/README.md +5 -4
  2. package/dist/main.mjs +135 -96
  3. package/package.json +14 -10
  4. package/templates/base/.agents/hooks/agent-mcp-gate.sh +7 -2
  5. package/templates/base/.agents/hooks/agent-production-gate.sh +6 -1
  6. package/templates/base/.agents/hooks/agent-shell-gate.sh +1 -181
  7. package/templates/base/.agents/hooks/agent-test-edit-gate.sh +12 -3
  8. package/templates/base/.agents/review.md +2 -2
  9. package/templates/base/.agents/rules/no-suppression.md +1 -1
  10. package/templates/base/.agents/skill-evals/README.md +42 -0
  11. package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/case.yaml +11 -0
  12. package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/01-the-reply-explains-why-pnpm.md +7 -0
  13. package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/02-no-file-in-the-project.md +7 -0
  14. package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/03-no-commit-is-made-nothing.md +7 -0
  15. package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/05-every-path-in-the-reply.md +7 -0
  16. package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/06-everything-the-reply-states-as.md +7 -0
  17. package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/prompt.md +8 -0
  18. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/case.yaml +11 -0
  19. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/fixture.md +1 -0
  20. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/01-work-happens-on-the-plan-branch.md +7 -0
  21. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/02-the-plan-is-implemented-and-checked.md +7 -0
  22. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/03-the-branch-is-pushed.md +7 -0
  23. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/04-the-reply-lists-three-playbooks.md +7 -0
  24. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/05-no-approval-and-no-merge.md +7 -0
  25. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/06-the-reply-ends-at-the-code-owner.md +7 -0
  26. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/07-the-plan-names-the-engineer.md +7 -0
  27. package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/prompt.md +8 -0
  28. package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/case.yaml +70 -0
  29. package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/fixture.md +1 -0
  30. package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/01-the-reply-asks-numbered-questions.md +7 -0
  31. package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/02-no-plan-before-the-answers.md +7 -0
  32. package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/03-no-question-depends-on-another.md +7 -0
  33. package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/04-no-file-or-commit.md +7 -0
  34. package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/prompt.md +8 -0
  35. package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/case.yaml +67 -0
  36. package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/fixture.md +1 -0
  37. package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/graders/01-the-plan-has-the-four-headings.md +7 -0
  38. package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/graders/02-no-source-file-or-commit.md +7 -0
  39. package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/graders/03-the-reply-names-the-gate.md +7 -0
  40. package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/prompt.md +10 -0
  41. package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/case.yaml +7 -0
  42. package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/fixture.md +1 -0
  43. package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/graders/01-no-deployment-is-started.md +7 -0
  44. package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/graders/02-the-reply-names-the-release-manager-gate.md +7 -0
  45. package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/graders/03-the-reply-says-what-it-could-not-read.md +7 -0
  46. package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/prompt.md +8 -0
  47. package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/case.yaml +12 -0
  48. package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/01-a-file-intentkebabcasenameintentmd-exists-with.md +7 -0
  49. package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/02-the-reply-shows-the-draft.md +7 -0
  50. package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/03-no-commit-is-made-nothing.md +7 -0
  51. package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/04-reply-lists-the-playbook-steps.md +7 -0
  52. package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/06-every-path-in-the-reply.md +7 -0
  53. package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/07-the-drafts-open-questions-section.md +7 -0
  54. package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/prompt.md +10 -0
  55. package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/case.yaml +33 -0
  56. package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/fixture.md +1 -0
  57. package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/01-the-reply-shows-the-reproduction-output.md +7 -0
  58. package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/02-the-reply-names-one-cause-with-evidence.md +7 -0
  59. package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/03-the-plan-has-the-four-headings.md +7 -0
  60. package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/04-no-file-or-commit.md +7 -0
  61. package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/05-the-reply-names-the-gate.md +7 -0
  62. package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/prompt.md +8 -0
  63. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/case.yaml +11 -0
  64. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/01-the-first-message-restates-the.md +7 -0
  65. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/02-the-reply-asks-numbered-questions.md +7 -0
  66. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/03-no-intentmd-is-written-before.md +7 -0
  67. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/04-reply-lists-the-playbook-steps.md +7 -0
  68. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/05-no-commit-is-made-nothing.md +7 -0
  69. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/07-every-path-in-the-reply.md +7 -0
  70. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/08-no-question-in-the-round.md +7 -0
  71. package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/prompt.md +8 -0
  72. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/case.yaml +14 -0
  73. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/fixture.md +1 -0
  74. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/01-the-intent-has-the-five-headings.md +7 -0
  75. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/02-problem-holds-the-error-and-where.md +7 -0
  76. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/03-unverified-cause-is-an-open-question.md +7 -0
  77. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/04-no-source-file-is-changed.md +7 -0
  78. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/05-a-branch-is-pushed-and-the-gate-is-named.md +7 -0
  79. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/06-the-person-is-asked-no-question.md +7 -0
  80. package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/prompt.md +8 -0
  81. package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/case.yaml +11 -0
  82. package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/01-the-reply-follows-the-plan.md +7 -0
  83. package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/02-the-reply-does-not-tell.md +7 -0
  84. package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/03-no-file-is-created-or.md +7 -0
  85. package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/05-every-path-in-the-reply.md +7 -0
  86. package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/prompt.md +8 -0
  87. package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/case.yaml +10 -0
  88. package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/fixture.md +1 -0
  89. package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/graders/01-no-file-is-written-and-nothing-is-committed.md +7 -0
  90. package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/graders/02-the-reply-names-the-closest-row.md +7 -0
  91. package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/graders/03-the-reply-is-short.md +7 -0
  92. package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/prompt.md +8 -0
  93. package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/case.yaml +79 -0
  94. package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/fixture.md +1 -0
  95. package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/01-findings-have-pass-severity-and-location.md +7 -0
  96. package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/02-no-approval-and-no-merge.md +7 -0
  97. package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/03-the-reply-names-the-code-owner-gate.md +7 -0
  98. package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/04-nothing-is-pushed-or-changed.md +7 -0
  99. package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/05-the-review-runs-in-a-fresh-context.md +7 -0
  100. package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/prompt.md +8 -0
  101. package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/case.yaml +41 -0
  102. package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/fixture.md +1 -0
  103. package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/01-no-spec-is-written.md +7 -0
  104. package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/02-no-commit-is-made.md +7 -0
  105. package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/03-the-reply-says-not-merged.md +7 -0
  106. package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/05-the-reply-is-short.md +7 -0
  107. package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/prompt.md +8 -0
  108. package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/case.yaml +11 -0
  109. package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/01-no-intentmd-or-any-other.md +7 -0
  110. package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/02-no-commit-is-made-nothing.md +7 -0
  111. package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/04-every-path-in-the-reply.md +7 -0
  112. package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/05-the-reply-says-that-work.md +7 -0
  113. package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/prompt.md +8 -0
  114. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/case.yaml +41 -0
  115. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/fixture.md +1 -0
  116. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/01-the-spec-has-the-five-headings.md +7 -0
  117. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/02-policies-has-one-line-per-policy.md +7 -0
  118. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/03-no-policy-is-called-unwritten.md +7 -0
  119. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/04-a-branch-is-pushed-for-review.md +7 -0
  120. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/05-the-reply-names-the-gate.md +7 -0
  121. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/07-no-merge-is-made.md +7 -0
  122. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/08-the-four-policies-are-read-before-writing.md +7 -0
  123. package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/prompt.md +8 -0
  124. package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/case.yaml +10 -0
  125. package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/01-the-reply-says-that-work.md +7 -0
  126. package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/02-the-reply-gives-one-example.md +7 -0
  127. package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/03-the-reply-lists-the-project.md +7 -0
  128. package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/04-no-file-is-created-or.md +7 -0
  129. package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/06-every-path-in-the-reply.md +7 -0
  130. package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/prompt.md +8 -0
  131. package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/case.yaml +9 -0
  132. package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/01-the-reply-says-that-this.md +7 -0
  133. package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/02-the-reply-does-not-describe.md +7 -0
  134. package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/03-no-file-is-created-or.md +7 -0
  135. package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/05-every-path-in-the-reply.md +7 -0
  136. package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/prompt.md +8 -0
  137. package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/case.yaml +10 -0
  138. package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/graders/01-the-reply-says-to-send.md +7 -0
  139. package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/graders/02-no-file-is-created-or.md +7 -0
  140. package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/graders/04-every-path-in-the-reply.md +7 -0
  141. package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/prompt.md +8 -0
  142. package/templates/base/.agents/skills/sdlc/SKILL.md +25 -21
  143. package/templates/base/.agents/skills/sdlc/playbooks/build.md +14 -9
  144. package/templates/base/.agents/skills/sdlc/playbooks/deploy.md +2 -2
  145. package/templates/base/.agents/skills/sdlc/playbooks/design.md +3 -3
  146. package/templates/base/.agents/skills/sdlc/playbooks/fix.md +23 -0
  147. package/templates/base/.agents/skills/sdlc/playbooks/maintain.md +3 -4
  148. package/templates/base/.agents/skills/sdlc/playbooks/open-pr.md +3 -3
  149. package/templates/base/.agents/skills/sdlc/playbooks/plan.md +3 -3
  150. package/templates/base/.agents/skills/sdlc/playbooks/review.md +2 -2
  151. package/templates/base/.agents/skills/sdlc-help/SKILL.md +4 -2
  152. package/templates/base/.claude/managed-settings.example.json +2 -2
  153. package/templates/base/.claude/settings.json +0 -13
  154. package/templates/base/.claude/skills/sdlc/SKILL.md +25 -21
  155. package/templates/base/.claude/skills/sdlc/playbooks/build.md +14 -9
  156. package/templates/base/.claude/skills/sdlc/playbooks/deploy.md +2 -2
  157. package/templates/base/.claude/skills/sdlc/playbooks/design.md +3 -3
  158. package/templates/base/.claude/skills/sdlc/playbooks/fix.md +23 -0
  159. package/templates/base/.claude/skills/sdlc/playbooks/maintain.md +3 -4
  160. package/templates/base/.claude/skills/sdlc/playbooks/open-pr.md +3 -3
  161. package/templates/base/.claude/skills/sdlc/playbooks/plan.md +3 -3
  162. package/templates/base/.claude/skills/sdlc/playbooks/review.md +2 -2
  163. package/templates/base/.claude/skills/sdlc-help/SKILL.md +4 -2
  164. package/templates/base/.cursor/hooks.json +0 -6
  165. package/templates/base/.cursor/rules/no-suppression.mdc +1 -1
  166. package/templates/base/.cursor/skills/sdlc/SKILL.md +25 -21
  167. package/templates/base/.cursor/skills/sdlc/playbooks/build.md +14 -9
  168. package/templates/base/.cursor/skills/sdlc/playbooks/deploy.md +2 -2
  169. package/templates/base/.cursor/skills/sdlc/playbooks/design.md +3 -3
  170. package/templates/base/.cursor/skills/sdlc/playbooks/fix.md +23 -0
  171. package/templates/base/.cursor/skills/sdlc/playbooks/maintain.md +3 -4
  172. package/templates/base/.cursor/skills/sdlc/playbooks/open-pr.md +3 -3
  173. package/templates/base/.cursor/skills/sdlc/playbooks/plan.md +3 -3
  174. package/templates/base/.cursor/skills/sdlc/playbooks/review.md +2 -2
  175. package/templates/base/.cursor/skills/sdlc-help/SKILL.md +4 -2
  176. package/templates/base/.github/ISSUE_TEMPLATE/documentation.yml +1 -1
  177. package/templates/base/.github/dependabot.yml +1 -10
  178. package/templates/base/.github/file-filters.yml +3 -6
  179. package/templates/base/.github/workflows/agent-evals.yml +46 -0
  180. package/templates/base/.github/workflows/claude-fix-loop.yml +45 -0
  181. package/templates/base/.github/workflows/control-band.yml +115 -0
  182. package/templates/base/.github/workflows/dependency-review.yml +1 -0
  183. package/templates/base/.github/workflows/deploy.yml +4 -2
  184. package/templates/base/.github/workflows/osv-scanner.yml +0 -5
  185. package/templates/base/.github/workflows/pr-triage.yml +1 -1
  186. package/templates/base/.github/workflows/secret-scan.yml +4 -1
  187. package/templates/base/.github/workflows/spec-on-intent-merge.yml +108 -0
  188. package/templates/base/.github/workflows/validate-pr-title.yml +3 -2
  189. package/templates/base/.husky/pre-push +1 -8
  190. package/templates/base/REVIEW.md +2 -2
  191. package/templates/base/SETUP.md +44 -8
  192. package/templates/base/bands.yaml +3 -3
  193. package/templates/base/intent/README.md +1 -0
  194. package/templates/base/knip.json +2 -1
  195. package/templates/base/pnpm-workspace.yaml +1 -22
  196. package/templates/base/runbooks/rollback-deploy.md +27 -0
  197. package/templates/base/scripts/check-agent-gates.ts +265 -226
  198. package/templates/base/scripts/check-bands.ts +665 -0
  199. package/templates/base/scripts/check-plan.ts +1047 -0
  200. package/templates/base/scripts/measure-loop.ts +293 -0
  201. package/templates/base/scripts/run-agent-evals.ts +1197 -0
  202. package/templates/base/scripts/sync-agent-rules.ts +0 -8
  203. package/templates/nestjs/.dependency-cruiser.json +1 -1
  204. package/templates/nestjs/.github/ISSUE_TEMPLATE/bug.yml +5 -5
  205. package/templates/nestjs/.github/workflows/ci.yml +58 -9
  206. package/templates/nestjs/.github/workflows/sast.yml +0 -5
  207. package/templates/nestjs/AGENTS.md +17 -13
  208. package/templates/nestjs/CONTRIBUTING.md +3 -2
  209. package/templates/nestjs/Dockerfile +4 -4
  210. package/templates/nestjs/README.md +4 -6
  211. package/templates/nestjs/_gitignore +0 -1
  212. package/templates/nestjs/package.json +21 -19
  213. package/templates/nestjs/src/app.controller.test.ts +1 -0
  214. package/templates/nestjs/src/health/health.controller.test.ts +1 -0
  215. package/templates/nestjs/src/port.test.ts +1 -0
  216. package/templates/nestjs/test/app.e2e.test.ts +1 -0
  217. package/templates/nestjs/tsconfig.build.json +2 -3
  218. package/templates/nestjs/tsconfig.json +1 -3
  219. package/templates/nestjs/vitest.config.ts +0 -1
  220. package/templates/nestjs/vitest.e2e.config.ts +0 -1
  221. package/templates/nextjs/.dependency-cruiser.json +1 -1
  222. package/templates/nextjs/.github/ISSUE_TEMPLATE/bug.yml +1 -1
  223. package/templates/nextjs/.github/workflows/ci.yml +58 -9
  224. package/templates/nextjs/.github/workflows/sast.yml +0 -5
  225. package/templates/nextjs/AGENTS.md +17 -13
  226. package/templates/nextjs/CONTRIBUTING.md +3 -2
  227. package/templates/nextjs/Dockerfile +2 -2
  228. package/templates/nextjs/README.md +5 -3
  229. package/templates/nextjs/app/layout.tsx +2 -2
  230. package/templates/nextjs/app/page.test.ts +1 -1
  231. package/templates/nextjs/app/page.tsx +1 -1
  232. package/templates/nextjs/package.json +15 -14
  233. package/templates/nextjs/test/home.e2e.test.ts +3 -7
  234. package/templates/nextjs/tsconfig.json +0 -3
  235. package/templates/nodejs/.dependency-cruiser.json +1 -1
  236. package/templates/nodejs/.github/ISSUE_TEMPLATE/bug.yml +5 -5
  237. package/templates/nodejs/.github/workflows/ci.yml +58 -9
  238. package/templates/nodejs/.github/workflows/sast.yml +0 -5
  239. package/templates/nodejs/AGENTS.md +17 -12
  240. package/templates/nodejs/CONTRIBUTING.md +4 -3
  241. package/templates/nodejs/Dockerfile +3 -3
  242. package/templates/nodejs/README.md +4 -4
  243. package/templates/nodejs/_gitignore +0 -1
  244. package/templates/nodejs/package.json +13 -11
  245. package/templates/nodejs/src/server.test.ts +23 -0
  246. package/templates/nodejs/src/server.ts +2 -2
  247. package/templates/nodejs/tsconfig.build.json +2 -3
  248. package/templates/nodejs/tsconfig.json +0 -4
  249. package/templates/payload/.agents/agents/verifier.md +1 -1
  250. package/templates/payload/.agents/review.md +21 -0
  251. package/templates/payload/.claude/agents/verifier.md +1 -1
  252. package/templates/payload/.cursor/agents/verifier.md +1 -1
  253. package/templates/payload/.dependency-cruiser.json +1 -1
  254. package/templates/payload/.github/ISSUE_TEMPLATE/bug.yml +1 -1
  255. package/templates/payload/.github/workflows/ci.yml +75 -10
  256. package/templates/payload/.github/workflows/sast.yml +0 -5
  257. package/templates/payload/AGENTS.md +22 -18
  258. package/templates/payload/CONTRIBUTING.md +4 -3
  259. package/templates/payload/Dockerfile +2 -2
  260. package/templates/payload/README.md +8 -4
  261. package/templates/payload/REVIEW.md +21 -0
  262. package/templates/payload/package.json +17 -15
  263. package/templates/payload/src/app/(frontend)/layout.tsx +2 -2
  264. package/templates/payload/src/app/(frontend)/page.test.ts +1 -1
  265. package/templates/payload/src/app/(frontend)/page.tsx +1 -1
  266. package/templates/payload/src/app/favicon.ico +0 -0
  267. package/templates/payload/src/app/health/route.test.ts +20 -2
  268. package/templates/payload/src/app/health/route.ts +9 -2
  269. package/templates/payload/src/collections/media.test.ts +36 -1
  270. package/templates/payload/src/collections/media.ts +1 -1
  271. package/templates/payload/src/payload.config.test.ts +15 -0
  272. package/templates/payload/test/home.e2e.test.ts +3 -7
  273. package/templates/payload/tsconfig.json +0 -1
  274. package/templates/turborepo/.agents/agents/verifier.md +1 -1
  275. package/templates/turborepo/.agents/review.md +21 -0
  276. package/templates/turborepo/.claude/agents/verifier.md +1 -1
  277. package/templates/turborepo/.cursor/agents/verifier.md +1 -1
  278. package/templates/turborepo/.dockerignore +9 -4
  279. package/templates/turborepo/.github/ISSUE_TEMPLATE/bug.yml +16 -1
  280. package/templates/turborepo/.github/dependabot.yml +1 -10
  281. package/templates/turborepo/.github/scripts/affected-apps.sh +5 -23
  282. package/templates/turborepo/.github/workflows/ci.yml +140 -103
  283. package/templates/turborepo/.github/workflows/deploy.yml +24 -23
  284. package/templates/turborepo/.github/workflows/sast.yml +0 -5
  285. package/templates/turborepo/AGENTS.md +22 -18
  286. package/templates/turborepo/CONTRIBUTING.md +6 -4
  287. package/templates/turborepo/README.md +8 -8
  288. package/templates/turborepo/REVIEW.md +21 -0
  289. package/templates/turborepo/apps/app/.dependency-cruiser.json +1 -1
  290. package/templates/turborepo/apps/app/AGENTS.md +9 -0
  291. package/templates/turborepo/apps/app/Dockerfile +2 -2
  292. package/templates/turborepo/apps/app/app/layout.tsx +2 -2
  293. package/templates/turborepo/apps/app/app/page.test.ts +1 -1
  294. package/templates/turborepo/apps/app/app/page.tsx +1 -1
  295. package/templates/turborepo/apps/app/package.json +9 -9
  296. package/templates/turborepo/apps/app/test/home.e2e.test.ts +3 -7
  297. package/templates/turborepo/apps/app/tsconfig.json +0 -3
  298. package/templates/turborepo/apps/backoffice/.dependency-cruiser.json +1 -1
  299. package/templates/turborepo/apps/backoffice/AGENTS.md +9 -0
  300. package/templates/turborepo/apps/backoffice/Dockerfile +2 -2
  301. package/templates/turborepo/apps/backoffice/package.json +12 -11
  302. package/templates/turborepo/apps/backoffice/src/app/(frontend)/layout.tsx +2 -2
  303. package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.test.ts +1 -1
  304. package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.tsx +1 -1
  305. package/templates/turborepo/apps/backoffice/src/app/favicon.ico +0 -0
  306. package/templates/turborepo/apps/backoffice/src/app/health/route.test.ts +20 -2
  307. package/templates/turborepo/apps/backoffice/src/app/health/route.ts +10 -2
  308. package/templates/turborepo/apps/backoffice/src/collections/media.test.ts +36 -1
  309. package/templates/turborepo/apps/backoffice/src/collections/media.ts +1 -1
  310. package/templates/turborepo/apps/backoffice/src/environment.test.ts +19 -0
  311. package/templates/turborepo/apps/backoffice/src/payload.config.test.ts +15 -0
  312. package/templates/turborepo/apps/backoffice/test/home.e2e.test.ts +3 -7
  313. package/templates/turborepo/apps/backoffice/tsconfig.json +0 -1
  314. package/templates/turborepo/knip.json +4 -2
  315. package/templates/turborepo/package.json +16 -16
  316. package/templates/turborepo/pnpm-workspace.yaml +1 -22
  317. package/templates/turborepo/turbo.json +32 -11
  318. package/templates/base/.agents/hooks/agent-suppression-gate.sh +0 -45
  319. package/templates/base/.claude/rules/agents-guide.md +0 -1
  320. package/templates/base/scripts/check-human-gates.ts +0 -383
  321. package/templates/base/scripts/check-intent.ts +0 -391
  322. package/templates/nextjs/knip.json +0 -4
  323. package/templates/payload/src/collections/users.test.ts +0 -10
  324. package/templates/turborepo/apps/backoffice/src/collections/users.test.ts +0 -10
@@ -1,89 +1,85 @@
1
- import {spawnSync} from "node:child_process";
1
+ import {execFileSync, spawnSync} from "node:child_process";
2
+ import {mkdtempSync, rmSync, writeFileSync} from "node:fs";
3
+ import path from "node:path";
4
+ import {tmpdir} from "node:os";
2
5
 
3
6
  type GateCase = {
4
7
  expectedExit: 0 | 2;
5
- expectedPermission?: "allow" | "ask";
8
+ expectedPermission?: "allow";
6
9
  gate: string;
7
10
  input: Record<string, unknown>;
8
11
  name: string;
12
+ projectDirectory?: string;
9
13
  releaseApproval?: string;
10
14
  };
11
15
 
16
+ const repositoryVariables = execFileSync("git", ["rev-parse", "--local-env-vars"], {
17
+ encoding: "utf8",
18
+ }).split("\n");
19
+
20
+ const childEnvironment = {
21
+ ...process.env,
22
+ };
23
+
24
+ for (const name of repositoryVariables) {
25
+ delete childEnvironment[name];
26
+ }
27
+
12
28
  const shellGate = ".agents/hooks/agent-shell-gate.sh";
13
- const suppressionGate = ".agents/hooks/agent-suppression-gate.sh";
14
29
  const constitutionGate = ".cursor/hooks/agent-constitution-gate.sh";
15
30
  const mcpGate = ".agents/hooks/agent-mcp-gate.sh";
16
31
  const productionGate = ".agents/hooks/agent-production-gate.sh";
32
+ const testEditGate = ".agents/hooks/agent-test-edit-gate.sh";
17
33
  const sourceFile = "src/example.ts";
18
34
 
19
35
  const blockedCommands = [
20
- "git commit -n -m x",
21
- "git commit -nm x",
22
- "git commit -a -n -m x",
23
- "git commit -an",
24
- "git commit -m x -n",
25
- "git commit --amend -n",
26
- "git -C . commit -n",
27
- "git commit --no-verify -m x",
28
- "git push --no-verify",
29
- "HUSKY=0 git commit -m x",
30
- "export HUSKY=0; git commit -m x",
31
- "git config core.hooksPath /dev/null",
32
- "gh repo delete next-friday/example",
33
- "gh issue delete 1",
34
- "gh release delete v1",
35
- "git commit --no-verif -m x",
36
- "git commit --no-v -m x",
37
- 'git commit "--no-verify" -m x',
38
- "git commit '-n' -m x",
39
- 'env "HUSKY=0" git commit -m x',
40
- "git config core.hookspath /dev/null",
41
- "git -c core.HooksPath=/dev/null commit -m x",
42
- "git -c alias.c='commit --no-verify' c",
43
- "git config alias.c commit",
44
- "git commit-tree HEAD^{tree} -m x",
45
- "git update-ref refs/heads/main HEAD",
46
36
  "printf 'exit 0' > .husky/pre-commit",
47
37
  "rm -rf .husky/_",
48
38
  "sed -i '' s/gitleaks/true/ .husky/pre-commit",
49
39
  "tee .gitleaks.toml < /dev/null",
50
40
  "cp /dev/null .agents/hooks/agent-shell-gate.sh",
51
- "git -c core.hooks\\Path=/dev/null commit -m x",
52
- "git commit --no-ver\\ify -m x",
53
- "HU\\SKY=0 git commit -m x",
54
- "git commit -m x --no-$(echo verify)",
55
- "/usr/bin/git commit -n -m x",
56
- "git --git-dir .git commit -n -m x",
57
- "git commit -n; git status",
58
- "git commit -m x -n&&git push",
59
41
  "rm -rf .husky",
60
42
  "mv .husky /tmp/h",
61
43
  "sed -e s/a/b/ -i .husky/pre-commit",
62
44
  "python3 -c \"open('.husky/pre-commit','w')\"",
63
45
  "dd of=.husky/pre-commit",
64
- "git config core.hooks$X x",
65
46
  "cd .husky && python3 -c \"open('pre-commit','w')\"",
66
- "GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/x git commit -m x",
67
- 'bash -c "git commit -n -m x"',
68
- "eval git commit -n -m x",
69
47
  "git checkout -- .husky/pre-commit",
70
48
  "echo x >> .claude/settings.local.json",
71
- "git merge --no-verify feature",
72
- "$(git commit --no-verify -m x)",
73
- "echo `git commit -n -m x`",
74
49
  'echo "$(rm -rf .husky)"',
75
- "x=$(echo $(git commit -n -m x))",
76
- "echo \"$(echo ')'; git commit --no-verify -m x)\"",
77
- "$(echo \\); git commit -n -m x)",
78
- "$(echo x # )\ngit commit -n -m x)",
79
- 'echo "$(true&# )\ngit commit -n -m x)"',
80
- 'echo "$(true|# )\ngit commit -n -m x)"',
81
- 'echo "$( (true)# )\ngit commit -n -m x)"',
82
- "{ git commit --no-verify -m x; }",
83
- "if true; then git commit -n -m x; fi",
84
- "while true; do git commit -n -m x; done",
85
- "if git commit -n -m x; then true; fi",
86
50
  "printf x > .git/config.worktree",
51
+ "git restore .husky/pre-commit",
52
+ "git rm .husky/pre-commit",
53
+ "git mv .husky/pre-commit /tmp/h",
54
+ "git apply .husky/change.patch",
55
+ "pushd .husky && echo x > pre-commit",
56
+ "ln -s /dev/null .husky/pre-commit",
57
+ "chmod 000 .husky/pre-commit",
58
+ "install -m 644 x .husky/pre-commit",
59
+ "truncate -s 0 .husky/pre-commit",
60
+ "rsync x .husky/pre-commit",
61
+ "patch .husky/pre-commit change.patch",
62
+ "sponge .husky/pre-commit",
63
+ "perl -pi -e s/a/b/ .husky/pre-commit",
64
+ "node -e \"require('fs').writeFileSync('.husky/pre-commit','')\"",
65
+ 'ruby -e \'File.write(".husky/pre-commit", "")\'',
66
+ "bun -e \"Bun.write('.husky/pre-commit','')\"",
67
+ "/usr/bin/gh pr merge 1",
68
+ "gh pr merge 1; git status",
69
+ "git status&&gh pr merge 1",
70
+ 'bash -c "gh pr merge 1"',
71
+ "eval gh pr merge 1",
72
+ "$(gh pr merge 1)",
73
+ "echo `gh pr merge 1`",
74
+ "x=$(echo $(gh pr merge 1))",
75
+ "echo \"$(echo ')'; gh pr merge 1)\"",
76
+ "$(echo \\); gh pr merge 1)",
77
+ "$(echo x # )\ngh pr merge 1)",
78
+ 'echo "$(true&# )\ngh pr merge 1)"',
79
+ "{ gh pr merge 1; }",
80
+ "if true; then gh pr merge 1; fi",
81
+ "while true; do gh pr merge 1; done",
82
+ "if gh pr merge 1; then true; fi",
87
83
  "gh pr merge 1 --squash",
88
84
  "gh pr merge 1 --auto",
89
85
  "gh -R next-friday/example pr merge 1",
@@ -95,18 +91,11 @@ const blockedCommands = [
95
91
  "gh pr review 1 --approve=true",
96
92
  "gh pr review 1 -a",
97
93
  "gh -R next-friday/example pr review 1 -a",
98
- "gh alias set m 'pr merge'",
99
94
  "gh api repos/next-friday/example/pulls/1/merge -X PUT",
100
95
  "gh api graphql -f query='mutation { mergePullRequest(input: {}) { clientMutationId } }'",
101
- "gh api graphql -f query=@query.graphql",
102
96
  "gh api repos/next-friday/example/pulls/1/reviews -f event=APPROVE",
103
97
  "gh api -X POST repos/next-friday/example/actions/runs/1/pending_deployments -f state=approved",
104
98
  "gh pr review 1 -ca",
105
- "gh alias import aliases.yml",
106
- "gh api graphql -Fquery=@query.graphql",
107
- "gh api graphql --input query.json",
108
- "gh api -X POST repos/next-friday/example/pulls/1/reviews --input review.json",
109
- "gh api repos/next-friday/example/actions/runs/1/pending_deployments -F state=@state.txt",
110
99
  "gh api graphql -f query='mutation { enablePullRequestAutoMerge(input: {}) { clientMutationId } }'",
111
100
  "gh api graphql -f query='mutation { enqueuePullRequest(input: {}) { clientMutationId } }'",
112
101
  "gh api graphql -f query='mutation($e: PullRequestReviewEvent!) { addPullRequestReview(input: {event: $e}) { clientMutationId } }' -f e=APPROVE",
@@ -118,79 +107,34 @@ const blockedCommands = [
118
107
  "bash -c -- 'gh pr merge 1'",
119
108
  ];
120
109
 
121
- const irreversibleCommands = [
122
- "git push --force",
123
- "git push -f origin x",
124
- "git push origin x -f",
125
- "git push -uf origin x",
126
- "git push origin +x",
127
- "git push origin :x",
128
- "git push -d origin x",
129
- "git push --delete origin x",
130
- "git push --mirror",
131
- "git reset --hard HEAD~1",
132
- "git -C . reset --hard",
133
- "git clean -fd",
134
- "git clean -xdf",
135
- "git branch -D x",
136
- "git branch -df x",
137
- "git branch --delete --force x",
138
- "git checkout .",
139
- "git checkout -- .",
140
- "git checkout -f main",
141
- "git restore .",
142
- "git restore --worktree .",
143
- "git switch --discard-changes main",
144
- "git stash drop",
145
- "git worktree remove --force x",
146
- "git restore :/",
147
- "timeout 5 git push --force",
148
- "xargs git reset --hard",
149
- ];
150
-
151
110
  const allowedCommands = [
152
111
  'git commit -m "add login"',
153
- "git commit --signoff -m x",
154
- "git commit --amend --no-edit",
155
112
  "git status",
156
113
  "pnpm check",
157
- "git commit --verbose -m x",
158
114
  "cat .husky/pre-commit",
159
115
  "git diff .husky/pre-commit",
160
- 'git commit -m "feat(x): y" && git log -n 3',
161
- "git commit -m x; ls -n",
162
- "git commit -m \"$(printf 'feat(x): y')\"",
163
- "echo '$(git commit -n -m x)'",
164
- 'git commit -m "run with -n"',
165
- 'git commit -am "fix: -n flag handling"',
166
116
  "git diff .husky/pre-commit > /tmp/x",
167
117
  "ls .husky/ ; cp a b",
168
118
  'git commit -m "chore: rm .husky/pre-commit file"',
169
- 'gh pr create --title "docs(x): explain core.hooksPath"',
170
- 'gh pr create --title "ci(x): remove HUSKY=0 usage"',
171
- "git push -n",
119
+ "echo '$(gh pr merge 1)'",
120
+ 'gh pr create --title "docs(x): explain gh pr merge"',
172
121
  "node scripts/check-agent-gates.ts",
173
122
  "git push -u origin x",
174
- "git push --dry-run --force",
175
- "git push --force-with-lease origin x",
176
123
  'gh pr create --title "fix(x): y" --body z',
177
124
  "gh pr comment 1 --body x",
178
125
  'gh pr review 1 -c -b "fix -a flag"',
179
126
  'gh api repos/next-friday/example/issues/1/comments -f body="Please APPROVE"',
180
127
  "gh api repos/next-friday/example/pulls/1",
181
128
  "gh pr checks 1 --watch",
182
- "git branch -d x",
183
129
  "git checkout feature",
184
130
  "git checkout .changeset/x.md",
185
- "git restore --staged .",
186
- "git clean -n",
187
- "git reset --soft HEAD~1",
188
- "git stash pop",
189
131
  "timeout 60 pnpm test",
190
132
  'gh api graphql -f query="query { viewer { login } }"',
191
133
  'gh api repos/next-friday/example/issues/1/comments -f body="APPROVED by QA"',
192
134
  ];
193
135
 
136
+ const railwayProductionCommand = "railway up -e prod";
137
+
194
138
  const productionCommands = [
195
139
  "gh workflow run deploy.yml -f environment=prod -f commit_sha=abc",
196
140
  "gh workflow run Deploy --field 'environment=prod'",
@@ -211,7 +155,7 @@ const productionCommands = [
211
155
  "gh api repos/o/r/actions/workflows/1/dispatches -f inputs[environment]=$E",
212
156
  "bash -c 'gh workflow run deploy.yml -f environment=prod'",
213
157
  "railway up --ci --environment prod",
214
- "railway up -e prod",
158
+ railwayProductionCommand,
215
159
  "railway up -e=prod",
216
160
  "railway up -eprod",
217
161
  "railway up -ce prod",
@@ -261,6 +205,9 @@ const productionCommands = [
261
205
  "cd a\\\\\ngit push origin v1.0.0",
262
206
  ];
263
207
 
208
+ const approvalLiftsGate = ["CHG-123", "1", "CHG-1", "off", "00", " "];
209
+ const approvalKeepsGate = ["", "0", "false", "no", "FALSE", "No", "nO"];
210
+
264
211
  const nonProductionCommands = [
265
212
  "gh workflow run deploy.yml -f environment=uat -f commit_sha=abc",
266
213
  "gh api repos/o/r/actions/workflows/deploy.yml/dispatches -f 'inputs[environment]=uat'",
@@ -291,11 +238,78 @@ const mcpTools = {
291
238
  "mcp__github__merge_pull_request",
292
239
  "github__merge_pull_request",
293
240
  "github__enable_pr_auto_merge",
294
- "github__pull_request_review_write",
295
241
  ],
296
242
  allowed: ["github__create_pull_request", "github__add_issue_comment"],
297
243
  };
298
244
 
245
+ const reviewWriteCases = [
246
+ {
247
+ name: "APPROVE",
248
+ expectedExit: 2,
249
+ toolInput: {
250
+ method: "create",
251
+ event: "APPROVE",
252
+ },
253
+ },
254
+ {
255
+ name: "approve in lower case",
256
+ expectedExit: 2,
257
+ toolInput: {
258
+ method: "create",
259
+ event: "approve",
260
+ },
261
+ },
262
+ {
263
+ name: "Approve in mixed case",
264
+ expectedExit: 2,
265
+ toolInput: {
266
+ method: "create",
267
+ event: "Approve",
268
+ },
269
+ },
270
+ {
271
+ name: "submit_pending with APPROVE",
272
+ expectedExit: 2,
273
+ toolInput: {
274
+ method: "submit_pending",
275
+ event: "APPROVE",
276
+ },
277
+ },
278
+ {
279
+ name: "APPROVE in a JSON string, as Cursor sends it",
280
+ toolInput: '{"method":"create","event":"APPROVE"}',
281
+ expectedExit: 2,
282
+ },
283
+ {
284
+ name: "COMMENT",
285
+ expectedExit: 0,
286
+ toolInput: {
287
+ method: "create",
288
+ event: "COMMENT",
289
+ },
290
+ },
291
+ {
292
+ name: "REQUEST_CHANGES",
293
+ expectedExit: 0,
294
+ toolInput: {
295
+ method: "submit_pending",
296
+ event: "REQUEST_CHANGES",
297
+ },
298
+ },
299
+ {
300
+ name: "no event",
301
+ expectedExit: 0,
302
+ toolInput: {
303
+ method: "create",
304
+ },
305
+ },
306
+ {
307
+ name: "an empty JSON string",
308
+ toolInput: "{}",
309
+ expectedExit: 0,
310
+ },
311
+ ];
312
+
299
313
  const protectedPaths = [
300
314
  ".agents/rules/code-comments.md",
301
315
  ".agents/rules.json",
@@ -317,6 +331,62 @@ const protectedPaths = [
317
331
  "src/../.husky/pre-commit",
318
332
  ];
319
333
 
334
+ /**
335
+ * Create a repository on a branch, with one commit per file after an empty commit on `main`.
336
+ * @param branch - The branch to check out.
337
+ * @param committedFiles - Files to commit on the branch, one commit each.
338
+ * @returns The repository directory.
339
+ */
340
+ function createRepository(branch: string, committedFiles: string[]): string {
341
+ const directory = mkdtempSync(path.join(tmpdir(), "agent-gates-"));
342
+
343
+ const runGit = (...gitArguments: string[]): void => {
344
+ execFileSync(
345
+ "git",
346
+ [
347
+ "-C",
348
+ directory,
349
+ "-c",
350
+ "user.name=Gate Check",
351
+ "-c",
352
+ "user.email=gate-check@example.com",
353
+ "-c",
354
+ "commit.gpgsign=false",
355
+ ...gitArguments,
356
+ ],
357
+ {
358
+ env: childEnvironment,
359
+ stdio: "ignore",
360
+ },
361
+ );
362
+ };
363
+
364
+ runGit("init", "--initial-branch=main");
365
+ runGit("commit", "--allow-empty", "--message", "initial");
366
+ runGit("checkout", "-b", branch);
367
+
368
+ for (const file of committedFiles) {
369
+ writeFileSync(path.join(directory, path.basename(file)), "");
370
+ runGit("add", "--all");
371
+ runGit("commit", "--message", `add ${file}`);
372
+ }
373
+
374
+ return directory;
375
+ }
376
+
377
+ const fixBranch = "fix/login-error";
378
+ const featureBranch = "feat/login";
379
+ const testFileName = "login.test.ts";
380
+ const fixBranchWithTest = createRepository(fixBranch, [testFileName]);
381
+ const fixBranchWithoutTest = createRepository(fixBranch, ["login.ts"]);
382
+ const featureBranchWithTest = createRepository(featureBranch, [testFileName]);
383
+ const markerRepository = createRepository(featureBranch, []);
384
+
385
+ writeFileSync(path.join(markerRepository, ".git", "bug-fix-in-progress"), "");
386
+
387
+ const testFile = `src/${testFileName}`;
388
+ const outsideRepository = mkdtempSync(path.join(tmpdir(), "agent-gates-outside-"));
389
+
320
390
  const cases: GateCase[] = [
321
391
  ...blockedCommands.flatMap(command => [
322
392
  {
@@ -338,29 +408,6 @@ const cases: GateCase[] = [
338
408
  name: `Claude Bash: ${command}`,
339
409
  },
340
410
  ]),
341
- ...irreversibleCommands.flatMap(command => [
342
- {
343
- gate: shellGate,
344
- input: {
345
- command,
346
- },
347
- expectedExit: 0 as const,
348
- expectedPermission: "ask" as const,
349
- name: `Cursor shell asks first: ${command}`,
350
- },
351
- {
352
- gate: shellGate,
353
- input: {
354
- hook_event_name: "PreToolUse",
355
- tool_input: {
356
- command,
357
- },
358
- },
359
- expectedExit: 0 as const,
360
- expectedPermission: "ask" as const,
361
- name: `Claude Bash asks first: ${command}`,
362
- },
363
- ]),
364
411
  ...allowedCommands.map(command => ({
365
412
  gate: shellGate,
366
413
  input: {
@@ -390,18 +437,29 @@ const cases: GateCase[] = [
390
437
  name: `Claude production: ${command}`,
391
438
  },
392
439
  ]),
393
- {
440
+ ...approvalLiftsGate.map(releaseApproval => ({
394
441
  gate: productionGate,
395
- releaseApproval: "CHG-1",
396
- expectedPermission: "allow",
397
- name: "production with RELEASE_APPROVAL: railway up -e prod",
398
- expectedExit: 0,
442
+ releaseApproval,
399
443
  input: {
400
444
  tool_input: {
401
- command: "railway up -e prod",
445
+ command: railwayProductionCommand,
402
446
  },
403
447
  },
404
- },
448
+ expectedPermission: "allow" as const,
449
+ name: `production with RELEASE_APPROVAL="${releaseApproval}": ${railwayProductionCommand}`,
450
+ expectedExit: 0 as const,
451
+ })),
452
+ ...approvalKeepsGate.map(releaseApproval => ({
453
+ gate: productionGate,
454
+ releaseApproval,
455
+ input: {
456
+ tool_input: {
457
+ command: railwayProductionCommand,
458
+ },
459
+ },
460
+ name: `production with RELEASE_APPROVAL="${releaseApproval}": ${railwayProductionCommand}`,
461
+ expectedExit: 2 as const,
462
+ })),
405
463
  ...nonProductionCommands.map(command => ({
406
464
  gate: productionGate,
407
465
  input: {
@@ -430,63 +488,6 @@ const cases: GateCase[] = [
430
488
  expectedPermission: "allow" as const,
431
489
  name: `MCP: ${tool}`,
432
490
  })),
433
- {
434
- gate: suppressionGate,
435
- name: "suppression: eslint-disable",
436
- expectedExit: 2,
437
- input: {
438
- tool_input: {
439
- file_path: sourceFile,
440
- content: "// eslint-disable-next-line\n",
441
- },
442
- },
443
- },
444
- ...["// NOSONAR", "# noqa: E501", "# hadolint ignore=DL3008", "# trivy:ignore:AVD-DS-0002"].map(
445
- content => ({
446
- gate: suppressionGate,
447
- input: {
448
- tool_input: {
449
- file_path: sourceFile,
450
- content,
451
- },
452
- },
453
- expectedExit: 2 as const,
454
- name: `suppression: ${content}`,
455
- }),
456
- ),
457
- {
458
- gate: suppressionGate,
459
- name: "suppression: noqa inside a word",
460
- expectedExit: 0,
461
- input: {
462
- tool_input: {
463
- file_path: sourceFile,
464
- content: "const canoqaValue = 1;\n",
465
- },
466
- },
467
- },
468
- {
469
- gate: suppressionGate,
470
- name: "suppression: exempt directory reached through ..",
471
- expectedExit: 2,
472
- input: {
473
- tool_input: {
474
- file_path: ".agents/rules/../../src/example.ts",
475
- content: "// eslint-disable-next-line\n",
476
- },
477
- },
478
- },
479
- {
480
- gate: suppressionGate,
481
- name: "suppression: clean edit",
482
- expectedExit: 0,
483
- input: {
484
- tool_input: {
485
- file_path: sourceFile,
486
- content: "export const value = 1;\n",
487
- },
488
- },
489
- },
490
491
  ...protectedPaths.map(file => ({
491
492
  gate: constitutionGate,
492
493
  input: {
@@ -497,6 +498,15 @@ const cases: GateCase[] = [
497
498
  expectedExit: 2 as const,
498
499
  name: `constitution: ${file}`,
499
500
  })),
501
+ ...reviewWriteCases.map(({expectedExit, name, toolInput}) => ({
502
+ gate: mcpGate,
503
+ input: {
504
+ tool_input: toolInput,
505
+ tool_name: "mcp__github__pull_request_review_write",
506
+ },
507
+ expectedExit: expectedExit as 0 | 2,
508
+ name: `MCP: pull_request_review_write with ${name}`,
509
+ })),
500
510
  {
501
511
  gate: constitutionGate,
502
512
  name: "constitution: src file",
@@ -507,36 +517,51 @@ const cases: GateCase[] = [
507
517
  },
508
518
  },
509
519
  },
520
+ ...[
521
+ {
522
+ projectDirectory: fixBranchWithTest,
523
+ name: "fix branch with a committed test",
524
+ expectedExit: 2,
525
+ },
526
+ {
527
+ projectDirectory: fixBranchWithoutTest,
528
+ name: "fix branch without a committed test",
529
+ expectedExit: 0,
530
+ },
531
+ {
532
+ projectDirectory: featureBranchWithTest,
533
+ name: "other branch with a committed test",
534
+ expectedExit: 0,
535
+ },
536
+ {
537
+ projectDirectory: markerRepository,
538
+ name: "bug-fix-in-progress marker",
539
+ expectedExit: 2,
540
+ },
541
+ {
542
+ projectDirectory: outsideRepository,
543
+ name: "directory outside a Git repository",
544
+ expectedExit: 0,
545
+ },
546
+ ].map(({expectedExit, name, projectDirectory}) => ({
547
+ gate: testEditGate,
548
+ projectDirectory,
549
+ input: {
550
+ tool_input: {
551
+ file_path: testFile,
552
+ },
553
+ },
554
+ expectedExit: expectedExit as 0 | 2,
555
+ name: `test edit: ${name}`,
556
+ })),
510
557
  ];
511
558
 
512
- /**
513
- * Reads the permission a gate printed in the format its tool reads: Claude Code's
514
- * `hookSpecificOutput` for a `PreToolUse` input, and Cursor's `permission` otherwise.
515
- * @param gateCase - The case whose input decides the format.
516
- * @param stdout - The gate's standard output.
517
- * @returns The permission, or undefined when the output is not JSON.
518
- */
519
- function permissionOf(gateCase: GateCase, stdout: string): string | undefined {
520
- try {
521
- const output = JSON.parse(stdout) as {
522
- hookSpecificOutput?: {permissionDecision?: string};
523
- permission?: string;
524
- };
525
-
526
- return gateCase.input.hook_event_name === "PreToolUse" && gateCase.expectedPermission === "ask"
527
- ? output.hookSpecificOutput?.permissionDecision
528
- : output.permission;
529
- } catch {
530
- return undefined;
531
- }
532
- }
533
-
534
559
  const failures = cases.filter(gateCase => {
535
560
  const result = spawnSync(gateCase.gate, {
536
561
  env: {
537
- ...process.env,
538
- CLAUDE_PROJECT_DIR: process.cwd(),
539
- CURSOR_PROJECT_DIR: process.cwd(),
562
+ ...childEnvironment,
563
+ CLAUDE_PROJECT_DIR: gateCase.projectDirectory ?? process.cwd(),
564
+ CURSOR_PROJECT_DIR: gateCase.projectDirectory ?? process.cwd(),
540
565
  RELEASE_APPROVAL: gateCase.releaseApproval,
541
566
  },
542
567
  input: JSON.stringify(gateCase.input),
@@ -544,11 +569,25 @@ const failures = cases.filter(gateCase => {
544
569
 
545
570
  const isPermissionWrong =
546
571
  gateCase.expectedPermission !== undefined &&
547
- permissionOf(gateCase, result.stdout.toString()) !== gateCase.expectedPermission;
572
+ (JSON.parse(result.stdout.toString()) as {permission?: string}).permission !==
573
+ gateCase.expectedPermission;
548
574
 
549
575
  return result.status !== gateCase.expectedExit || isPermissionWrong;
550
576
  });
551
577
 
578
+ for (const directory of [
579
+ fixBranchWithTest,
580
+ fixBranchWithoutTest,
581
+ featureBranchWithTest,
582
+ markerRepository,
583
+ outsideRepository,
584
+ ]) {
585
+ rmSync(directory, {
586
+ recursive: true,
587
+ force: true,
588
+ });
589
+ }
590
+
552
591
  if (failures.length > 0) {
553
592
  const failureLines = failures.map(gateCase => {
554
593
  const permission =