@next-friday/create-app 9.1.0 → 9.2.0

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