@soleur/opencode 0.0.0-stage → 3.354.2

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 (832) hide show
  1. package/.claude-plugin/agents.manifest.json +476 -0
  2. package/.claude-plugin/plugin.json +42 -0
  3. package/README.md +44 -2
  4. package/agents/community/.gitkeep +0 -0
  5. package/agents/engineering/cto.md +87 -0
  6. package/agents/engineering/design/ddd-architect.md +95 -0
  7. package/agents/engineering/discovery/agent-finder.md +214 -0
  8. package/agents/engineering/discovery/functional-discovery.md +182 -0
  9. package/agents/engineering/infra/infra-security.md +151 -0
  10. package/agents/engineering/infra/platform-strategist.md +109 -0
  11. package/agents/engineering/infra/terraform-architect.md +171 -0
  12. package/agents/engineering/prompt-engineer.md +37 -0
  13. package/agents/engineering/research/best-practices-researcher.md +115 -0
  14. package/agents/engineering/research/framework-docs-researcher.md +93 -0
  15. package/agents/engineering/research/git-history-analyzer.md +53 -0
  16. package/agents/engineering/research/learnings-researcher.md +285 -0
  17. package/agents/engineering/research/repo-research-analyst.md +118 -0
  18. package/agents/engineering/review/agent-native-reviewer.md +279 -0
  19. package/agents/engineering/review/architecture-strategist.md +61 -0
  20. package/agents/engineering/review/code-quality-analyst.md +92 -0
  21. package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
  22. package/agents/engineering/review/data-integrity-guardian.md +82 -0
  23. package/agents/engineering/review/data-migration-expert.md +98 -0
  24. package/agents/engineering/review/deployment-verification-agent.md +183 -0
  25. package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
  26. package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
  27. package/agents/engineering/review/legacy-code-expert.md +55 -0
  28. package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
  29. package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
  30. package/agents/engineering/review/performance-oracle.md +130 -0
  31. package/agents/engineering/review/security-sentinel.md +167 -0
  32. package/agents/engineering/review/semgrep-sast.md +36 -0
  33. package/agents/engineering/review/test-design-reviewer.md +150 -0
  34. package/agents/engineering/review/user-impact-reviewer.md +70 -0
  35. package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
  36. package/agents/finance/budget-analyst.md +29 -0
  37. package/agents/finance/cfo.md +44 -0
  38. package/agents/finance/financial-reporter.md +30 -0
  39. package/agents/finance/revenue-analyst.md +30 -0
  40. package/agents/legal/clo.md +115 -0
  41. package/agents/legal/legal-compliance-auditor.md +110 -0
  42. package/agents/legal/legal-document-generator.md +134 -0
  43. package/agents/marketing/analytics-analyst.md +25 -0
  44. package/agents/marketing/brand-architect.md +181 -0
  45. package/agents/marketing/cmo.md +76 -0
  46. package/agents/marketing/conversion-optimizer.md +19 -0
  47. package/agents/marketing/copywriter.md +20 -0
  48. package/agents/marketing/fact-checker.md +70 -0
  49. package/agents/marketing/growth-strategist.md +145 -0
  50. package/agents/marketing/paid-media-strategist.md +28 -0
  51. package/agents/marketing/pricing-strategist.md +17 -0
  52. package/agents/marketing/programmatic-seo-specialist.md +25 -0
  53. package/agents/marketing/retention-strategist.md +23 -0
  54. package/agents/marketing/seo-aeo-analyst.md +164 -0
  55. package/agents/operations/coo.md +50 -0
  56. package/agents/operations/ops-advisor.md +78 -0
  57. package/agents/operations/ops-provisioner.md +101 -0
  58. package/agents/operations/ops-research.md +52 -0
  59. package/agents/operations/service-automator.md +278 -0
  60. package/agents/product/business-validator.md +206 -0
  61. package/agents/product/competitive-intelligence.md +89 -0
  62. package/agents/product/cpo.md +63 -0
  63. package/agents/product/design/ux-design-lead.md +209 -0
  64. package/agents/product/spec-flow-analyzer.md +122 -0
  65. package/agents/sales/cro.md +44 -0
  66. package/agents/sales/deal-architect.md +29 -0
  67. package/agents/sales/outbound-strategist.md +29 -0
  68. package/agents/sales/pipeline-analyst.md +29 -0
  69. package/agents/support/cco.md +48 -0
  70. package/agents/support/community-manager.md +570 -0
  71. package/agents/support/ticket-triage.md +70 -0
  72. package/bin/soleur-opencode.js +29 -0
  73. package/commands/go.md +660 -0
  74. package/commands/help.md +273 -0
  75. package/commands/sync.md +977 -0
  76. package/commands/version.md +103 -0
  77. package/hooks/antigravity-hooks.json +84 -0
  78. package/hooks/antigravity-session-start.sh +15 -0
  79. package/hooks/bare-root-cwd-guard.sh +131 -0
  80. package/hooks/browser-snapshot-credential-guard.sh +200 -0
  81. package/hooks/codex-session-start.sh +9 -0
  82. package/hooks/compaction-state.sh +525 -0
  83. package/hooks/destructive-command-guard.sh +1387 -0
  84. package/hooks/devin-session-start.sh +90 -0
  85. package/hooks/ext-filter-detect.sh +171 -0
  86. package/hooks/hooks.json +133 -0
  87. package/hooks/lib/hook-tool-kind.sh +24 -0
  88. package/hooks/lib/log-rotation.sh +176 -0
  89. package/hooks/lib/shell-argv.pl +576 -0
  90. package/hooks/memory-backstop.sh +1114 -0
  91. package/hooks/operator-stage-approval.sh +391 -0
  92. package/hooks/stop-hook.sh +367 -0
  93. package/hooks/unkept-promise-hook.sh +383 -0
  94. package/hooks/version-banner.sh +127 -0
  95. package/hooks/welcome-hook.sh +50 -0
  96. package/install.js +29 -0
  97. package/lib/agent-registry.ts +154 -0
  98. package/lib/c4-canonical-cli.mjs +34 -0
  99. package/lib/c4-canonical.mjs +46 -0
  100. package/lib/c4-from-components.ts +554 -0
  101. package/lib/go-routing.ts +81 -0
  102. package/lib/grok-inspect-contract.ts +311 -0
  103. package/lib/harness-model-map.ts +101 -0
  104. package/lib/harness-parity.ts +726 -0
  105. package/lib/harness-tool-map.ts +180 -0
  106. package/lib/harness.ts +716 -0
  107. package/lib/heartbeat-live-reconcile.ts +1153 -0
  108. package/lib/heartbeat-manifest.ts +361 -0
  109. package/lib/kb-coverage.ts +252 -0
  110. package/lib/pr-merge-poll.ts +103 -0
  111. package/lib/workflow-fidelity.ts +388 -0
  112. package/opencode/INSTRUCTIONS.md +171 -0
  113. package/opencode/agents/engineering/cto.md +10 -0
  114. package/opencode/agents/engineering/design/ddd-architect.md +10 -0
  115. package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
  116. package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
  117. package/opencode/agents/engineering/infra/infra-security.md +10 -0
  118. package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
  119. package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
  120. package/opencode/agents/engineering/prompt-engineer.md +10 -0
  121. package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
  122. package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
  123. package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
  124. package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
  125. package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
  126. package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
  127. package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
  128. package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
  129. package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
  130. package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
  131. package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
  132. package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
  133. package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
  134. package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
  135. package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
  136. package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
  137. package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
  138. package/opencode/agents/engineering/review/performance-oracle.md +10 -0
  139. package/opencode/agents/engineering/review/security-sentinel.md +10 -0
  140. package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
  141. package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
  142. package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
  143. package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
  144. package/opencode/agents/finance/budget-analyst.md +10 -0
  145. package/opencode/agents/finance/cfo.md +10 -0
  146. package/opencode/agents/finance/financial-reporter.md +10 -0
  147. package/opencode/agents/finance/revenue-analyst.md +10 -0
  148. package/opencode/agents/legal/clo.md +10 -0
  149. package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
  150. package/opencode/agents/legal/legal-document-generator.md +10 -0
  151. package/opencode/agents/marketing/analytics-analyst.md +10 -0
  152. package/opencode/agents/marketing/brand-architect.md +10 -0
  153. package/opencode/agents/marketing/cmo.md +10 -0
  154. package/opencode/agents/marketing/conversion-optimizer.md +10 -0
  155. package/opencode/agents/marketing/copywriter.md +10 -0
  156. package/opencode/agents/marketing/fact-checker.md +10 -0
  157. package/opencode/agents/marketing/growth-strategist.md +10 -0
  158. package/opencode/agents/marketing/paid-media-strategist.md +10 -0
  159. package/opencode/agents/marketing/pricing-strategist.md +10 -0
  160. package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
  161. package/opencode/agents/marketing/retention-strategist.md +10 -0
  162. package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
  163. package/opencode/agents/operations/coo.md +10 -0
  164. package/opencode/agents/operations/ops-advisor.md +10 -0
  165. package/opencode/agents/operations/ops-provisioner.md +10 -0
  166. package/opencode/agents/operations/ops-research.md +10 -0
  167. package/opencode/agents/operations/service-automator.md +10 -0
  168. package/opencode/agents/product/business-validator.md +10 -0
  169. package/opencode/agents/product/competitive-intelligence.md +10 -0
  170. package/opencode/agents/product/cpo.md +10 -0
  171. package/opencode/agents/product/design/ux-design-lead.md +10 -0
  172. package/opencode/agents/product/spec-flow-analyzer.md +10 -0
  173. package/opencode/agents/sales/cro.md +10 -0
  174. package/opencode/agents/sales/deal-architect.md +10 -0
  175. package/opencode/agents/sales/outbound-strategist.md +10 -0
  176. package/opencode/agents/sales/pipeline-analyst.md +10 -0
  177. package/opencode/agents/support/cco.md +10 -0
  178. package/opencode/agents/support/community-manager.md +10 -0
  179. package/opencode/agents/support/ticket-triage.md +10 -0
  180. package/opencode/commands/go.md +10 -0
  181. package/opencode/commands/help.md +10 -0
  182. package/opencode/commands/sync.md +10 -0
  183. package/opencode/commands/version.md +11 -0
  184. package/opencode/opencode.json +8 -0
  185. package/opencode/plugin.ts +818 -0
  186. package/package.json +36 -4
  187. package/scripts/admin-merge-ready.sh +545 -0
  188. package/scripts/admin-merge-ready.test.sh +753 -0
  189. package/scripts/affected-tests.sh +908 -0
  190. package/scripts/affected-tests.test.sh +1797 -0
  191. package/scripts/alpha-metrics.sh +78 -0
  192. package/scripts/alpha-metrics.test.sh +115 -0
  193. package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
  194. package/scripts/audit-flag-flip.sh +107 -0
  195. package/scripts/check-red-on-main.sh +316 -0
  196. package/scripts/ci-head-verdict.sh +411 -0
  197. package/scripts/cloud-detect.sh +193 -0
  198. package/scripts/delegation-verify.sh +657 -0
  199. package/scripts/delegation-verify.test.sh +1424 -0
  200. package/scripts/deploy-arm.sh +475 -0
  201. package/scripts/domain-model-drift.sh +419 -0
  202. package/scripts/emit-decision.sh +144 -0
  203. package/scripts/emit-decision.test.sh +144 -0
  204. package/scripts/ext-filter-divergence-corpus.txt +15 -0
  205. package/scripts/ext-filter-divergence.sh +189 -0
  206. package/scripts/generate-c4-from-components.ts +442 -0
  207. package/scripts/grok-fidelity-gate.sh +68 -0
  208. package/scripts/grok-pre-push-gate.sh +232 -0
  209. package/scripts/harness-discovery-smoke.ts +906 -0
  210. package/scripts/harness-parity-census.ts +92 -0
  211. package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
  212. package/scripts/install-opencode-adapter.ts +509 -0
  213. package/scripts/lib/affected-tests/common.sh +365 -0
  214. package/scripts/lib/affected-tests/go.sh +157 -0
  215. package/scripts/lib/affected-tests/js.sh +188 -0
  216. package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
  217. package/scripts/lib/affected-tests/python.sh +185 -0
  218. package/scripts/lib/context-sanitize.sh +49 -0
  219. package/scripts/lib/domain-model-lib.sh +180 -0
  220. package/scripts/lib/ext-filter-grammar.tsv +34 -0
  221. package/scripts/lib/operator-script.sh +1236 -0
  222. package/scripts/lib/proc.sh +580 -0
  223. package/scripts/lib/session-state.sh +748 -0
  224. package/scripts/lib/tmp-classify.sh +843 -0
  225. package/scripts/lib/wrapper-boundary.sh +47 -0
  226. package/scripts/lint-ratchet.py +1547 -0
  227. package/scripts/monitor-pr-checks.sh +441 -0
  228. package/scripts/pipeline-tally.sh +756 -0
  229. package/scripts/pipeline-tally.test.sh +740 -0
  230. package/scripts/plugin-version.sh +528 -0
  231. package/scripts/precommit-guard.sh +115 -0
  232. package/scripts/precommit-guard.test.sh +254 -0
  233. package/scripts/reconcile-live-heartbeats.ts +705 -0
  234. package/scripts/render-c4-model.sh +217 -0
  235. package/scripts/resolve-git-root.sh +55 -0
  236. package/scripts/resolve-regenerable-conflicts.sh +363 -0
  237. package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
  238. package/scripts/ruff-requirements.txt +22 -0
  239. package/scripts/run-in-pid-namespace.sh +98 -0
  240. package/scripts/run-in-pid-namespace.test.sh +290 -0
  241. package/scripts/scan-ancestor-signal-helpers.py +556 -0
  242. package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
  243. package/scripts/setup-ralph-loop.sh +238 -0
  244. package/scripts/sync-cursor-name-map.ts +316 -0
  245. package/scripts/sync-grok-agent-compat.ts +188 -0
  246. package/scripts/sync-npm-package.ts +165 -0
  247. package/scripts/sync-opencode-adapter.ts +498 -0
  248. package/scripts/sync-pr-behind.sh +647 -0
  249. package/scripts/sync-pr-behind.test.sh +252 -0
  250. package/scripts/taste-profile-update.sh +180 -0
  251. package/scripts/taste-profile-update.test.sh +167 -0
  252. package/scripts/write-budget-marker.sh +23 -0
  253. package/scripts/write-kb-coverage.ts +113 -0
  254. package/skills/admin-ip-refresh/SKILL.md +81 -0
  255. package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
  256. package/skills/agent-browser/SKILL.md +698 -0
  257. package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
  258. package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
  259. package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
  260. package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
  261. package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
  262. package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
  263. package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
  264. package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
  265. package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
  266. package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
  267. package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
  268. package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
  269. package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
  270. package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
  271. package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
  272. package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
  273. package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
  274. package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
  275. package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
  276. package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
  277. package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
  278. package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
  279. package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
  280. package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
  281. package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
  282. package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
  283. package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
  284. package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
  285. package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
  286. package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
  287. package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
  288. package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
  289. package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
  290. package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
  291. package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
  292. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
  293. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
  294. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
  295. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
  296. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
  297. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
  298. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
  299. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
  300. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
  301. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
  302. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
  303. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
  304. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
  305. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
  306. package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
  307. package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
  308. package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
  309. package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
  310. package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
  311. package/skills/agent-native-architecture/SKILL.md +479 -0
  312. package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
  313. package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
  314. package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
  315. package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
  316. package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
  317. package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
  318. package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
  319. package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
  320. package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
  321. package/skills/agent-native-architecture/references/product-implications.md +478 -0
  322. package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
  323. package/skills/agent-native-architecture/references/self-modification.md +294 -0
  324. package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
  325. package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
  326. package/skills/agent-native-audit/SKILL.md +300 -0
  327. package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
  328. package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
  329. package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
  330. package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
  331. package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
  332. package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
  333. package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
  334. package/skills/architecture/SKILL.md +436 -0
  335. package/skills/architecture/references/adr-template.md +154 -0
  336. package/skills/architecture/references/likec4-reference.md +136 -0
  337. package/skills/architecture/references/nfr-reference.md +126 -0
  338. package/skills/archive-kb/SKILL.md +86 -0
  339. package/skills/archive-kb/scripts/archive-kb.sh +300 -0
  340. package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
  341. package/skills/atdd-developer/SKILL.md +65 -0
  342. package/skills/brainstorm/SKILL.md +701 -0
  343. package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
  344. package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
  345. package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
  346. package/skills/brainstorm/references/ui-surface-terms.md +42 -0
  347. package/skills/brainstorm-techniques/SKILL.md +231 -0
  348. package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
  349. package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
  350. package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
  351. package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
  352. package/skills/campaign-calendar/SKILL.md +145 -0
  353. package/skills/cf-token-scope/SKILL.md +164 -0
  354. package/skills/cf-token-scope/references/widen-playbook.md +114 -0
  355. package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
  356. package/skills/changelog/SKILL.md +142 -0
  357. package/skills/code-to-prd/SKILL.md +66 -0
  358. package/skills/code-to-prd/references/banner-template.md +12 -0
  359. package/skills/code-to-prd/references/prd-template.md +41 -0
  360. package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
  361. package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
  362. package/skills/code-to-prd/test/fixture/.env.example +7 -0
  363. package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
  364. package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
  365. package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
  366. package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
  367. package/skills/code-to-prd/test/fixture/package.json +6 -0
  368. package/skills/cohort-status/SKILL.md +62 -0
  369. package/skills/community/SKILL.md +210 -0
  370. package/skills/community/scripts/bsky-community.sh +644 -0
  371. package/skills/community/scripts/bsky-setup.sh +631 -0
  372. package/skills/community/scripts/community-router.sh +93 -0
  373. package/skills/community/scripts/discord-community.sh +487 -0
  374. package/skills/community/scripts/discord-setup.sh +784 -0
  375. package/skills/community/scripts/github-community.sh +706 -0
  376. package/skills/community/scripts/hn-community.sh +233 -0
  377. package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
  378. package/skills/community/scripts/linkedin-community.sh +750 -0
  379. package/skills/community/scripts/linkedin-setup.sh +758 -0
  380. package/skills/community/scripts/x-community.sh +890 -0
  381. package/skills/community/scripts/x-setup.sh +713 -0
  382. package/skills/community/test/community-argv.test.sh +2922 -0
  383. package/skills/community/test/github-community.test.sh +981 -0
  384. package/skills/competitive-analysis/SKILL.md +69 -0
  385. package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
  386. package/skills/compound/SKILL.md +673 -0
  387. package/skills/compound/scripts/token-efficiency-report.sh +334 -0
  388. package/skills/compound/test/phase-16.test.sh +379 -0
  389. package/skills/compound-capture/SKILL.md +791 -0
  390. package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
  391. package/skills/compound-capture/assets/resolution-template.md +116 -0
  392. package/skills/compound-capture/references/yaml-schema.md +67 -0
  393. package/skills/compound-capture/schema.yaml +181 -0
  394. package/skills/constraint-scaffold/SKILL.md +175 -0
  395. package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
  396. package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
  397. package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
  398. package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
  399. package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
  400. package/skills/constraint-scaffold/references/shared-runner.template +135 -0
  401. package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
  402. package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
  403. package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
  404. package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
  405. package/skills/constraint-scaffold/test/generator.test.sh +93 -0
  406. package/skills/constraint-scaffold/test/parity.test.sh +285 -0
  407. package/skills/content-writer/SKILL.md +242 -0
  408. package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
  409. package/skills/cron-delete/SKILL.md +56 -0
  410. package/skills/cron-list/SKILL.md +54 -0
  411. package/skills/deepen-plan/SKILL.md +937 -0
  412. package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
  413. package/skills/deploy/SKILL.md +115 -0
  414. package/skills/deploy/references/hetzner-setup.md +67 -0
  415. package/skills/deploy/scripts/deploy.sh +37 -0
  416. package/skills/deploy-docs/SKILL.md +95 -0
  417. package/skills/dhh-rails-style/SKILL.md +203 -0
  418. package/skills/dhh-rails-style/references/architecture.md +717 -0
  419. package/skills/dhh-rails-style/references/controllers.md +323 -0
  420. package/skills/dhh-rails-style/references/frontend.md +556 -0
  421. package/skills/dhh-rails-style/references/gems.md +316 -0
  422. package/skills/dhh-rails-style/references/models.md +388 -0
  423. package/skills/dhh-rails-style/references/testing.md +354 -0
  424. package/skills/discord-content/SKILL.md +126 -0
  425. package/skills/docs-site/SKILL.md +214 -0
  426. package/skills/drain-labeled-backlog/SKILL.md +203 -0
  427. package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
  428. package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
  429. package/skills/drain-prs/SKILL.md +143 -0
  430. package/skills/drain-prs/scripts/triage-prs.sh +192 -0
  431. package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
  432. package/skills/dspy-ruby/SKILL.md +634 -0
  433. package/skills/dspy-ruby/assets/config-template.rb +359 -0
  434. package/skills/dspy-ruby/assets/module-template.rb +326 -0
  435. package/skills/dspy-ruby/assets/signature-template.rb +143 -0
  436. package/skills/dspy-ruby/references/core-concepts.md +268 -0
  437. package/skills/dspy-ruby/references/optimization.md +623 -0
  438. package/skills/dspy-ruby/references/providers.md +335 -0
  439. package/skills/eval-harness/README.md +194 -0
  440. package/skills/eval-harness/SKILL.md +157 -0
  441. package/skills/eval-harness/enums/go-routes.json +13 -0
  442. package/skills/eval-harness/enums/incident-threshold.json +5 -0
  443. package/skills/eval-harness/enums/lane.json +5 -0
  444. package/skills/eval-harness/enums/tool-selection.json +20 -0
  445. package/skills/eval-harness/enums/triage-levels.json +5 -0
  446. package/skills/eval-harness/gated-skills.json +34 -0
  447. package/skills/eval-harness/models.generated.json +5 -0
  448. package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
  449. package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
  450. package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
  451. package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
  452. package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
  453. package/skills/eval-harness/prompts/go-baseline.txt +6 -0
  454. package/skills/eval-harness/prompts/go-skill.txt +21 -0
  455. package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
  456. package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
  457. package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
  458. package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
  459. package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
  460. package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
  461. package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
  462. package/skills/eval-harness/prompts/triage-skill.txt +10 -0
  463. package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
  464. package/skills/eval-harness/scripts/extract-block.cjs +87 -0
  465. package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
  466. package/skills/eval-harness/scripts/gen-models.sh +41 -0
  467. package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
  468. package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
  469. package/skills/eval-harness/scripts/parse-label.cjs +93 -0
  470. package/skills/eval-harness/scripts/verdict.cjs +132 -0
  471. package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
  472. package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
  473. package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
  474. package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
  475. package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
  476. package/skills/eval-harness/test/eval-gate.test.sh +132 -0
  477. package/skills/eval-harness/test/extract-block.test.sh +87 -0
  478. package/skills/eval-harness/test/gate-classification.test.sh +77 -0
  479. package/skills/eval-harness/test/gen-models.test.sh +75 -0
  480. package/skills/eval-harness/test/measure-classification.test.sh +74 -0
  481. package/skills/eval-harness/test/parse-label.test.sh +129 -0
  482. package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
  483. package/skills/eval-harness/test/verdict.test.sh +123 -0
  484. package/skills/every-style-editor/SKILL.md +142 -0
  485. package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
  486. package/skills/feature-tweet/SKILL.md +199 -0
  487. package/skills/feature-video/SKILL.md +412 -0
  488. package/skills/feature-video/scripts/check_deps.sh +281 -0
  489. package/skills/file-todos/SKILL.md +267 -0
  490. package/skills/file-todos/assets/todo-template.md +166 -0
  491. package/skills/fix-issue/SKILL.md +278 -0
  492. package/skills/fix-issue/references/agent-authored-exclusion.md +120 -0
  493. package/skills/fix-issue/references/exclude-label-jq-snippet.md +97 -0
  494. package/skills/flag-bootstrap/SETUP.md +142 -0
  495. package/skills/flag-create/SKILL.md +145 -0
  496. package/skills/flag-create/scripts/create.sh +273 -0
  497. package/skills/flag-delete/SKILL.md +167 -0
  498. package/skills/flag-delete/scripts/delete.sh +288 -0
  499. package/skills/flag-list/SKILL.md +99 -0
  500. package/skills/flag-list/scripts/list.sh +275 -0
  501. package/skills/flag-set-role/SKILL.md +229 -0
  502. package/skills/flag-set-role/scripts/flip.sh +774 -0
  503. package/skills/frontend-anti-slop/SKILL.md +106 -0
  504. package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
  505. package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
  506. package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
  507. package/skills/frontend-design/SKILL.md +77 -0
  508. package/skills/gdpr-gate/NOTICE +136 -0
  509. package/skills/gdpr-gate/SKILL.md +341 -0
  510. package/skills/gdpr-gate/references/fields.md +160 -0
  511. package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
  512. package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
  513. package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
  514. package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
  515. package/skills/gdpr-gate/references/layers/frontend.md +249 -0
  516. package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
  517. package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
  518. package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
  519. package/skills/gdpr-gate/references/legal-consent.md +140 -0
  520. package/skills/gdpr-gate/references/non-negotiables.md +60 -0
  521. package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
  522. package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
  523. package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
  524. package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
  525. package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
  526. package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
  527. package/skills/gemini-imagegen/SKILL.md +283 -0
  528. package/skills/gemini-imagegen/requirements.txt +5 -0
  529. package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
  530. package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
  531. package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
  532. package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
  533. package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
  534. package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
  535. package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
  536. package/skills/git-worktree/SKILL.md +534 -0
  537. package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
  538. package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
  539. package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
  540. package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
  541. package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
  542. package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
  543. package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
  544. package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
  545. package/skills/go/SKILL.md +54 -0
  546. package/skills/growth/SKILL.md +164 -0
  547. package/skills/harvest-debt/SKILL.md +78 -0
  548. package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
  549. package/skills/heal-skill/SKILL.md +173 -0
  550. package/skills/help/SKILL.md +46 -0
  551. package/skills/incident/NOTICE +39 -0
  552. package/skills/incident/SKILL.md +395 -0
  553. package/skills/incident/scripts/dry-run.sh +448 -0
  554. package/skills/incident/scripts/redact-engine.py +412 -0
  555. package/skills/incident/scripts/redact-sentinel.sh +44 -0
  556. package/skills/incident/templates/pir.md +139 -0
  557. package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
  558. package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
  559. package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
  560. package/skills/incident/test/fixtures/positive-corpus.md +87 -0
  561. package/skills/incident/test/redact-sentinel.test.sh +1420 -0
  562. package/skills/invoice/SKILL.md +206 -0
  563. package/skills/kb-glossary/SKILL.md +71 -0
  564. package/skills/kb-glossary/references/glossary-format.md +111 -0
  565. package/skills/kb-glossary/references/rejected-request-register.md +115 -0
  566. package/skills/kb-search/SKILL.md +276 -0
  567. package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
  568. package/skills/legal-audit/SKILL.md +161 -0
  569. package/skills/legal-generate/NOTICE +242 -0
  570. package/skills/legal-generate/SKILL.md +244 -0
  571. package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
  572. package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
  573. package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
  574. package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
  575. package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
  576. package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
  577. package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
  578. package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
  579. package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
  580. package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
  581. package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
  582. package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
  583. package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
  584. package/skills/linear-fetch/SKILL.md +247 -0
  585. package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
  586. package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
  587. package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
  588. package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
  589. package/skills/linear-fetch/test/parity.test.sh +112 -0
  590. package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
  591. package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
  592. package/skills/merge-pr/SKILL.md +670 -0
  593. package/skills/model-launch-review/SKILL.md +255 -0
  594. package/skills/model-launch-review/scripts/audit-models.sh +501 -0
  595. package/skills/one-shot/SKILL.md +316 -0
  596. package/skills/one-shot/references/token-discipline.md +74 -0
  597. package/skills/operator-bootstrap/SKILL.md +412 -0
  598. package/skills/operator-bootstrap/template.sh +439 -0
  599. package/skills/operator-digest/SKILL.md +287 -0
  600. package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
  601. package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
  602. package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
  603. package/skills/operator-rephrase/SKILL.md +98 -0
  604. package/skills/pencil-setup/SKILL.md +201 -0
  605. package/skills/pencil-setup/scripts/check_deps.sh +556 -0
  606. package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
  607. package/skills/pencil-setup/scripts/package-lock.json +1172 -0
  608. package/skills/pencil-setup/scripts/package.json +14 -0
  609. package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
  610. package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
  611. package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
  612. package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
  613. package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
  614. package/skills/plan/SKILL.md +1140 -0
  615. package/skills/plan/references/differential-migration-testing.md +185 -0
  616. package/skills/plan/references/plan-community-discovery.md +42 -0
  617. package/skills/plan/references/plan-founder-check.md +137 -0
  618. package/skills/plan/references/plan-functional-overlap.md +25 -0
  619. package/skills/plan/references/plan-host-fanout.md +51 -0
  620. package/skills/plan/references/plan-issue-templates.md +779 -0
  621. package/skills/plan/references/plan-network-outage-checklist.md +131 -0
  622. package/skills/plan/references/plan-scope-check.md +126 -0
  623. package/skills/plan/references/plan-sharp-edges.md +331 -0
  624. package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
  625. package/skills/plan/references/webapp-cwv-observability.md +143 -0
  626. package/skills/plan-review/SKILL.md +91 -0
  627. package/skills/plan-review/lib/named-panel.mjs +58 -0
  628. package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
  629. package/skills/postmerge/SKILL.md +642 -0
  630. package/skills/postmerge/references/deploy-status-debugging.md +141 -0
  631. package/skills/postmerge/references/graceful-degradation.md +20 -0
  632. package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
  633. package/skills/preflight/SKILL.md +1553 -0
  634. package/skills/preflight/references/check-13-founder-check.md +286 -0
  635. package/skills/preflight/scripts/founder-check.py +1640 -0
  636. package/skills/preflight/scripts/parse-form-a.awk +107 -0
  637. package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
  638. package/skills/product-roadmap/SKILL.md +357 -0
  639. package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
  640. package/skills/provision-cloudflare/SKILL.md +67 -0
  641. package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
  642. package/skills/provision-doppler/SKILL.md +58 -0
  643. package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
  644. package/skills/provision-github/SKILL.md +59 -0
  645. package/skills/provision-github/scripts/provision-github.sh +300 -0
  646. package/skills/provision-hetzner/SKILL.md +76 -0
  647. package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
  648. package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
  649. package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
  650. package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
  651. package/skills/qa/SKILL.md +263 -0
  652. package/skills/qa/references/browser-preflight.md +22 -0
  653. package/skills/questionnaire-generate/SKILL.md +328 -0
  654. package/skills/questionnaire-generate/references/questionnaire.template +60 -0
  655. package/skills/rclone/SKILL.md +162 -0
  656. package/skills/rclone/scripts/check_setup.sh +63 -0
  657. package/skills/release-announce/SKILL.md +65 -0
  658. package/skills/release-docs/SKILL.md +92 -0
  659. package/skills/reproduce-bug/SKILL.md +358 -0
  660. package/skills/resolve-debt/SKILL.md +85 -0
  661. package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
  662. package/skills/resolve-parallel/SKILL.md +45 -0
  663. package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
  664. package/skills/resolve-pr-parallel/SKILL.md +68 -0
  665. package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
  666. package/skills/resolve-todo-parallel/SKILL.md +57 -0
  667. package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
  668. package/skills/review/SKILL.md +1620 -0
  669. package/skills/review/references/anti-slop-scanner-hook.md +40 -0
  670. package/skills/review/references/defect-classes-migrations.md +9 -0
  671. package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
  672. package/skills/review/references/review-e2e-testing.md +116 -0
  673. package/skills/review/references/review-evidence-comment.md +36 -0
  674. package/skills/review/references/review-todo-structure.md +165 -0
  675. package/skills/review/references/risk-tier-and-fix-rounds.md +97 -0
  676. package/skills/review/references/semgrep-custom-rules.yaml +174 -0
  677. package/skills/review/references/ultrathink-phases.md +98 -0
  678. package/skills/review/references/wfs.md +6 -0
  679. package/skills/review/scripts/emit-review-trailer.sh +499 -0
  680. package/skills/review/scripts/ensure-semgrep.sh +99 -0
  681. package/skills/review/scripts/fix-round-seats.sh +197 -0
  682. package/skills/review/test/emit-review-trailer.test.sh +387 -0
  683. package/skills/review/test/ensure-semgrep.test.sh +111 -0
  684. package/skills/review/test/fix-round-seats.test.sh +237 -0
  685. package/skills/review/workflows/README.md +147 -0
  686. package/skills/review/workflows/review.workflow.js +667 -0
  687. package/skills/schedule/SKILL.md +796 -0
  688. package/skills/seo-aeo/SKILL.md +139 -0
  689. package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
  690. package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
  691. package/skills/ship/SKILL.md +3023 -0
  692. package/skills/ship/references/adr-ordinal-collision.md +47 -0
  693. package/skills/ship/references/affected-gate-contract.md +115 -0
  694. package/skills/ship/references/ci-run-diagnosis.md +29 -0
  695. package/skills/ship/references/ci-workflow-authoring.md +68 -0
  696. package/skills/ship/references/cleanup-merged-archival.md +39 -0
  697. package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
  698. package/skills/ship/references/cto-ruling-armed-push.md +5 -0
  699. package/skills/ship/references/decision-challenges-render.md +71 -0
  700. package/skills/ship/references/delegated-act-authoring-form.md +7 -0
  701. package/skills/ship/references/detached-battery-verdict.md +13 -0
  702. package/skills/ship/references/epic-close-sweep.md +62 -0
  703. package/skills/ship/references/feature-tweet-draft.md +42 -0
  704. package/skills/ship/references/followthrough-issue-body-template.md +47 -0
  705. package/skills/ship/references/followthrough-stub-template.sh +92 -0
  706. package/skills/ship/references/merge-queue-dequeue.md +39 -0
  707. package/skills/ship/references/operator-holds.md +179 -0
  708. package/skills/ship/references/pipeline-tally-render.md +43 -0
  709. package/skills/ship/references/pr-overlap.md +153 -0
  710. package/skills/ship/references/queue-mode.md +7 -0
  711. package/skills/ship/references/ready-run-wait.md +36 -0
  712. package/skills/ship/references/red-on-main-quarantine.md +110 -0
  713. package/skills/ship/references/register-update-pr-pattern.md +88 -0
  714. package/skills/ship/references/settle-then-admin-merge.md +130 -0
  715. package/skills/ship/references/supabase-column-probe.md +13 -0
  716. package/skills/ship/scripts/auto-close-scan.sh +76 -0
  717. package/skills/ship/scripts/battery-owed.sh +403 -0
  718. package/skills/ship/scripts/hold-gate-eval.sh +410 -0
  719. package/skills/ship/scripts/net-issue-flow.sh +665 -0
  720. package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
  721. package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
  722. package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
  723. package/skills/skill-creator/SKILL.md +299 -0
  724. package/skills/skill-creator/references/api-security.md +248 -0
  725. package/skills/skill-creator/references/authoring-levers.md +110 -0
  726. package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
  727. package/skills/skill-creator/references/best-practices.md +431 -0
  728. package/skills/skill-creator/references/common-patterns.md +639 -0
  729. package/skills/skill-creator/references/core-principles.md +193 -0
  730. package/skills/skill-creator/references/executable-code.md +188 -0
  731. package/skills/skill-creator/references/iteration-and-testing.md +509 -0
  732. package/skills/skill-creator/references/official-spec.md +193 -0
  733. package/skills/skill-creator/references/recommended-structure.md +180 -0
  734. package/skills/skill-creator/references/skill-structure.md +396 -0
  735. package/skills/skill-creator/references/using-scripts.md +122 -0
  736. package/skills/skill-creator/references/using-templates.md +120 -0
  737. package/skills/skill-creator/references/workflows-and-validation.md +541 -0
  738. package/skills/skill-creator/scripts/init_skill.py +322 -0
  739. package/skills/skill-creator/scripts/package_skill.py +110 -0
  740. package/skills/skill-creator/scripts/quick_validate.py +64 -0
  741. package/skills/skill-creator/templates/router-skill.md +73 -0
  742. package/skills/skill-creator/templates/simple-skill.md +34 -0
  743. package/skills/skill-creator/workflows/add-reference.md +96 -0
  744. package/skills/skill-creator/workflows/add-script.md +104 -0
  745. package/skills/skill-creator/workflows/add-template.md +83 -0
  746. package/skills/skill-creator/workflows/add-workflow.md +128 -0
  747. package/skills/skill-creator/workflows/audit-skill.md +159 -0
  748. package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
  749. package/skills/skill-creator/workflows/create-new-skill.md +207 -0
  750. package/skills/skill-creator/workflows/get-guidance.md +134 -0
  751. package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
  752. package/skills/skill-creator/workflows/verify-skill.md +226 -0
  753. package/skills/skill-security-scan/SKILL.md +145 -0
  754. package/skills/skill-security-scan/references/disclaimer.md +21 -0
  755. package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
  756. package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
  757. package/skills/skill-security-scan/references/override-mechanism.md +171 -0
  758. package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
  759. package/skills/skill-security-scan/references/regex-patterns.md +136 -0
  760. package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
  761. package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
  762. package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
  763. package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
  764. package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
  765. package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
  766. package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
  767. package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
  768. package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
  769. package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
  770. package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
  771. package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
  772. package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
  773. package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
  774. package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
  775. package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
  776. package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
  777. package/skills/skill-security-scan/scripts/lib.sh +197 -0
  778. package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
  779. package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
  780. package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
  781. package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
  782. package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
  783. package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
  784. package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
  785. package/skills/social-distribute/SKILL.md +485 -0
  786. package/skills/spec-templates/SKILL.md +274 -0
  787. package/skills/sync/SKILL.md +49 -0
  788. package/skills/test-browser/SKILL.md +464 -0
  789. package/skills/test-browser/references/feature-map.md +161 -0
  790. package/skills/test-fix-loop/SKILL.md +159 -0
  791. package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
  792. package/skills/triage/SKILL.md +402 -0
  793. package/skills/trigger-cron/SKILL.md +136 -0
  794. package/skills/trigger-cron/scripts/trigger.sh +198 -0
  795. package/skills/user-set-role/SKILL.md +147 -0
  796. package/skills/user-set-role/scripts/set-role.sh +218 -0
  797. package/skills/user-story-writer/SKILL.md +77 -0
  798. package/skills/ux-audit/SKILL.md +251 -0
  799. package/skills/ux-audit/references/finding.schema.json +62 -0
  800. package/skills/ux-audit/references/route-list.yaml +95 -0
  801. package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
  802. package/skills/ux-audit/scripts/bot-signin.ts +135 -0
  803. package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
  804. package/skills/verification-scaffold/SKILL.md +73 -0
  805. package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
  806. package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
  807. package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
  808. package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
  809. package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
  810. package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
  811. package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
  812. package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
  813. package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
  814. package/skills/verification-scaffold/references/guards.tsv +21 -0
  815. package/skills/verification-scaffold/references/verification-readme.template +55 -0
  816. package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
  817. package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
  818. package/skills/verification-scaffold/test/generator.test.sh +319 -0
  819. package/skills/verification-scaffold/test/manifest.test.sh +144 -0
  820. package/skills/work/SKILL.md +1480 -0
  821. package/skills/work/references/end-of-work-resume.md +7 -0
  822. package/skills/work/references/hook-test-main-cwd.md +24 -0
  823. package/skills/work/references/key-principles.md +83 -0
  824. package/skills/work/references/property-testing.md +122 -0
  825. package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
  826. package/skills/work/references/work-agent-teams.md +97 -0
  827. package/skills/work/references/work-lifecycle-parallel.md +147 -0
  828. package/skills/work/references/work-lockfile-bumps.md +97 -0
  829. package/skills/work/references/work-scratch-sandboxes.md +212 -0
  830. package/skills/work/references/work-subagent-fanout.md +61 -0
  831. package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
  832. package/skills/xcode-test/SKILL.md +360 -0
@@ -0,0 +1,1797 @@
1
+ #!/usr/bin/env bash
2
+ # affected-tests.test.sh — the contract suite for the plugin-shipped stack-detecting
3
+ # affected test gate (#9307, deliverable 2).
4
+ #
5
+ # SUT: plugins/soleur/scripts/affected-tests.sh — invoked as
6
+ # bash affected-tests.sh [--full] [--base <ref>] [--explain] [--self-check]
7
+ # from the ROOT of a synthesized fixture repository.
8
+ #
9
+ # PROPERTY. The gate exits 0 only when at least one selected test ran and passed (or the
10
+ # whole diff is inert and it printed NO_TESTS_AFFECTED); it refuses loudly (rc 4) on a
11
+ # vacuous/undecidable selection; it delegates --affected/--full ONLY to a repo runner that
12
+ # registers --print-selection at a case-label or usage-table position; it fails closed on
13
+ # malformed config; it emits `AFFECTED_GATE ran=X of=N skipped=Y mode=… reason=… stack=… ci=…`
14
+ # on EVERY run path. See the plan's Guard Contract for the mutation matrices this suite
15
+ # pins — each matrix row below is tagged [G<n>-<row>].
16
+ #
17
+ # ASSEMBLY. Every run happens inside a synthesized fixture repo (cq-test-fixtures-
18
+ # synthesized-only) under one mktemp root, behind PATH-shimmed fake runners
19
+ # (vitest/jest/pytest/go/nx/turbo/node/python3 + the package managers) that LOG every
20
+ # invocation to $CALLS and validate argv VALUES — an argv shape this suite does not
21
+ # prescribe is a stub miss (rc 64), never a silent success. Base refs come from minted
22
+ # refs/remotes/origin/* refs; the shallow-clone row uses a real file:// --depth 1 clone.
23
+ #
24
+ # INSTRUMENT FIDELITY (G1-11). Rows that claim "the runner ran" assert against the
25
+ # invocation ledger, never against exit codes alone — a fake that printed success
26
+ # without recording itself would turn those rows red. The `log_nonempty` check is
27
+ # itself self-tested against an empty ledger.
28
+ #
29
+ # cloud coverage: run_gate ALWAYS invokes the SUT with `env -u CLAUDE_PLUGIN_ROOT`, so
30
+ # every row exercises the no-plugin-root path (the gate resolves its libs by
31
+ # BASH_SOURCE; no in-gate resolver exists — ADR-179's three-arm diagnostic lives
32
+ # in commands/go.md, not here).
33
+ #
34
+ # Authoring note (work/SKILL.md): never `producer | grep -q` under pipefail — every rc is
35
+ # captured on its own line before it is inspected.
36
+
37
+ set -uo pipefail
38
+ export TMPDIR="${TMPDIR:-/var/tmp}"
39
+
40
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
41
+ REPO_ROOT="$(cd "$DIR/../../.." && pwd)"
42
+ # AFFECTED_TESTS_SUT exists so the fixture plumbing itself can be smoke-tested
43
+ # against a stand-in without touching the real gate's path.
44
+ SUT="${AFFECTED_TESTS_SUT:-$DIR/affected-tests.sh}"
45
+
46
+ # assert_fixture_dir / git_fixture_env / print_results live here; sourcing arms the
47
+ # git-location tripwire (#7833) — this suite must never run with an inherited GIT_DIR.
48
+ # shellcheck source=plugins/soleur/test/test-helpers.sh
49
+ source "$DIR/../test/test-helpers.sh" || { echo "FATAL: could not source test-helpers.sh" >&2; exit 2; }
50
+ set +e -uo pipefail
51
+
52
+ TESTROOT="$(mktemp -d -t affected-gate.XXXXXXXX)" || { echo "FATAL: mktemp failed" >&2; exit 2; }
53
+ assert_fixture_dir "$TESTROOT"
54
+ trap 'rm -rf "$TESTROOT"' EXIT INT TERM HUP
55
+
56
+ WORLD="$TESTROOT/world" # all fixture repos + remotes live under here
57
+ BIN="$TESTROOT/bin" # PATH shims
58
+ CALLS="$TESTROOT/calls.log" # the invocation ledger every row asserts on
59
+ RUNS="$TESTROOT/runs.tsv" # per-run record: kind\trc\toutfile
60
+ mkdir -p "$WORLD/repos" "$BIN" "$TESTROOT/outs"
61
+ : > "$CALLS"; : > "$RUNS"
62
+
63
+ # Constructed git env: synthesized identity, isolated config, discovery ceiling pinned
64
+ # one level above $WORLD (ceiling = $TESTROOT), so no fixture git ever reads the
65
+ # operator's credentials, global config, or an ancestor repository.
66
+ git_fixture_env "$WORLD" || { echo "FATAL: git_fixture_env refused $WORLD" >&2; exit 2; }
67
+
68
+ # Host tool paths captured BEFORE the shim dir goes on PATH: the node/python3 shims
69
+ # delegate unrecognized argv to these so the SUT's own internal use keeps working.
70
+ REAL_NODE="$(command -v node || true)"
71
+ REAL_PY="$(command -v python3 || true)"
72
+ REAL_GIT="$(command -v git || true)" # census-failure row: the git shim delegates here
73
+ BASEPATH="$PATH"
74
+
75
+ pass() { PASS=$((PASS + 1)); printf ' [ok] %s\n' "$1"; }
76
+ fail() { FAIL=$((FAIL + 1)); printf ' [FAIL] %s\n' "$1" >&2; }
77
+
78
+ # Instrument self-test: both verdict helpers must move their counters.
79
+ _sp=$PASS; _sf=$FAIL
80
+ pass "instrument self-test" >/dev/null 2>&1
81
+ fail "instrument self-test" >/dev/null 2>&1
82
+ if (( PASS != _sp + 1 )) || (( FAIL != _sf + 1 )); then
83
+ echo "[FATAL] instrument self-test failed" >&2; exit 1
84
+ fi
85
+ PASS=$_sp; FAIL=$_sf
86
+
87
+ [[ -f "$SUT" ]] || {
88
+ printf '\n[FATAL] SUT missing at %s — nothing to test (expected RED while the gate is unwritten).\n' "$SUT" >&2
89
+ exit 1
90
+ }
91
+
92
+ # ================================ fakes =======================================
93
+ # Every fake: logs `<basename>\t<argv>` to $FAKE_LOG, validates argv VALUES against
94
+ # FAKE_*_EXPECT / FAKE_*_MAXW env pins, exits 64 on an unrouted or mis-shaped call,
95
+ # and takes its verdict rc from FAKE_*_RC. Logging happens BEFORE validation so a row
96
+ # asserting "never called" cannot be laundered by a 64.
97
+
98
+ cat > "$BIN/vitest" <<'SHIM'
99
+ #!/usr/bin/env bash
100
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub vitest: FAKE_LOG unset" >&2; exit 64; }
101
+ printf 'vitest\t%s\n' "$*" >> "$FAKE_LOG"
102
+ for a in "$@"; do case "$a" in --version|-V|-v) echo "4.1.11"; exit 0 ;; --help) echo "vitest 4.1.11"; exit 0 ;; esac; done
103
+ [[ -z "${FAKE_VITEST_SLEEP:-}" ]] || sleep "$FAKE_VITEST_SLEEP"
104
+ mode=full
105
+ for a in "$@"; do [[ "$a" == "related" ]] && mode=related; done
106
+ # `vitest related <files> --run` is the #9307 contract — the subcommand form, and the
107
+ # --run flag is mandatory on the affected path (watch mode would hang the gate).
108
+ if [[ "$mode" == "related" ]]; then
109
+ case " $* " in
110
+ *" --run "*) : ;;
111
+ *) echo "stub vitest: 'related' without --run: $*" >&2; exit 64 ;;
112
+ esac
113
+ fi
114
+ maxw=""; prev=""
115
+ for a in "$@"; do
116
+ if [[ -n "$prev" ]]; then maxw="$a"; prev=""; continue; fi
117
+ case "$a" in
118
+ --maxWorkers|--max-workers|-w) prev="$a" ;;
119
+ --maxWorkers=*|--max-workers=*|-w=*) maxw="${a#*=}" ;;
120
+ esac
121
+ done
122
+ if [[ -n "${FAKE_VITEST_MAXW:-}" && "$maxw" != "$FAKE_VITEST_MAXW" ]]; then
123
+ echo "stub vitest: --maxWorkers=${maxw:-<absent>} want $FAKE_VITEST_MAXW" >&2; exit 64
124
+ fi
125
+ for want in ${FAKE_VITEST_EXPECT:-}; do
126
+ case " $* " in
127
+ *"$want"*) : ;;
128
+ *) echo "stub vitest: expected arg '$want' missing: $*" >&2; exit 64 ;;
129
+ esac
130
+ done
131
+ # The empty-selection answer is scoped to `related`: a selector that finds nothing
132
+ # is not the same verdict as a full run that found nothing.
133
+ if [[ "$mode" == "related" && "${FAKE_VITEST_NOTESTS:-0}" == "1" ]]; then
134
+ echo "No test files found"
135
+ exit "${FAKE_VITEST_RC:-0}"
136
+ fi
137
+ # Honest tally: the reported count must equal the fixture's own test-file
138
+ # count — a constant here would let ran= diverge from of= and launder the
139
+ # unit-integrity rows (D7/D16/D19a pin file units).
140
+ _n="$(find . -type f \( -name '*.test.*' -o -name '*.spec.*' \) \
141
+ -not -path './node_modules/*' 2>/dev/null | wc -l | tr -d ' ')"
142
+ [[ "$_n" =~ ^[0-9]+$ ]] && ((_n > 0)) || _n=1
143
+ echo "Test Files $_n passed ($_n)"
144
+ exit "${FAKE_VITEST_RC:-0}"
145
+ SHIM
146
+
147
+ cat > "$BIN/jest" <<'SHIM'
148
+ #!/usr/bin/env bash
149
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub jest: FAKE_LOG unset" >&2; exit 64; }
150
+ printf 'jest\t%s\n' "$*" >> "$FAKE_LOG"
151
+ for a in "$@"; do case "$a" in --version|-V|-v) echo "30.0.5"; exit 0 ;; --help) echo "jest 30.0.5"; exit 0 ;; esac; done
152
+ rel=0
153
+ for a in "$@"; do [[ "$a" == "--findRelatedTests" ]] && rel=1; done
154
+ if [[ "${FAKE_JEST_REQUIRE_RELATED:-0}" == "1" && "$rel" == "0" ]]; then
155
+ echo "stub jest: expected --findRelatedTests in: $*" >&2; exit 64
156
+ fi
157
+ for want in ${FAKE_JEST_EXPECT:-}; do
158
+ case " $* " in
159
+ *"$want"*) : ;;
160
+ *) echo "stub jest: expected arg '$want' missing: $*" >&2; exit 64 ;;
161
+ esac
162
+ done
163
+ # Same honest-tally rule as the vitest stub — the __tests__ convention carries
164
+ # no .test./.spec. marker, so count the directory shape too (mirrors the
165
+ # adapter's census).
166
+ _n="$(find . -type f \( -name '*.test.*' -o -name '*.spec.*' \
167
+ -o -path '*/__tests__/*' \) -not -path './node_modules/*' 2>/dev/null \
168
+ | wc -l | tr -d ' ')"
169
+ [[ "$_n" =~ ^[0-9]+$ ]] && ((_n > 0)) || _n=1
170
+ echo "Test Suites: $_n passed, $_n total"
171
+ exit "${FAKE_JEST_RC:-0}"
172
+ SHIM
173
+
174
+ cat > "$BIN/pytest" <<'SHIM'
175
+ #!/usr/bin/env bash
176
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub pytest: FAKE_LOG unset" >&2; exit 64; }
177
+ printf 'pytest\t%s\n' "$*" >> "$FAKE_LOG"
178
+ for a in "$@"; do
179
+ case "$a" in --version|-V|--help) echo "pytest 8.3.0"; exit 0 ;; esac
180
+ done
181
+ for want in ${FAKE_PYTEST_EXPECT:-}; do
182
+ case " $* " in
183
+ *"$want"*) : ;;
184
+ *) echo "stub pytest: expected arg '$want' missing: $*" >&2; exit 64 ;;
185
+ esac
186
+ done
187
+ rc="${FAKE_PYTEST_RC:-0}"
188
+ if [[ "$rc" == "5" ]]; then echo "no tests collected"; exit 5; fi
189
+ # pytest's own tally line: `collected N items` (testmon adds `/ M deselected`)
190
+ # — the gate pins ran to FILE units, so this line is realism, not the counter.
191
+ _nitems="${FAKE_PYTEST_COLLECTED:-1}"
192
+ if [[ -n "${FAKE_PYTEST_DESELECTED:-}" ]]; then
193
+ echo "collected $_nitems items / $FAKE_PYTEST_DESELECTED deselected"
194
+ else
195
+ echo "collected $_nitems items"
196
+ fi
197
+ echo "=== $_nitems passed in 0.01s ==="
198
+ exit "$rc"
199
+ SHIM
200
+
201
+ cat > "$BIN/go" <<'SHIM'
202
+ #!/usr/bin/env bash
203
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub go: FAKE_LOG unset" >&2; exit 64; }
204
+ printf 'go\t%s\n' "$*" >> "$FAKE_LOG"
205
+ cmd="${1:-}"
206
+ case "$cmd" in
207
+ version) echo "go version go1.23.4 linux/amd64"; exit 0 ;;
208
+ env)
209
+ for a in "$@"; do
210
+ case "$a" in GOMOD) printf '%s\n' "${FAKE_GO_GOMOD:-$PWD/go.mod}" ;; esac
211
+ done
212
+ exit 0 ;;
213
+ list)
214
+ # FAKE_GO_DIR/graph rows are pipe-form: `ImportPath|deps deps|TestImports|XTestImports`
215
+ # — the same shape the real `go list -e -f` template emits.
216
+ graph="${FAKE_GO_DIR:-}/graph"
217
+ [[ -f "$graph" ]] || { echo "stub go: list with no FAKE_GO_DIR/graph" >&2; exit 64; }
218
+ has_f=0; has_deps=0; targets=""
219
+ prev=""
220
+ for a in "$@"; do
221
+ if [[ "$prev" == "-f" ]]; then has_f=1; prev=""; continue; fi
222
+ case "$a" in
223
+ list) : ;;
224
+ -f) prev="-f" ;;
225
+ -f*) has_f=1 ;;
226
+ -deps) has_deps=1 ;;
227
+ -*) : ;;
228
+ *) targets="$targets $a" ;;
229
+ esac
230
+ done
231
+ if (( has_f )); then
232
+ if [[ "$targets" == *"/..."* || -z "${targets// /}" ]]; then
233
+ while IFS= read -r line; do printf '%s\n' "$line"; done < "$graph"
234
+ else
235
+ # `go list -e -f '{{.ImportPath}}' <dir>` — a dir target (./x) resolves
236
+ # to the graph row whose import path ENDS in it; `.` is the module root
237
+ # (the shortest import path).
238
+ for t in $targets; do
239
+ td="${t#./}"
240
+ if [[ "$td" == "." ]]; then
241
+ awk -F'|' '{n=gsub(/\//,"/",$1); if(NR==1||n<min){min=n;best=$1}} END{if(best!="")print best; else exit 1}' "$graph" \
242
+ || { echo "stub go: unknown dir $t" >&2; exit 64; }
243
+ else
244
+ awk -F'|' -v t="$td" 'length($1)>=length(t) && substr($1,length($1)-length(t)+1)==t{print $1; f=1} END{if(!f) exit 1}' "$graph" \
245
+ || { echo "stub go: unknown dir $t" >&2; exit 64; }
246
+ fi
247
+ done
248
+ fi
249
+ exit 0
250
+ fi
251
+ if [[ "$targets" == *"/..."* || -z "${targets// /}" ]]; then
252
+ awk -F'|' '{print $1}' "$graph"
253
+ exit 0
254
+ fi
255
+ # named package(s): `go list -deps <pkg>` prints one import path per line
256
+ # (target plus the deps the graph row declares); plain `go list <pkg>` prints it.
257
+ for t in $targets; do
258
+ [[ "$t" == "list" ]] && continue
259
+ if (( has_deps )); then
260
+ awk -F'|' -v t="$t" '$1==t{print $1; n=split($2,d," "); for(i=1;i<=n;i++) print d[i]; f=1} END{if(!f) exit 1}' "$graph" \
261
+ || { echo "stub go: unknown package $t" >&2; exit 64; }
262
+ else
263
+ awk -F'|' -v t="$t" '$1==t{print $1; f=1} END{if(!f) exit 1}' "$graph" \
264
+ || { echo "stub go: unknown package $t" >&2; exit 64; }
265
+ fi
266
+ done
267
+ exit 0 ;;
268
+ test)
269
+ # `go test -p <n> [pkg …]` — emit the real `ok`/`?` per-package lines so
270
+ # the adapter can count *_test.go files under each reported package.
271
+ has_p=0; tgts=""; prev=""
272
+ for a in "$@"; do
273
+ if [[ -n "$prev" ]]; then prev=""; continue; fi
274
+ case "$a" in
275
+ -p|--parallel) prev="$a"; has_p=1 ;;
276
+ -p=*|--parallel=*) has_p=1 ;;
277
+ test) : ;;
278
+ -*) : ;;
279
+ *) tgts="$tgts $a" ;;
280
+ esac
281
+ done
282
+ if [[ "${FAKE_GO_REQUIRE_P:-1}" == "1" && "$has_p" == "0" ]]; then
283
+ echo "stub go: 'go test' without a -p worker cap: $*" >&2; exit 64
284
+ fi
285
+ for want in ${FAKE_GO_EXPECT:-}; do
286
+ case " $* " in
287
+ *"$want"*) : ;;
288
+ *) echo "stub go: expected package '$want' not under test: $*" >&2; exit 64 ;;
289
+ esac
290
+ done
291
+ [[ -n "${tgts// /}" ]] || tgts=" ./..."
292
+ for t in $tgts; do
293
+ if [[ "$t" == "./..." ]]; then
294
+ awk '{print "ok\t"$1"\t0.01s"}' "${FAKE_GO_DIR:-}/graph" 2>/dev/null || true
295
+ else
296
+ printf 'ok\t%s\t0.01s\n' "$t"
297
+ fi
298
+ done
299
+ exit "${FAKE_GO_TEST_RC:-0}" ;;
300
+ *) echo "stub go: unrouted call: $*" >&2; exit 64 ;;
301
+ esac
302
+ SHIM
303
+
304
+ cat > "$BIN/nx" <<'SHIM'
305
+ #!/usr/bin/env bash
306
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub nx: FAKE_LOG unset" >&2; exit 64; }
307
+ printf 'nx\t%s\n' "$*" >> "$FAKE_LOG"
308
+ for a in "$@"; do case "$a" in --version|-V|-v) echo "22.4.0"; exit 0 ;; esac; done
309
+ case " $* " in
310
+ *" affected "*|*" affected"*)
311
+ for want in ${FAKE_NX_EXPECT:-}; do
312
+ case " $* " in
313
+ *"$want"*) : ;;
314
+ *) echo "stub nx: expected arg '$want' missing: $*" >&2; exit 64 ;;
315
+ esac
316
+ done
317
+ # FAKE_NX_LINE controls the summary line — e.g. "… for 10 tasks" must NOT
318
+ # read as a vacuous "0 tasks" verdict.
319
+ echo "${FAKE_NX_LINE:-nx affected: ran target test}"
320
+ exit "${FAKE_NX_RC:-0}" ;;
321
+ *" run-many "*|*" run-many"*)
322
+ echo "${FAKE_NX_FULL_LINE:-nx run-many: ran target test for 3 projects}"
323
+ exit "${FAKE_NX_FULL_RC:-0}" ;;
324
+ *" show "*)
325
+ for p in ${FAKE_NX_PROJECTS:-webapp}; do printf '%s\n' "$p"; done
326
+ exit 0 ;;
327
+ *) echo "stub nx: unrouted call: $*" >&2; exit 64 ;;
328
+ esac
329
+ SHIM
330
+
331
+ cat > "$BIN/turbo" <<'SHIM'
332
+ #!/usr/bin/env bash
333
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub turbo: FAKE_LOG unset" >&2; exit 64; }
334
+ printf 'turbo\t%s\n' "$*" >> "$FAKE_LOG"
335
+ for a in "$@"; do case "$a" in --version|-V|-v) echo "2.5.6"; exit 0 ;; esac; done
336
+ has_run=0; has_test=0
337
+ for a in "$@"; do
338
+ [[ "$a" == "run" ]] && has_run=1
339
+ [[ "$a" == "test" || "$a" == test* ]] && has_test=1
340
+ done
341
+ if [[ "$has_run" == "1" && "$has_test" == "1" ]]; then
342
+ for want in ${FAKE_TURBO_EXPECT:-}; do
343
+ case " $* " in
344
+ *"$want"*) : ;;
345
+ *) echo "stub turbo: expected arg '$want' missing: $*" >&2; exit 64 ;;
346
+ esac
347
+ done
348
+ echo "turbo: tasks complete"
349
+ exit "${FAKE_TURBO_RC:-0}"
350
+ fi
351
+ echo "stub turbo: unrouted call: $*" >&2
352
+ exit 64
353
+ SHIM
354
+
355
+ cat > "$BIN/node" <<'SHIM'
356
+ #!/usr/bin/env bash
357
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub node: FAKE_LOG unset" >&2; exit 64; }
358
+ printf 'node\t%s\n' "$*" >> "$FAKE_LOG"
359
+ case "${1:-}" in --version|-v) echo "v22.11.0"; exit 0 ;; esac
360
+ # `node <path>/vitest.mjs <args>` / `node <path>/jest.js <args>` — the first arg
361
+ # whose basename names a tool becomes the tool call; the tool shim logs it.
362
+ args=("$@"); i=0
363
+ while (( i < ${#args[@]} )); do
364
+ b="${args[i]##*/}"
365
+ case "$b" in
366
+ vitest*|vitest.mjs) exec vitest ${args[@]+"${args[@]:$((i+1))}"} ;;
367
+ jest*|jest.js|jest-cli*) exec jest ${args[@]+"${args[@]:$((i+1))}"} ;;
368
+ esac
369
+ i=$((i+1))
370
+ done
371
+ [[ -n "${REAL_NODE:-}" ]] && exec "$REAL_NODE" "$@"
372
+ echo "stub node: unrouted call: $*" >&2
373
+ exit 64
374
+ SHIM
375
+
376
+ cat > "$BIN/python3" <<'SHIM'
377
+ #!/usr/bin/env bash
378
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub python3: FAKE_LOG unset" >&2; exit 64; }
379
+ printf 'python3\t%s\n' "$*" >> "$FAKE_LOG"
380
+ # The gate probes with isolated mode (`python3 -I -c …`): consume leading
381
+ # single-letter interpreter flags before routing on the payload.
382
+ while [[ "${1:-}" == -[a-zA-Z] && "${1:-}" != "-c" && "${1:-}" != "-m" && "${1:-}" != "-V" ]]; do shift; done
383
+ # `python3 -m pytest <args>` runs the pytest shim.
384
+ if [[ "${1:-}" == "-m" && "${2:-}" == "pytest" ]]; then
385
+ shift 2; exec pytest "$@"
386
+ fi
387
+ # Importability probes: `python3 -c 'import xdist'` (and find_spec spellings whose
388
+ # -c text names the module). Answers come from FAKE_PY_IMPORTABLE (space-separated
389
+ # module names that import cleanly); anything else delegates to the real python3 so
390
+ # the SUT's own config parsing keeps working.
391
+ if [[ "${1:-}" == "-c" && -n "${2:-}" ]]; then
392
+ script="$2"
393
+ mod=""
394
+ if [[ "$script" =~ (^|[^A-Za-z_])(pytest_testmon|testmon)([^A-Za-z_]|$) ]]; then mod="testmon";
395
+ elif [[ "$script" =~ (^|[^A-Za-z_])xdist([^A-Za-z_]|$) ]]; then mod="xdist";
396
+ elif [[ "$script" =~ (^|[^A-Za-z_])pytest([^A-Za-z_]|$) ]]; then mod="pytest"; fi
397
+ if [[ -n "$mod" ]]; then
398
+ case " ${FAKE_PY_IMPORTABLE:-} " in
399
+ *" $mod "*) exit 0 ;;
400
+ *) exit 1 ;;
401
+ esac
402
+ fi
403
+ fi
404
+ case "${1:-}" in --version|-V) echo "Python 3.15.0"; exit 0 ;; esac
405
+ [[ -n "${REAL_PY:-}" ]] && exec "$REAL_PY" "$@"
406
+ echo "stub python3: unrouted call: $*" >&2
407
+ exit 64
408
+ SHIM
409
+
410
+ # The package-manager shims share one body: they log, then either exec through to a
411
+ # shimmed tool (`npx vitest related …` → the vitest shim re-logs and validates) or
412
+ # answer a `test`/`run test` invocation from FAKE_PM_TEST_RC.
413
+ PM_BODY='#!/usr/bin/env bash
414
+ name="$(basename "$0")"
415
+ [[ -n "${FAKE_LOG:-}" ]] || { echo "stub $name: FAKE_LOG unset" >&2; exit 64; }
416
+ printf "%s\t%s\n" "$name" "$*" >> "$FAKE_LOG"
417
+ args=("$@"); i=0
418
+ while (( i < ${#args[@]} )); do
419
+ a="${args[i]}"
420
+ case "$a" in
421
+ -y|--yes|--|x|exec|dlx|-dlx) i=$((i+1)); continue ;;
422
+ vitest@*|jest@*|nx@*|turbo@*) exec "${a%%@*}" ${args[@]+"${args[@]:$((i+1))}"} ;;
423
+ vitest|jest|nx|turbo|pytest|go) exec "$a" ${args[@]+"${args[@]:$((i+1))}"} ;;
424
+ run)
425
+ nxt="${args[$((i+1))]:-}"
426
+ [[ "$nxt" == "test" ]] && exit "${FAKE_PM_TEST_RC:-0}"
427
+ i=$((i+2)); continue ;;
428
+ test) exit "${FAKE_PM_TEST_RC:-0}" ;;
429
+ install|i|add|remove|ci|create|init|info|view|ls|list|audit|outdated|pack|publish|version|whoami|pkg)
430
+ exit 0 ;;
431
+ *) i=$((i+1)) ;;
432
+ esac
433
+ done
434
+ echo "stub $name: unrouted call: $*" >&2
435
+ exit 64
436
+ '
437
+ for _pm in npm npx pnpm yarn bun bunx; do
438
+ printf '%s\n' "$PM_BODY" > "$BIN/$_pm"
439
+ done
440
+ chmod +x "$BIN"/*
441
+ unset _pm
442
+
443
+ # ============================ fixture builders ================================
444
+
445
+ # mk_repo <name> [default-branch] — repo at $WORLD/repos/<name> with a `feature`
446
+ # branch checked out. Base-ref resolution gets its refs WITHOUT a live remote:
447
+ # refs/remotes/origin/<br> + refs/remotes/origin/HEAD are minted directly, so
448
+ # `@{upstream}` fails (feature has none) and the origin/HEAD → origin/<br> arm wins.
449
+ mk_repo() { # prints the repo path
450
+ local name="$1" br="${2:-main}" r
451
+ r="$WORLD/repos/$name"
452
+ mkdir -p "$r"
453
+ printf 'fixture seed\n' > "$r/SEED.txt"
454
+ git -C "$r" init -q -b "$br"
455
+ git -C "$r" config user.email soleur-test@example.invalid
456
+ git -C "$r" config user.name "Soleur Fixture"
457
+ git -C "$r" config commit.gpgsign false
458
+ printf '%s' "$r"
459
+ }
460
+
461
+ # seal_base <repo> — commit the seeded base, mint origin refs, move to `feature`.
462
+ seal_base() {
463
+ local r="$1" br
464
+ assert_fixture_dir "$r"
465
+ # symbolic-ref, not rev-parse: HEAD is UNBORN until the commit below lands, and
466
+ # rev-parse --abbrev-ref errors out there while symbolic-ref still reads the
467
+ # branch HEAD points at.
468
+ br="$(git -C "$r" symbolic-ref --short -q HEAD)"
469
+ git -C "$r" add -A
470
+ git -C "$r" commit -q -m "base"
471
+ git -C "$r" update-ref "refs/remotes/origin/$br" HEAD
472
+ git -C "$r" symbolic-ref refs/remotes/origin/HEAD "refs/remotes/origin/$br"
473
+ git -C "$r" checkout -q -b feature
474
+ }
475
+
476
+ chg() { local r="$1" f="$2"; mkdir -p "$r/$(dirname "$f")"; printf '%b\n' "${3:-change}" > "$r/$f"; }
477
+ commit_chg() { assert_fixture_dir "$1"; chg "$1" "$2" "${3:-change}"; git -C "$1" add -A; git -C "$1" commit -q -m "change: $2"; }
478
+
479
+ seed_js() { # <repo> — vitest stack: package.json, src, a test file, node_modules stub
480
+ local r="$1"
481
+ mkdir -p "$r/src" "$r/node_modules/.bin"
482
+ cat > "$r/package.json" <<'JSON'
483
+ {
484
+ "name": "fixture-js",
485
+ "private": true,
486
+ "scripts": { "test": "vitest run" },
487
+ "devDependencies": { "vitest": "^4.0.0" }
488
+ }
489
+ JSON
490
+ printf 'node_modules/\n' > "$r/.gitignore"
491
+ printf 'export const a = 1;\n' > "$r/src/a.ts"
492
+ printf 'import { test } from "vitest";\ntest("a", () => {});\n' > "$r/src/a.test.ts"
493
+ ln -sf "$BIN/vitest" "$r/node_modules/.bin/vitest"
494
+ }
495
+
496
+ seed_js_nobin() { # <repo> — vitest stack but NO repo-local binary: used by the
497
+ # toolchain-missing row, where a .bin symlink would resolve the
498
+ # shim even on a stripped PATH.
499
+ local r="$1"
500
+ mkdir -p "$r/src"
501
+ cat > "$r/package.json" <<'JSON'
502
+ {
503
+ "name": "fixture-js",
504
+ "private": true,
505
+ "scripts": { "test": "vitest run" },
506
+ "devDependencies": { "vitest": "^4.0.0" }
507
+ }
508
+ JSON
509
+ printf 'node_modules/\n' > "$r/.gitignore"
510
+ printf 'export const a = 1;\n' > "$r/src/a.ts"
511
+ printf 'import { test } from "vitest";\ntest("a", () => {});\n' > "$r/src/a.test.ts"
512
+ }
513
+
514
+ seed_jest() { # <repo> — jest stack (jest.config.js present, no vitest config)
515
+ local r="$1"
516
+ mkdir -p "$r/src" "$r/node_modules/.bin"
517
+ cat > "$r/package.json" <<'JSON'
518
+ {
519
+ "name": "fixture-jest",
520
+ "private": true,
521
+ "scripts": { "test": "jest" },
522
+ "devDependencies": { "jest": "^30.0.0" }
523
+ }
524
+ JSON
525
+ printf 'node_modules/\n' > "$r/.gitignore"
526
+ printf 'module.exports = {};\n' > "$r/jest.config.js"
527
+ printf 'export const b = 1;\n' > "$r/src/b.ts"
528
+ printf 'test("b", () => {});\n' > "$r/src/b.test.ts"
529
+ ln -sf "$BIN/jest" "$r/node_modules/.bin/jest"
530
+ }
531
+
532
+ seed_python() { # <repo> — pytest stack
533
+ local r="$1"
534
+ mkdir -p "$r/src" "$r/tests"
535
+ cat > "$r/pyproject.toml" <<'TOML'
536
+ [project]
537
+ name = "fixture-py"
538
+ [tool.pytest.ini_options]
539
+ testpaths = ["tests"]
540
+ TOML
541
+ printf 'def f():\n return 1\n' > "$r/src/mod.py"
542
+ printf 'def test_a():\n assert True\n' > "$r/tests/test_a.py"
543
+ }
544
+
545
+ seed_go() { # <repo> <graph-dir> — go.mod + packages; writes <graph-dir>/graph as
546
+ # "pkg dep dep" lines the go shim answers `go list` from
547
+ local r="$1" g="$2"
548
+ mkdir -p "$r/internal/leaf" "$r/cmd/app" "$g"
549
+ printf 'module example.com/m\n\ngo 1.23\n' > "$r/go.mod"
550
+ printf 'package leaf\n\nfunc Leaf() int { return 1 }\n' > "$r/internal/leaf/leaf.go"
551
+ printf 'package leaf\n\nimport "testing"\n\nfunc TestLeaf(t *testing.T) {}\n' > "$r/internal/leaf/leaf_test.go"
552
+ printf 'package main\n\nimport _ "example.com/m/internal/leaf"\n\nfunc main() {}\n' > "$r/cmd/app/main.go"
553
+ # Pipe-form rows: ImportPath|Imports|TestImports|XTestImports — the shape
554
+ # `go list -e -f '{{.ImportPath}}|{{join .Imports " "}}|…'` emits.
555
+ cat > "$g/graph" <<'GRAPH'
556
+ example.com/m/cmd/app|example.com/m/internal/leaf fmt||
557
+ example.com/m/internal/leaf|||
558
+ GRAPH
559
+ }
560
+
561
+ seed_nx() { local r="$1"; seed_js "$r"; printf '{"npmScope":"fx","affected":{}}\n' > "$r/nx.json"; }
562
+ seed_turbo(){ local r="$1"; seed_js "$r"; printf '{"pipeline":{"test":{}}}\n' > "$r/turbo.json"; }
563
+
564
+ seed_ci() { # <repo> — a CI test workflow (must be committed in BASE: the workflows
565
+ # dir is itself a built-in global_path)
566
+ local r="$1"
567
+ mkdir -p "$r/.github/workflows"
568
+ cat > "$r/.github/workflows/ci.yml" <<'YML'
569
+ name: ci
570
+ on: pull_request
571
+ jobs:
572
+ test:
573
+ runs-on: ubuntu-latest
574
+ steps:
575
+ - run: npm test
576
+ YML
577
+ }
578
+
579
+ write_config() { # <repo> <json>
580
+ printf '%s\n' "$2" > "$1/.soleur-test-gate.json"
581
+ }
582
+
583
+ # The capability-positive runner (G2): registers --print-selection at BOTH the
584
+ # case-label position and the usage-table position, as scripts/test-all.sh does.
585
+ write_runner_capable() { # <repo>
586
+ local r="$1"
587
+ mkdir -p "$r/scripts"
588
+ cat > "$r/scripts/test-all.sh" <<'RUNNER'
589
+ #!/usr/bin/env bash
590
+ printf 'test-all\t%s\n' "$*" >> "${FAKE_LOG:?}"
591
+ usage() {
592
+ cat <<'USAGE'
593
+ Usage: bash scripts/test-all.sh [--affected] [--full]
594
+ --affected run the suites this diff selects
595
+ --full run the entire battery
596
+ --print-selection print the suites this diff selects; runs nothing
597
+ USAGE
598
+ }
599
+ for a in "$@"; do
600
+ case "$a" in
601
+ --print-selection) printf 'AFFECTED_SELECTED\tsuite\taffected\n'; exit 0 ;;
602
+ --affected) exit "${FAKE_RUNNER_AFFECTED_RC:-${FAKE_RUNNER_RC:-0}}" ;;
603
+ --full) exit "${FAKE_RUNNER_FULL_RC:-${FAKE_RUNNER_RC:-0}}" ;;
604
+ --capacity) printf 'measured_runs=0\n'; exit 0 ;;
605
+ --help|-h) usage; exit 0 ;;
606
+ *) echo "runner stub: unexpected argv: $a" >&2; exit 64 ;;
607
+ esac
608
+ done
609
+ exit "${FAKE_RUNNER_RC:-0}"
610
+ RUNNER
611
+ chmod +x "$r/scripts/test-all.sh"
612
+ }
613
+
614
+ # Usage-table-only registration (G2-5 must-PASS): --print-selection appears ONLY as
615
+ # a usage-table row; the dispatch loop matches it through a conditional, not a
616
+ # case label. A probe that insists on the `)` case-label spelling misses this.
617
+ write_runner_usage_table() { # <repo>
618
+ local r="$1"
619
+ mkdir -p "$r/scripts"
620
+ cat > "$r/scripts/test-all.sh" <<'RUNNER'
621
+ #!/usr/bin/env bash
622
+ printf 'test-all\t%s\n' "$*" >> "${FAKE_LOG:?}"
623
+ usage() {
624
+ cat <<'USAGE'
625
+ Usage: bash scripts/test-all.sh [--affected] [--full]
626
+ --affected run the suites this diff selects
627
+ --full run the entire battery
628
+ --print-selection print the suites this diff selects; runs nothing
629
+ USAGE
630
+ }
631
+ for a in "$@"; do
632
+ if [[ "$a" == "--print-selection" ]]; then
633
+ printf 'AFFECTED_SELECTED\tsuite\taffected\n'; exit 0
634
+ elif [[ "$a" == "--affected" ]]; then
635
+ exit "${FAKE_RUNNER_AFFECTED_RC:-${FAKE_RUNNER_RC:-0}}"
636
+ elif [[ "$a" == "--full" ]]; then
637
+ exit "${FAKE_RUNNER_FULL_RC:-${FAKE_RUNNER_RC:-0}}"
638
+ elif [[ "$a" == "--help" || "$a" == "-h" ]]; then
639
+ usage; exit 0
640
+ else
641
+ echo "runner stub: unexpected argv: $a" >&2; exit 64
642
+ fi
643
+ done
644
+ exit "${FAKE_RUNNER_RC:-0}"
645
+ RUNNER
646
+ chmod +x "$r/scripts/test-all.sh"
647
+ }
648
+
649
+ # A foreign scripts/test-all.sh (G2-1): a same-named script that belongs to the
650
+ # user's repo, advertises nothing, and must NEVER receive Soleur flags.
651
+ write_runner_foreign() { # <repo>
652
+ local r="$1"
653
+ mkdir -p "$r/scripts"
654
+ cat > "$r/scripts/test-all.sh" <<'RUNNER'
655
+ #!/usr/bin/env bash
656
+ printf 'foreign-runner\t%s\n' "$*" >> "${FAKE_LOG:-/dev/null}"
657
+ echo "ok"
658
+ exit 0
659
+ RUNNER
660
+ chmod +x "$r/scripts/test-all.sh"
661
+ }
662
+
663
+ # Foreign + comment-only mention (G2-4): the flag text appears in a comment —
664
+ # never at a case-label or usage-table position.
665
+ write_runner_comment_mention() { # <repo>
666
+ local r="$1"
667
+ mkdir -p "$r/scripts"
668
+ cat > "$r/scripts/test-all.sh" <<'RUNNER'
669
+ #!/usr/bin/env bash
670
+ # NOTE: --print-selection support is planned for a future release of this
671
+ # runner; until then it knows only how to run everything.
672
+ printf 'foreign-runner\t%s\n' "$*" >> "${FAKE_LOG:-/dev/null}"
673
+ echo "ok"
674
+ exit 0
675
+ RUNNER
676
+ chmod +x "$r/scripts/test-all.sh"
677
+ }
678
+
679
+ # ============================ run + assert plumbing ===========================
680
+
681
+ reset_fakes() {
682
+ : > "$CALLS"
683
+ export FAKE_VITEST_RC=0 FAKE_VITEST_EXPECT="" FAKE_VITEST_MAXW="" FAKE_VITEST_NOTESTS=0 FAKE_VITEST_SLEEP=""
684
+ export FAKE_JEST_RC=0 FAKE_JEST_EXPECT="" FAKE_JEST_REQUIRE_RELATED=0
685
+ export FAKE_PYTEST_RC=0 FAKE_PYTEST_EXPECT=""
686
+ export FAKE_PY_IMPORTABLE="pytest"
687
+ export FAKE_GO_TEST_RC=0 FAKE_GO_EXPECT="" FAKE_GO_DIR="" FAKE_GO_GOMOD="" FAKE_GO_REQUIRE_P=1
688
+ export FAKE_NX_RC=0 FAKE_NX_EXPECT="" FAKE_NX_PROJECTS="" FAKE_NX_LINE="" FAKE_NX_FULL_LINE="" FAKE_NX_FULL_RC=0
689
+ export FAKE_PYTEST_COLLECTED="" FAKE_PYTEST_DESELECTED=""
690
+ export FAKE_TURBO_RC=0 FAKE_TURBO_EXPECT=""
691
+ export FAKE_RUNNER_RC=0 FAKE_RUNNER_AFFECTED_RC="" FAKE_RUNNER_FULL_RC=""
692
+ export FAKE_PM_TEST_RC=0
693
+ export FAKE_GIT_FAIL_OTHERS=0
694
+ }
695
+
696
+ RSEQ=0; RC=0; OUT=""
697
+ _run_gate() { # <PATH> <repo> <kind:gate|explain|selfcheck> [gate args…]
698
+ local p="$1" repo="$2" kind="$3"; shift 3
699
+ RSEQ=$((RSEQ + 1))
700
+ ( cd "$repo" && env -u CLAUDE_PLUGIN_ROOT \
701
+ PATH="$p" \
702
+ FAKE_LOG="$CALLS" FAKE_DIR="$TESTROOT" \
703
+ REAL_NODE="$REAL_NODE" REAL_PY="$REAL_PY" REAL_GIT="$REAL_GIT" \
704
+ bash "$SUT" "$@" ) > "$TESTROOT/outs/$RSEQ.out" 2>&1
705
+ RC=$?
706
+ OUT="$(cat "$TESTROOT/outs/$RSEQ.out")"
707
+ printf '%s\t%s\t%s\n' "$kind" "$RC" "$TESTROOT/outs/$RSEQ.out" >> "$RUNS"
708
+ }
709
+ run_gate() { _run_gate "$BIN:$BASEPATH" "$@"; }
710
+ run_gate_path() { local p="$1"; shift; _run_gate "$p" "$@"; }
711
+
712
+ # --- verdict helpers ----------------------------------------------------------
713
+ want_rc() {
714
+ if [[ "$RC" == "$1" ]]; then pass "$2"
715
+ else fail "$2 — rc=$RC want=$1 :: $(tail -n 3 "$TESTROOT/outs/$RSEQ.out" 2>/dev/null | tr '\n' '|')"; fi
716
+ }
717
+ want_rc_in() { # <label> <rc> <rc>…
718
+ local label="$1"; shift
719
+ local want; for want in "$@"; do [[ "$RC" == "$want" ]] && { pass "$label"; return; }; done
720
+ fail "$label — rc=$RC want one of {$*} :: $(tail -n 3 "$TESTROOT/outs/$RSEQ.out" 2>/dev/null | tr '\n' '|')"
721
+ }
722
+ want_out() { # <label> <ere>
723
+ if grep -qE "$2" <<<"$OUT"; then pass "$1"
724
+ else fail "$1 — output lacks /$2/ :: $(tail -n 5 "$TESTROOT/outs/$RSEQ.out" 2>/dev/null | tr '\n' '|')"; fi
725
+ }
726
+ want_no_out() { # <label> <ere>
727
+ if grep -qE "$2" <<<"$OUT"; then fail "$1 — output unexpectedly matches /$2/"
728
+ else pass "$1"; fi
729
+ }
730
+ want_call() { # <label> <ere> — the invocation ledger must show it
731
+ if grep -qE "$2" "$CALLS"; then pass "$1"
732
+ else fail "$1 — call log lacks /$2/ :: $(tr '\n' '|' < "$CALLS")"; fi
733
+ }
734
+ want_no_call() { # <label> <ere>
735
+ if grep -qE "$2" "$CALLS"; then fail "$1 — call log unexpectedly matches /$2/ :: $(tr '\n' '|' < "$CALLS")"
736
+ else pass "$1"; fi
737
+ }
738
+ log_nonempty() { [[ -s "${1:-$CALLS}" ]]; }
739
+
740
+ # AFFECTED_GATE marker field extractor: the LAST `AFFECTED_GATE ` line (the REFUSED
741
+ # and BUDGET_DEFERRED markers carry a `_` after GATE and never match the space).
742
+ gate_field() { # <field> -> value
743
+ sed -n 's/^.*AFFECTED_GATE \(.*\)$/\1/p' <<<"$OUT" | tail -n 1 \
744
+ | tr ' ' '\n' | sed -n "s/^$1=//p" | head -n 1
745
+ }
746
+ want_field() { # <label> <field> <expected-value-or-ere>
747
+ local got; got="$(gate_field "$2")"
748
+ if [[ "$got" =~ ^$3$ ]]; then pass "$1"
749
+ else fail "$1 — marker field $2='$got' want /^$3$/ :: $(grep -oE 'AFFECTED_GATE .*' <<<"$OUT" | tail -n 1)"; fi
750
+ }
751
+
752
+ # ================================ ROWS =======================================
753
+
754
+ printf '\n=== affected-tests.sh contract suite (#9307) ===\n'
755
+
756
+ # --- A1. SHOULD-PASS control: vitest repo, scoped diff, selected run -----------
757
+ # The unmutated fixture must be green before any mutation-row verdict means
758
+ # anything; a red control poisons every row below it.
759
+ R_CONTROL="$(mk_repo js-basic)"; seed_js "$R_CONTROL"; seal_base "$R_CONTROL"
760
+ commit_chg "$R_CONTROL" src/util.ts 'export const u = 1;'
761
+ reset_fakes
762
+ run_gate "$R_CONTROL" gate
763
+ CONTROL_OK=0
764
+ [[ "$RC" == "0" ]] && CONTROL_OK=1
765
+ want_rc 0 "A1 control: vitest fixture exits 0"
766
+ want_out "A1 control: human sentence" "Ran [0-9]+ of the repo's [0-9]+ test files affected by this change: passed\."
767
+ want_call "A1 control: vitest invoked in related form" 'vitest.*related'
768
+ want_call "A1 control: --run on the related call" 'vitest.*related.*--run'
769
+ want_call "A1 control: the changed file reaches the selector" 'related.*util\.ts'
770
+ want_field "A1 control: mode" mode 'selected'
771
+ want_field "A1 control: stack named" stack '.+'
772
+ want_out "A1 control: machine marker shape" 'AFFECTED_GATE ran=[0-9-]+ of=[0-9-]+ skipped=[0-9-]+ mode='
773
+ if [[ "$CONTROL_OK" != "1" ]]; then
774
+ printf ' [CONTROL-RED] the unmutated fixture failed — every verdict below is suspect\n' >&2
775
+ fi
776
+
777
+ # --- A2. --explain prints the selection and runs nothing -----------------------
778
+ reset_fakes
779
+ run_gate "$R_CONTROL" explain --explain
780
+ want_rc 0 "A2: --explain exits 0"
781
+ if [[ -s "$CALLS" ]]; then fail "A2: --explain invoked a runner: $(tr '\n' '|' < "$CALLS")"; else pass "A2: --explain ran nothing"; fi
782
+ # The marker alone always carries reason=/mode= — pin the EXPLAIN-only lines:
783
+ # the base dump, the per-path classification, and the SELECTION verdict.
784
+ want_out "A2: --explain prints the base dump" 'AFFECTED_EXPLAIN base='
785
+ want_out "A2: --explain lists per-path reasons" 'PATH src/util\.ts class=changed'
786
+ want_out "A2: --explain prints the selection verdict" 'SELECTION mode=selected reason='
787
+
788
+ # --- A3. --self-check ----------------------------------------------------------
789
+ reset_fakes
790
+ run_gate "$R_CONTROL" selfcheck --self-check
791
+ want_rc 0 "A3: --self-check exits 0"
792
+ want_out "A3: self-check marker" 'AFFECTED_GATE_SELF_CHECK ok'
793
+
794
+ # ============================ Guard 2 — capability probe =======================
795
+
796
+ # B1 [G2-2 inverse, G2-1 inverse]: a runner registering --print-selection at a
797
+ # case-label position IS delegated to, and the stack adapters stay dark.
798
+ R_CAP="$(mk_repo capable)"; write_runner_capable "$R_CAP"; seed_js "$R_CAP"; seal_base "$R_CAP"
799
+ commit_chg "$R_CAP" src/util.ts 'export const u = 1;'
800
+ reset_fakes
801
+ run_gate "$R_CAP" gate
802
+ want_rc 0 "B1: capability-positive runner delegates cleanly"
803
+ want_call "B1: runner received --affected" 'test-all.*--affected'
804
+ want_no_call "B1: adapters stay dark under delegation" 'vitest '
805
+ want_out "B1: a delegated pass still prints the verdict line" 'affected-suite gate passed\.'
806
+
807
+ # B1b: --full passthrough hands --full to the same runner.
808
+ reset_fakes
809
+ run_gate "$R_CAP" gate --full
810
+ want_rc 0 "B1b: --full passthrough exits 0"
811
+ want_call "B1b: runner received --full" 'test-all.*--full'
812
+ want_no_call "B1b: --full never reaches an adapter" 'vitest '
813
+
814
+ # B2 [G2-3]: test_command outranks even a capability-positive runner.
815
+ R_CUST="$(mk_repo custom-cmd)"; write_runner_capable "$R_CUST"; seed_js "$R_CUST"
816
+ mkdir -p "$R_CUST/scripts"
817
+ cat > "$R_CUST/scripts/custom.sh" <<'FIX'
818
+ #!/usr/bin/env bash
819
+ printf 'custom\tcwd=%s\n' "$PWD" >> "${FAKE_LOG:?}"
820
+ exit 0
821
+ FIX
822
+ chmod +x "$R_CUST/scripts/custom.sh"
823
+ write_config "$R_CUST" '{"test_command": "bash scripts/custom.sh"}'
824
+ seal_base "$R_CUST"
825
+ commit_chg "$R_CUST" src/util.ts 'export const u = 1;'
826
+ reset_fakes
827
+ run_gate "$R_CUST" gate
828
+ want_rc 0 "B2: test_command fixture exits 0"
829
+ want_call "B2: the custom command ran from the repo root" "custom cwd=$R_CUST"
830
+ want_no_call "B2: test_command bypasses the repo runner" 'test-all '
831
+ want_no_call "B2: test_command bypasses adapters" 'vitest '
832
+ want_field "B2: mode=custom" mode 'custom'
833
+
834
+ # B2b: --explain under test_command must answer BEFORE the arm runs — the custom
835
+ # command IS the whole selection; explain emits mode=custom + the command, rc 0.
836
+ reset_fakes
837
+ run_gate "$R_CUST" explain --explain
838
+ want_rc 0 "B2b: --explain under test_command exits 0"
839
+ want_out "B2b: explain reports mode=custom" 'mode=custom'
840
+ want_out "B2b: the configured command is printed" 'bash scripts/custom\.sh'
841
+ want_no_call "B2b: explain ran nothing (no command, no runner, no adapter)" 'custom|test-all|vitest'
842
+
843
+ # B3 [G2-1]: a foreign same-named runner is never handed Soleur flags — the diff
844
+ # falls through to the stack adapter.
845
+ R_FOR="$(mk_repo foreign)"; write_runner_foreign "$R_FOR"; seed_js "$R_FOR"; seal_base "$R_FOR"
846
+ commit_chg "$R_FOR" src/util.ts 'export const u = 1;'
847
+ reset_fakes
848
+ run_gate "$R_FOR" gate
849
+ want_rc 0 "B3: foreign-runner repo still gates green via the adapter"
850
+ want_no_call "B3: foreign runner never invoked" 'foreign-runner|test-all'
851
+ want_call "B3: the JS adapter ran instead" 'vitest.*related'
852
+
853
+ # B4 [G2-4]: a comment-only mention of --print-selection is not a registration.
854
+ R_COM="$(mk_repo comment-mention)"; write_runner_comment_mention "$R_COM"; seed_js "$R_COM"; seal_base "$R_COM"
855
+ commit_chg "$R_COM" src/util.ts 'export const u = 1;'
856
+ reset_fakes
857
+ run_gate "$R_COM" gate
858
+ want_rc 0 "B4: comment-mention repo gates green via the adapter"
859
+ want_no_call "B4: comment mention is not a capability" 'foreign-runner|test-all'
860
+ want_call "B4: the JS adapter ran instead" 'vitest.*related'
861
+
862
+ # B5 [G2-5 must-PASS]: usage-table-only registration still delegates.
863
+ R_UT="$(mk_repo usage-table)"; write_runner_usage_table "$R_UT"; seed_js "$R_UT"; seal_base "$R_UT"
864
+ commit_chg "$R_UT" src/util.ts 'export const u = 1;'
865
+ reset_fakes
866
+ run_gate "$R_UT" gate
867
+ want_rc 0 "B5: usage-table-only registration delegates cleanly"
868
+ want_call "B5: runner received --affected" 'test-all.*--affected'
869
+ want_no_call "B5: adapters stay dark under delegation" 'vitest '
870
+
871
+ # B6 — delegated runner exit-code mapping (contract: its 4 → 4 runner-refused,
872
+ # its 3 → 1 runner-signal; a plain 1 stays 1).
873
+ reset_fakes; export FAKE_RUNNER_AFFECTED_RC=4
874
+ run_gate "$R_CAP" gate
875
+ want_rc 4 "B6a: runner rc 4 maps to gate rc 4"
876
+ want_out "B6a: runner-refused reason" 'AFFECTED_GATE_REFUSED.*reason=runner-refused'
877
+
878
+ reset_fakes; export FAKE_RUNNER_AFFECTED_RC=3
879
+ run_gate "$R_CAP" gate
880
+ want_rc 1 "B6b: runner rc 3 maps to gate rc 1"
881
+ want_out "B6b: runner-signal reason" 'runner-signal'
882
+
883
+ reset_fakes; export FAKE_RUNNER_AFFECTED_RC=1
884
+ run_gate "$R_CAP" gate
885
+ want_rc 1 "B6c: runner rc 1 stays rc 1"
886
+
887
+ # A runner usage/refusal rc 2 is a REFUSAL, not a plain failure — it must map
888
+ # to rc 4 runner-refused the same as the runner's own 4.
889
+ reset_fakes; export FAKE_RUNNER_AFFECTED_RC=2
890
+ run_gate "$R_CAP" gate
891
+ want_rc 4 "B6d: runner rc 2 maps to gate rc 4"
892
+ want_out "B6d: runner-refused reason" 'AFFECTED_GATE_REFUSED.*reason=runner-refused'
893
+
894
+ # B7 — the Soleur repo itself delegates: --explain in the real tree must resolve
895
+ # to the repo runner or name its mode. Bounded at 120 s (the affected pre-pass is
896
+ # measured ~150 s contended on a busy host, seconds here). REAL_NODE/REAL_PY are
897
+ # passed exactly as run_gate does: the real repo carries a .soleur-test-gate.json,
898
+ # and the shimmed interpreters must delegate the config dump or the gate reads a
899
+ # fake-unparseable config and refuses — a harness artifact, not a gate defect.
900
+ reset_fakes
901
+ # Portability: bound with the gate's own affected_timeout chain (timeout →
902
+ # gtimeout → watchdog) — a bare `timeout` is absent on stock macOS.
903
+ # shellcheck source=plugins/soleur/scripts/lib/affected-tests/common.sh
904
+ source "$DIR/lib/affected-tests/common.sh"
905
+ ( cd "$REPO_ROOT" && affected_timeout 120 \
906
+ env -u CLAUDE_PLUGIN_ROOT PATH="$BIN:$BASEPATH" FAKE_LOG="$CALLS" \
907
+ REAL_NODE="$REAL_NODE" REAL_PY="$REAL_PY" \
908
+ bash "$SUT" --explain ) > "$TESTROOT/outs/live.out" 2>&1
909
+ RC=$?; OUT="$(cat "$TESTROOT/outs/live.out")"
910
+ printf 'explain\t%s\t%s\n' "$RC" "$TESTROOT/outs/live.out" >> "$RUNS"
911
+ want_rc 0 "B7: --explain in the Soleur repo exits 0"
912
+ # mode=/delegat as bare alternation legs are vacuous (every marker carries
913
+ # mode=): require the EXPLAIN-only emitters — a delegation line or the
914
+ # adapter-arm explain dump.
915
+ want_out "B7: live explain resolves to the repo runner or names its mode" 'SELECTION mode=delegated|SELECTION mode=(selected|full|custom|none)|AFFECTED_EXPLAIN base='
916
+
917
+ # B8 [arch-P1 regression]: the capability probe runs BEFORE the gate's inert/
918
+ # global classification — an all-inert diff under a capability-positive runner
919
+ # still delegates, because the runner's own classifier (and its always-on
920
+ # ratchets, which are live edges on AGENTS.md/SKILL.md diffs) is strictly
921
+ # better informed. The gate-side inert shortcut is the adapter arm's alone.
922
+ R_MD_RUNNER="$(mk_repo inert-delegated)"; write_runner_capable "$R_MD_RUNNER"; seal_base "$R_MD_RUNNER"
923
+ commit_chg "$R_MD_RUNNER" README.md 'docs only'
924
+ reset_fakes
925
+ run_gate "$R_MD_RUNNER" gate
926
+ want_rc 0 "B8: md-only diff delegates to the capable runner"
927
+ want_call "B8: runner received --affected on an inert diff" 'test-all.*--affected'
928
+ want_no_out "B8: the gate's inert shortcut does not preempt delegation" 'NO_TESTS_AFFECTED|reason=inert'
929
+ want_field "B8: delegated marker reason" reason 'delegated'
930
+
931
+ # B8b: a global-paths diff delegates the same way — the runner's classifier
932
+ # outranks the gate's global table; only an operator --full maps to --full.
933
+ R_GLOB_RUNNER="$(mk_repo global-delegated)"; write_runner_capable "$R_GLOB_RUNNER"; seal_base "$R_GLOB_RUNNER"
934
+ commit_chg "$R_GLOB_RUNNER" package.json '{"name":"x","scripts":{"test":"true"}}'
935
+ reset_fakes
936
+ run_gate "$R_GLOB_RUNNER" gate
937
+ want_rc 0 "B8b: global-paths diff delegates"
938
+ want_call "B8b: runner received --affected, not --full" 'test-all.*--affected'
939
+ want_no_call "B8b: the gate's global table did not force --full" 'test-all.*--full'
940
+
941
+ # B8c: --explain under a capable runner reports the delegation and runs
942
+ # nothing — not even --print-selection (the runner's own pre-pass has a cost).
943
+ reset_fakes
944
+ run_gate "$R_MD_RUNNER" explain --explain
945
+ want_rc 0 "B8c: --explain under a capable runner exits 0"
946
+ want_out "B8c: explain reports the delegation" 'mode=delegated|test-all\.sh'
947
+ want_no_call "B8c: explain invoked nothing" 'test-all'
948
+
949
+ # ============================ Guard 3 — config fails closed ===================
950
+
951
+ mk_cfg_repo() { # <name> <json> — vitest repo whose config is committed in the BASE
952
+ # (a config sitting in the diff would itself be an unclassified change)
953
+ local r; r="$(mk_repo "$1")"; seed_js "$r"; write_config "$r" "$2"; seal_base "$r"
954
+ commit_chg "$r" src/util.ts 'export const u = 1;'
955
+ printf '%s' "$r"
956
+ }
957
+
958
+ # C1 [G3-3]: malformed JSON → rc 4 bad-config.
959
+ R_BADJSON="$(mk_cfg_repo cfg-malformed '{')"
960
+ reset_fakes; run_gate "$R_BADJSON" gate
961
+ want_rc 4 "C1: malformed config refused"
962
+ want_out "C1: bad-config reason" 'AFFECTED_GATE_REFUSED.*reason=bad-config'
963
+
964
+ # C2 [G3-1, G3-4]: unknown key → rc 4, and the reason must NAME the key — a
965
+ # validator that greps key names but never reads values cannot emit it.
966
+ R_UNK="$(mk_cfg_repo cfg-unknown '{"surprise_key": 1}')"
967
+ reset_fakes; run_gate "$R_UNK" gate
968
+ want_rc 4 "C2: unknown key refused"
969
+ want_out "C2: reason names the key" 'bad-config.*surprise_key|surprise_key.*bad-config'
970
+
971
+ # C3 [G3-2]: wrong type → rc 4 bad-config key=<k>.
972
+ R_WT="$(mk_cfg_repo cfg-wrong-type '{"budget_seconds": "900"}')"
973
+ reset_fakes; run_gate "$R_WT" gate
974
+ want_rc 4 "C3: string budget_seconds refused"
975
+ want_out "C3: reason names the key" 'bad-config.*budget_seconds|budget_seconds.*bad-config'
976
+ R_WT2="$(mk_cfg_repo cfg-wrong-type2 '{"global_paths": "src/**"}')"
977
+ reset_fakes; run_gate "$R_WT2" gate
978
+ want_rc 4 "C3b: string global_paths refused"
979
+ want_out "C3b: reason names the key" 'bad-config.*global_paths|global_paths.*bad-config'
980
+
981
+ # C4 [G3-5 must-PASS]: config carrying only max_workers is valid and caps width.
982
+ R_MW="$(mk_cfg_repo cfg-maxw '{"max_workers": 3}')"
983
+ reset_fakes; export FAKE_VITEST_MAXW=3
984
+ run_gate "$R_MW" gate
985
+ want_rc 0 "C4: max_workers-only config accepted"
986
+ want_call "C4: --maxWorkers honoured at the cap" 'vitest.*[Mm]ax.?[Ww]orkers'
987
+
988
+ # C5: inert_paths REPLACES built-ins — docs/** is inert, README.md no longer is.
989
+ # Inert-only diff: build the fixture WITHOUT the src/util.ts change mk_cfg_repo
990
+ # bakes in — the config sits in the base and the sole post-base change is inert.
991
+ R_INERT="$(mk_repo cfg-inert)"; seed_js "$R_INERT"
992
+ write_config "$R_INERT" '{"inert_paths": ["docs/**"]}'; seal_base "$R_INERT"
993
+ commit_chg "$R_INERT" docs/guide.txt 'docs'
994
+ reset_fakes; run_gate "$R_INERT" gate
995
+ want_rc 0 "C5a: all-inert custom diff exits 0"
996
+ want_out "C5a: NO_TESTS_AFFECTED" 'NO_TESTS_AFFECTED'
997
+ want_out "C5a: inert_rule reported" 'inert_rule='
998
+ R_INERT2="$(mk_cfg_repo cfg-inert2 '{"inert_paths": ["docs/**"]}')"
999
+ commit_chg "$R_INERT2" README.md 'readme change'
1000
+ reset_fakes; run_gate "$R_INERT2" gate
1001
+ want_no_out "C5b: README.md is NOT inert once inert_paths replaces the built-ins" 'NO_TESTS_AFFECTED'
1002
+ want_rc_in "C5b: replaced-inert README diff is refused or falls back to full" 0 4
1003
+ # `reason=|mode=full` is vacuous — every marker carries reason=. Pin the named
1004
+ # non-refusal reason set: on rc 0 the verdict must disclose WHY it ran what it ran.
1005
+ if [[ "$RC" == "0" ]]; then
1006
+ want_out "C5b: fallback discloses a named reason" 'reason=(ok|empty-selection|empty-diff|census-degraded|global-paths|full-requested|no-selector|custom-command|delegated)'
1007
+ fi
1008
+
1009
+ # C6: a global_paths hit (package.json is a built-in) forces the full command.
1010
+ R_GLOB="$(mk_repo global-hit)"; seed_js "$R_GLOB"; seal_base "$R_GLOB"
1011
+ cat > "$R_GLOB/package.json" <<'JSON'
1012
+ { "name": "fixture-js", "private": true, "scripts": { "test": "vitest run" },
1013
+ "devDependencies": { "vitest": "^4.0.0" }, "x": 1 }
1014
+ JSON
1015
+ assert_fixture_dir "$R_GLOB"; git -C "$R_GLOB" add -A; git -C "$R_GLOB" commit -q -m "bump package.json"
1016
+ reset_fakes; run_gate "$R_GLOB" gate
1017
+ want_rc 0 "C6: global-path diff exits 0 via the full command"
1018
+ want_field "C6: mode=full" mode 'full'
1019
+ want_no_call "C6: no related selector under a global hit" 'related'
1020
+ # A bare 'vitest|npm|…' leg is satisfied by the toolchain's --version probe —
1021
+ # pin the invocation SHAPE of a test run, never the probe.
1022
+ want_call "C6: the full command did run" 'vitest run |npm test|pnpm test|yarn test|bun test'
1023
+
1024
+ # C7 [G1-7]: vacuous test_command values → rc 4 vacuous-test-command, each
1025
+ # spelling — INCLUDING the composition bypasses: a true-swallower tail
1026
+ # (`|| true`, `; exit 0`, `| true`), exit-inside-subcommand, comment padding.
1027
+ for _vac in 'true' ':' 'exit 0' 'echo' \
1028
+ 'npm test || true' 'npm test || :' 'make test; exit 0' \
1029
+ 'pytest && true' 'npm test | true' \
1030
+ "bash -c 'exit 0'" 'sh -c "exit 0"' 'true # comment' \
1031
+ 'echo done; exit 0' 'env X=1 bash -c "exit 0"'; do
1032
+ _r="$(mk_cfg_repo "cfg-vac-$(printf '%s' "$_vac" | tr -c 'a-z0-9' '_' | head -c 40)" "{\"test_command\": \"$(printf '%s' "$_vac" | sed 's/"/\\"/g')\"}")"
1033
+ reset_fakes; run_gate "$_r" gate
1034
+ want_rc 4 "C7: test_command '$_vac' refused"
1035
+ want_out "C7: vacuous reason for '$_vac'" 'AFFECTED_GATE_REFUSED.*reason=vacuous-test-command'
1036
+ done
1037
+ unset _vac _r
1038
+ # C7b must-PASS: a real command still runs — `echo` mid-command is not a
1039
+ # swallower when the LAST segment is substantive.
1040
+ R_VACOK="$(mk_cfg_repo cfg-vac-ok '{"test_command": "echo running && bash scripts/custom.sh"}')"
1041
+ mkdir -p "$R_VACOK/scripts"
1042
+ cat > "$R_VACOK/scripts/custom.sh" <<'FIX'
1043
+ #!/usr/bin/env bash
1044
+ printf 'custom\tcwd=%s\n' "$PWD" >> "${FAKE_LOG:?}"
1045
+ exit 0
1046
+ FIX
1047
+ chmod +x "$R_VACOK/scripts/custom.sh"
1048
+ assert_fixture_dir "$R_VACOK"; git -C "$R_VACOK" add -A; git -C "$R_VACOK" commit -q -m "add custom runner"
1049
+ reset_fakes; run_gate "$R_VACOK" gate
1050
+ want_rc 0 "C7b: substantive test_command accepted"
1051
+ want_call "C7b: the custom command ran" 'custom cwd='
1052
+
1053
+ # C8: non-positive budget_seconds / max_workers fail closed as bad-config — a
1054
+ # zero budget would defer instantly and a non-positive width is nonsense.
1055
+ R_CL1="$(mk_cfg_repo cfg-budget-zero '{"budget_seconds": 0}')"
1056
+ reset_fakes; run_gate "$R_CL1" gate
1057
+ want_rc 4 "C8a: budget_seconds=0 refused"
1058
+ want_out "C8a: reason names the key" 'bad-config.*budget_seconds|budget_seconds.*bad-config'
1059
+ R_CL2="$(mk_cfg_repo cfg-maxw-neg '{"max_workers": -2}')"
1060
+ reset_fakes; run_gate "$R_CL2" gate
1061
+ want_rc 4 "C8b: negative max_workers refused"
1062
+ want_out "C8b: reason names the key" 'bad-config.*max_workers|max_workers.*bad-config'
1063
+ want_out "C8b: refusal carries a remedy pointer" 'remedy:'
1064
+
1065
+ # C9: a match-everything inert_paths pattern is a permanent green — refused as
1066
+ # bad-config, never silently applied. `*`/`**`/`**/*` and any pattern that
1067
+ # matches an arbitrary probe path all fail closed.
1068
+ _ipn=0
1069
+ for _ip in '*' '**' '**/*' '*/**'; do
1070
+ _ipn=$((_ipn + 1)) # `**/*` and `*/**` both sanitise to `____` — index the name
1071
+ _r="$(mk_cfg_repo "cfg-inertstar-${_ipn}" "{\"inert_paths\": [\"$_ip\"]}")"
1072
+ reset_fakes; run_gate "$_r" gate
1073
+ want_rc 4 "C9: inert_paths '$_ip' refused"
1074
+ want_out "C9: refusal names inert_paths for '$_ip'" 'bad-config.*inert_paths|inert_paths.*bad-config'
1075
+ want_no_call "C9: nothing ran for '$_ip'" 'vitest '
1076
+ done
1077
+ unset _ip _r _ipn
1078
+ # C9b must-PASS: a real inert pattern still classifies (docs/** inert over a
1079
+ # docs-only diff → NO_TESTS_AFFECTED, same shape as C5a).
1080
+ R_INOK="$(mk_repo cfg-inert-ok)"; seed_js "$R_INOK"
1081
+ write_config "$R_INOK" '{"inert_paths": ["docs/**", "notes.txt"]}'; seal_base "$R_INOK"
1082
+ commit_chg "$R_INOK" docs/guide.md 'docs'
1083
+ reset_fakes; run_gate "$R_INOK" gate
1084
+ want_rc 0 "C9b: a non-total inert_paths set is accepted"
1085
+ want_out "C9b: inert verdict" 'NO_TESTS_AFFECTED'
1086
+
1087
+ # C10: the jq config decoder actually DECODES — a jq-only host (no python3, no
1088
+ # node on PATH) must still honour test_command. Literal NULs in the jq program
1089
+ # were stripped by bash before this row existed; the pinned engine is real jq.
1090
+ JQBIN="$TESTROOT/bin-jqonly"; mkdir -p "$JQBIN"
1091
+ for _t in jq git bash env grep find sed awk sort sort tr cut head tail cat date \
1092
+ sleep ps getconf sysctl mktemp rm wc dirname basename uname comm ls \
1093
+ mkdir chmod touch printf od; do
1094
+ _p="$(command -v "$_t" 2>/dev/null || true)"
1095
+ [[ -n "$_p" && "$_p" != "$BIN"/* ]] && ln -sf "$_p" "$JQBIN/$_t"
1096
+ done
1097
+ for _t in vitest jest pytest go nx turbo npm npx pnpm yarn bun bunx; do
1098
+ ln -sf "$BIN/$_t" "$JQBIN/$_t"
1099
+ done
1100
+ if [[ -x "$JQBIN/jq" && -x "$JQBIN/git" && -x "$JQBIN/bash" ]]; then
1101
+ R_JQONLY="$(mk_repo jq-only)"; seed_js "$R_JQONLY"
1102
+ mkdir -p "$R_JQONLY/scripts"
1103
+ cat > "$R_JQONLY/scripts/custom.sh" <<'FIX'
1104
+ #!/usr/bin/env bash
1105
+ printf 'custom\tcwd=%s\n' "$PWD" >> "${FAKE_LOG:?}"
1106
+ exit 0
1107
+ FIX
1108
+ chmod +x "$R_JQONLY/scripts/custom.sh"
1109
+ write_config "$R_JQONLY" '{"test_command": "bash scripts/custom.sh"}'
1110
+ seal_base "$R_JQONLY"
1111
+ commit_chg "$R_JQONLY" src/util.ts 'export const u = 1;'
1112
+ reset_fakes
1113
+ run_gate_path "$JQBIN" "$R_JQONLY" gate
1114
+ want_rc 0 "C10: jq-only host parses the config"
1115
+ want_field "C10: jq decode reached the custom arm" mode 'custom'
1116
+ want_call "C10: the configured command ran" 'custom cwd='
1117
+ else
1118
+ fail "C10: cannot build the jq-only PATH fixture (missing jq/git/bash on host)"
1119
+ fi
1120
+ unset _t _p
1121
+
1122
+ # C10b: a top-level NON-OBJECT config must refuse identically on every decoder.
1123
+ # py/node exit 3 on `[]`; a bare jq `to_entries` over `[]` decodes to ZERO
1124
+ # records — a silent accept (no keys, no badkey) unless the jq arm checks
1125
+ # `type == "object"` first. rc 4 bad-config is the contract.
1126
+ if [[ -x "$JQBIN/jq" && -x "$JQBIN/git" && -x "$JQBIN/bash" ]]; then
1127
+ R_JQARR="$(mk_repo jq-array)"; seed_js "$R_JQARR"
1128
+ write_config "$R_JQARR" '[]'
1129
+ seal_base "$R_JQARR"
1130
+ commit_chg "$R_JQARR" src/util.ts 'export const u = 1;'
1131
+ reset_fakes
1132
+ run_gate_path "$JQBIN" "$R_JQARR" gate
1133
+ want_rc 4 "C10b: jq-only host refuses a top-level array config"
1134
+ want_out "C10b: bad-config reason" 'AFFECTED_GATE_REFUSED.*reason=bad-config'
1135
+ fi
1136
+
1137
+ # ============================ Guard 1 — no vacuous green =======================
1138
+
1139
+ # D1: an all-inert diff (README.md only) is the ONLY rc-0-with-zero-tests path.
1140
+ R_MD="$(mk_repo inert-md)"; seed_js "$R_MD"; seal_base "$R_MD"
1141
+ commit_chg "$R_MD" README.md 'docs only'
1142
+ reset_fakes; run_gate "$R_MD" gate
1143
+ want_rc 0 "D1: docs-only diff exits 0"
1144
+ want_out "D1: NO_TESTS_AFFECTED" 'NO_TESTS_AFFECTED'
1145
+ want_out "D1: human inert line" 'No tests ran: docs-only change\.'
1146
+ want_out "D1: inert_rule reported" 'inert_rule='
1147
+ if [[ -s "$CALLS" ]]; then fail "D1: inert diff invoked a runner: $(tr '\n' '|' < "$CALLS")"; else pass "D1: inert diff ran nothing"; fi
1148
+
1149
+ # D2 [G1-3]: inert-then-code — a first-path inert shortcut would swallow src/util.ts.
1150
+ R_MIX="$(mk_repo mixed)"; seed_js "$R_MIX"; seal_base "$R_MIX"
1151
+ commit_chg "$R_MIX" README.md 'docs'
1152
+ commit_chg "$R_MIX" src/util.ts 'export const u = 1;'
1153
+ reset_fakes; run_gate "$R_MIX" gate
1154
+ want_rc 0 "D2: mixed diff exits 0 via real selection"
1155
+ want_no_out "D2: mixed diff is never inert" 'NO_TESTS_AFFECTED'
1156
+ want_call "D2: the code file was selected" 'related.*util\.ts'
1157
+
1158
+ # D3 [G1-1]: the native selector returns nothing for a code diff → full fallback,
1159
+ # never a zero-test green. rc 0 is acceptable ONLY with mode=full and a named
1160
+ # reason; rc 4 is an acceptable loud refusal.
1161
+ R_EMPTY="$(mk_repo empty-sel)"; seed_js "$R_EMPTY"; seal_base "$R_EMPTY"
1162
+ commit_chg "$R_EMPTY" src/orphan.ts 'export const o = 1;'
1163
+ reset_fakes; export FAKE_VITEST_NOTESTS=1
1164
+ run_gate "$R_EMPTY" gate
1165
+ if [[ "$RC" == "0" ]]; then
1166
+ pass "D3: empty selection resolved to a completed run"
1167
+ # Contract: the full command ran and the fallback is DISCLOSED. mode=full is
1168
+ # one spelling; mode=selected with reason=empty-selection is accepted too —
1169
+ # a bare green with no named reason is the only failure.
1170
+ want_out "D3: the empty-selection fallback is disclosed" 'reason=[^ ]*(empty|no.test|select)|mode=full'
1171
+ elif [[ "$RC" == "4" ]]; then
1172
+ pass "D3: empty selection refused loudly (rc 4)"
1173
+ else
1174
+ fail "D3: empty selection produced rc=$RC — a zero-test rc 0 is the Guard-1 defect"
1175
+ fi
1176
+
1177
+ # D3b: an unclassifiable changed file (not inert, not global, not a test or source
1178
+ # path) yields an empty native selection on the Python adapter → full fallback.
1179
+ R_EMPTY2="$(mk_repo empty-sel-py)"; seed_python "$R_EMPTY2"; seal_base "$R_EMPTY2"
1180
+ commit_chg "$R_EMPTY2" assets/logo.svg '<svg/>'
1181
+ reset_fakes; run_gate "$R_EMPTY2" gate
1182
+ if [[ "$RC" == "0" ]]; then
1183
+ want_call "D3b: pytest ran the suite" 'pytest '
1184
+ # `reason=|mode=full` is vacuous — the disclosure must be a NAMED fallback
1185
+ # reason, not the mere presence of the marker's reason field.
1186
+ want_out "D3b: a named fallback reason is disclosed" 'reason=(empty-selection|no-selector)|mode=full'
1187
+ else
1188
+ want_rc_in "D3b: unclassifiable diff is refused or falls back" 4
1189
+ fi
1190
+
1191
+ # D4: tests exist but no stack resolves → rc 4 no-stack. The fixture file must
1192
+ # match the test-file census shape (`*.test.*`) — a plain `.rb` name is
1193
+ # invisible to it and would land on rc 7 no-tests instead, which is why the
1194
+ # filename carries the marker.
1195
+ R_RB="$(mk_repo no-stack)"; seal_base "$R_RB"
1196
+ mkdir -p "$R_RB/test"; printf 'def test_w; end\n' > "$R_RB/test/test_widget.test.rb"
1197
+ assert_fixture_dir "$R_RB"; git -C "$R_RB" add -A; git -C "$R_RB" commit -q -m "ruby tests"
1198
+ reset_fakes; run_gate "$R_RB" gate
1199
+ # Deterministic: the census sees a test file, no adapter resolves, no foreign
1200
+ # runner → rc 4 reason=no-stack. Never a hedged 4-or-7.
1201
+ want_rc 4 "D4: tests-exist-but-no-stack refuses (rc 4)"
1202
+ want_out "D4: no-stack reason" 'AFFECTED_GATE_REFUSED.*reason=no-stack'
1203
+ want_no_out "D4: never labelled passed" 'passed\.|tests verified'
1204
+
1205
+ # D5 [G1-2]: no runner on PATH at all → rc 4, never a silent 0. The fixture has
1206
+ # no node_modules/.bin — a repo-local copy would resolve the shim anyway.
1207
+ R_NOPATH="$(mk_repo no-path)"; seed_js_nobin "$R_NOPATH"; seal_base "$R_NOPATH"
1208
+ commit_chg "$R_NOPATH" src/util.ts 'export const u = 1;'
1209
+ reset_fakes
1210
+ run_gate_path "/usr/bin:/bin" "$R_NOPATH" gate
1211
+ want_rc 4 "D5: toolchain-missing PATH is refused"
1212
+ want_out "D5: refusal emitted" 'AFFECTED_GATE_REFUSED'
1213
+
1214
+ # D6: no test suite detected at all → rc 7 with a disclosed note, never "passed".
1215
+ R_NONE="$(mk_repo no-suite)"; seal_base "$R_NONE"
1216
+ commit_chg "$R_NONE" src/plain.txt 'data'
1217
+ reset_fakes; run_gate "$R_NONE" gate
1218
+ want_rc 7 "D6: no test suite detected → rc 7"
1219
+ want_no_out "D6: rc 7 is never labelled passed" 'passed\.|tests verified'
1220
+
1221
+ # D7: the changed set is committed ∪ staged ∪ unstaged ∪ untracked.
1222
+ R_UN="$(mk_repo union)"; seed_python "$R_UN"; seal_base "$R_UN"
1223
+ printf 'def test_a():\n assert 1 + 1 == 2\n' > "$R_UN/tests/test_a.py" # unstaged mod
1224
+ assert_fixture_dir "$R_UN"; printf 'def test_b():\n assert True\n' > "$R_UN/tests/test_b.py"; git -C "$R_UN" add tests/test_b.py # staged
1225
+ printf 'def test_c():\n assert True\n' > "$R_UN/tests/test_c.py" # untracked
1226
+ reset_fakes; export FAKE_PYTEST_EXPECT="test_a.py test_b.py test_c.py"
1227
+ run_gate "$R_UN" gate
1228
+ want_rc 0 "D7: staged+unstaged+untracked union runs green"
1229
+ want_call "D7: unstaged file selected" 'test_a\.py'
1230
+ want_call "D7: staged file selected" 'test_b\.py'
1231
+ want_call "D7: untracked file selected" 'test_c\.py'
1232
+ # Unit integrity: ran/of both count test FILES — the 3 changed test files ran
1233
+ # out of a 3-file census (pytest's `collected N items` is items, not the count).
1234
+ want_field "D7: ran is in file units" ran '3'
1235
+ want_field "D7: of is the repo census" of '3'
1236
+
1237
+ # D8: --base overrides resolution — only the LAST commit's files are selected.
1238
+ R_BASE="$(mk_repo base-flag)"; seed_js "$R_BASE"; seal_base "$R_BASE"
1239
+ commit_chg "$R_BASE" src/first.ts 'export const f = 1;'
1240
+ commit_chg "$R_BASE" src/second.ts 'export const s = 1;'
1241
+ reset_fakes
1242
+ run_gate "$R_BASE" gate --base HEAD~1
1243
+ want_rc 0 "D8: --base override exits 0"
1244
+ _rel="$(grep 'related' "$CALLS" | head -n 1)"
1245
+ if [[ "$_rel" == *second.ts* && "$_rel" != *first.ts* ]]; then
1246
+ pass "D8: selection is scoped to the --base ref"
1247
+ else
1248
+ fail "D8: related argv not scoped to HEAD~1: $_rel"
1249
+ fi
1250
+ unset _rel
1251
+ reset_fakes
1252
+ run_gate "$R_BASE" gate --base refs/heads/definitely-missing
1253
+ want_rc 4 "D8b: an unresolvable --base is refused"
1254
+ want_out "D8b: refusal emitted" 'AFFECTED_GATE_REFUSED'
1255
+
1256
+ # D9: shallow clone whose one bounded deepen cannot recover a base → rc 4
1257
+ # no-base-ref, never a silent full and never a silent empty.
1258
+ R_SHSRC="$(mk_repo shallow-src)"; seed_js "$R_SHSRC"
1259
+ assert_fixture_dir "$R_SHSRC"; git -C "$R_SHSRC" add -A; git -C "$R_SHSRC" commit -q -m "base"
1260
+ R_SHALLOW="$WORLD/repos/shallow"
1261
+ git clone -q --depth 1 "file://$R_SHSRC" "$R_SHALLOW" 2>/dev/null
1262
+ git -C "$R_SHALLOW" config user.email soleur-test@example.invalid
1263
+ git -C "$R_SHALLOW" config user.name "Soleur Fixture"
1264
+ git -C "$R_SHALLOW" config commit.gpgsign false
1265
+ git -C "$R_SHALLOW" remote set-url origin "$WORLD/remotes/definitely-missing.git"
1266
+ while IFS= read -r _ref; do git -C "$R_SHALLOW" update-ref -d "$_ref"; done \
1267
+ < <(git -C "$R_SHALLOW" for-each-ref --format='%(refname)' refs/remotes/)
1268
+ git -C "$R_SHALLOW" branch --unset-upstream 2>/dev/null || true
1269
+ reset_fakes
1270
+ run_gate "$R_SHALLOW" gate
1271
+ want_rc 4 "D9: shallow clone with unrecoverable base is refused"
1272
+ want_out "D9: no-base-ref reason" 'AFFECTED_GATE_REFUSED.*reason=no-base-ref'
1273
+ want_no_out "D9: never a silent full on a dead base" 'mode=full'
1274
+
1275
+ # D9b [census legs]: a census leg that fails toward empty must NEVER silently
1276
+ # narrow the selection — the untracked leg is the worst case (a dead
1277
+ # `ls-files --others` drops every NEW test file). The contract: forced full +
1278
+ # a disclosed reason=census-degraded. A scoped git shim fails ONLY the
1279
+ # untracked leg, and only while armed — all other git calls delegate to the
1280
+ # real binary.
1281
+ GITFAILBIN="$TESTROOT/bin-gitfail"; mkdir -p "$GITFAILBIN"
1282
+ ln -sf "$BIN"/* "$GITFAILBIN/"
1283
+ cat > "$GITFAILBIN/git" <<'SHIM'
1284
+ #!/usr/bin/env bash
1285
+ if [[ "${FAKE_GIT_FAIL_OTHERS:-0}" == "1" ]]; then
1286
+ _ls=0; _oth=0
1287
+ for _a in "$@"; do
1288
+ [[ "$_a" == "ls-files" ]] && _ls=1
1289
+ [[ "$_a" == "--others" ]] && _oth=1
1290
+ done
1291
+ if (( _ls && _oth )); then
1292
+ echo "stub git: simulated ls-files --others failure" >&2
1293
+ exit 1
1294
+ fi
1295
+ fi
1296
+ exec "${REAL_GIT:?REAL_GIT unset}" "$@"
1297
+ SHIM
1298
+ chmod +x "$GITFAILBIN/git"
1299
+ R_CFAIL="$(mk_repo census-degraded)"; seed_js "$R_CFAIL"; seal_base "$R_CFAIL"
1300
+ commit_chg "$R_CFAIL" src/util.ts 'export const u = 1;'
1301
+ # An UNTRACKED test file — exactly what a masked ls-files leg would drop.
1302
+ printf 'import { test } from "vitest";\ntest("n", () => {});\n' > "$R_CFAIL/src/new.test.ts"
1303
+ reset_fakes; export FAKE_GIT_FAIL_OTHERS=1
1304
+ run_gate_path "$GITFAILBIN:$BASEPATH" "$R_CFAIL" gate
1305
+ want_rc 0 "D9b: a failed untracked census leg degrades to full — never narrows"
1306
+ want_field "D9b: mode=full" mode 'full'
1307
+ want_out "D9b: census-degraded reason disclosed" 'reason=census-degraded'
1308
+ want_call "D9b: the full command ran" 'vitest run '
1309
+ want_no_call "D9b: no related selection on a degraded census" 'related'
1310
+
1311
+ # ============================ adapters =========================================
1312
+
1313
+ # D15: jest — --findRelatedTests <files>; the __tests__ convention counts in of=.
1314
+ R_JEST="$(mk_repo jest-repo)"; seed_jest "$R_JEST"
1315
+ mkdir -p "$R_JEST/src/__tests__"; printf 'test("x", () => {});\n' > "$R_JEST/src/__tests__/x.js"
1316
+ seal_base "$R_JEST"
1317
+ commit_chg "$R_JEST" src/util.ts 'export const u = 1;'
1318
+ reset_fakes; export FAKE_JEST_REQUIRE_RELATED=1
1319
+ run_gate "$R_JEST" gate
1320
+ want_rc 0 "D15: jest fixture exits 0"
1321
+ want_call "D15: jest selector is --findRelatedTests" 'jest.*--findRelatedTests'
1322
+ want_call "D15: the changed file reaches jest" 'findRelatedTests.*util\.ts'
1323
+ want_field "D15: of= counts __tests__ files too" of '2'
1324
+
1325
+ # D16 [G1-9]: go — a leaf change must pull the reverse-dependency closure; the
1326
+ # changed package alone is the mutation this row exists to refuse.
1327
+ R_GO="$(mk_repo go-repo)"; seed_go "$R_GO" "$TESTROOT/go-graph"; seal_base "$R_GO"
1328
+ commit_chg "$R_GO" internal/leaf/leaf.go 'package leaf\n\nfunc Leaf() int { return 2 }'
1329
+ reset_fakes; export FAKE_GO_DIR="$TESTROOT/go-graph" FAKE_GO_EXPECT="internal/leaf cmd/app"
1330
+ run_gate "$R_GO" gate
1331
+ want_rc 0 "D16: go fixture exits 0"
1332
+ want_call "D16: leaf package under test" 'go.*test.*internal/leaf'
1333
+ want_call "D16: reverse dependent under test" 'go.*test.*cmd/app'
1334
+ # Unit integrity: two PACKAGES ran but only one test FILE exists — ran is in
1335
+ # file units like of= (packages≠files was the original unit-divergence defect).
1336
+ want_field "D16: ran counts test files not packages" ran '1'
1337
+ want_field "D16: of is the repo census" of '1'
1338
+
1339
+ # D17: nx.json wins over package.json.
1340
+ R_NX="$(mk_repo nx-repo)"; seed_nx "$R_NX"; seal_base "$R_NX"
1341
+ commit_chg "$R_NX" src/util.ts 'export const u = 1;'
1342
+ reset_fakes; run_gate "$R_NX" gate
1343
+ want_rc 0 "D17: nx fixture exits 0"
1344
+ want_call "D17: nx affected ran" 'nx.*affected'
1345
+ want_no_call "D17: vitest stays dark under nx" 'vitest '
1346
+
1347
+ # D17b: a scheduler reporting "10 tasks" is NOT the vacuous "0 tasks" shape —
1348
+ # an unanchored '0 tasks' grep matches inside the count and false-flags the run
1349
+ # as empty, triggering a spurious full retry.
1350
+ reset_fakes; export FAKE_NX_LINE="Successfully ran target test for 10 tasks"
1351
+ run_gate "$R_NX" gate
1352
+ want_rc 0 "D17b: nx '10 tasks' run exits 0"
1353
+ want_call "D17b: nx affected ran" 'nx.*affected'
1354
+ want_no_call "D17b: no vacuous full-retry on a real task count" 'nx.*run-many'
1355
+
1356
+ # D17c: a NUMERIC ran=0 (`0 successful` — a spelling the phrase grep misses) is
1357
+ # vacuous the same as `0 tasks` → empty-selection full retry, never a pass.
1358
+ reset_fakes; export FAKE_NX_LINE="Tasks: 0 successful" FAKE_NX_FULL_LINE="nx run-many: ran target test for 3 projects"
1359
+ run_gate "$R_NX" gate
1360
+ want_rc 0 "D17c: nx numeric-zero selection retries full"
1361
+ want_call "D17c: the full retry ran" 'nx.*run-many'
1362
+ want_out "D17c: the escalation is disclosed" 'empty-selection|mode=full'
1363
+
1364
+ # D18: turbo.json wins over package.json.
1365
+ R_TB="$(mk_repo turbo-repo)"; seed_turbo "$R_TB"; seal_base "$R_TB"
1366
+ commit_chg "$R_TB" src/util.ts 'export const u = 1;'
1367
+ reset_fakes; run_gate "$R_TB" gate
1368
+ want_rc 0 "D18: turbo fixture exits 0"
1369
+ want_call "D18: turbo run test ran" 'turbo.*run.*test'
1370
+ want_no_call "D18: vitest stays dark under turbo" 'vitest '
1371
+
1372
+ # D10/D11: Python source diffs — pytest-testmon when importable, else full suite.
1373
+ R_PYSRC="$(mk_repo py-src)"; seed_python "$R_PYSRC"; seal_base "$R_PYSRC"
1374
+ commit_chg "$R_PYSRC" src/mod.py 'def f():\n return 2'
1375
+ reset_fakes; run_gate "$R_PYSRC" gate
1376
+ want_rc 0 "D10: source diff without testmon runs the suite"
1377
+ # 'pytest' bare matches the `python3 -c 'import pytest'` probe line too —
1378
+ # anchor on the tool's own ledger slot.
1379
+ want_call "D10: pytest invoked" 'pytest '
1380
+ want_field "D10: full-escalation ran equals the census" ran '1'
1381
+ want_field "D10: census" of '1'
1382
+ R_PYTM="$(mk_repo py-testmon)"; seed_python "$R_PYTM"; seal_base "$R_PYTM"
1383
+ commit_chg "$R_PYTM" src/mod.py 'def f():\n return 2'
1384
+ reset_fakes; export FAKE_PY_IMPORTABLE="pytest testmon pytest_testmon"
1385
+ run_gate "$R_PYTM" gate
1386
+ want_rc 0 "D11: testmon-importable source diff exits 0"
1387
+ # A bare `testmon` grep is satisfied by the import-PROBE ledger line
1388
+ # (`python3 -I -c 'import pytest_testmon'`) — pin the RUN argv instead:
1389
+ # `pytest\t--testmon …` (the pytest stub re-logs under either tool spelling).
1390
+ want_call "D11: the testmon selection ran (--testmon argv, not the probe)" 'pytest .*--testmon'
1391
+ # testmon's file count is not derivable from `collected N / M deselected`
1392
+ # (items, not files) — the honest marker is ran=-, and the pass verdict still
1393
+ # prints because a real run happened.
1394
+ want_field "D11: testmon ran is honestly unknown" ran '-'
1395
+ want_out "D11: pass verdict prints despite opaque tally" 'affected-suite gate passed\.'
1396
+
1397
+ # D12/D13: pytest -n auto iff pytest-xdist imports, else serial with a reason.
1398
+ R_PYXD="$(mk_repo py-xdist)"; seed_python "$R_PYXD"; seal_base "$R_PYXD"
1399
+ commit_chg "$R_PYXD" tests/test_a.py 'def test_a():\n assert 1 + 1 == 2'
1400
+ reset_fakes; export FAKE_PY_IMPORTABLE="pytest xdist"
1401
+ run_gate "$R_PYXD" gate
1402
+ want_rc 0 "D12: xdist fixture exits 0"
1403
+ want_call "D12: xdist parallelism enabled (-n auto or a width)" 'pytest.*-n(=| )?(auto|[0-9]+)'
1404
+ R_PYSE="$(mk_repo py-serial)"; seed_python "$R_PYSE"; seal_base "$R_PYSE"
1405
+ commit_chg "$R_PYSE" tests/test_a.py 'def test_a():\n assert 1 + 1 == 2'
1406
+ reset_fakes; export FAKE_PY_IMPORTABLE="pytest"
1407
+ run_gate "$R_PYSE" gate
1408
+ want_rc 0 "D13: serial fixture exits 0"
1409
+ want_no_call "D13: no -n width without xdist" 'pytest.*-n(=| )'
1410
+ if grep -qiE 'serial|xdist' <<<"$OUT"; then
1411
+ pass "D13: serial reason is stated"
1412
+ else
1413
+ fail "D13: serial run without a stated reason"
1414
+ fi
1415
+
1416
+ # D14 [G1-5]: pytest rc 5 (no tests collected) is NOT a pass.
1417
+ R_PY5="$(mk_repo py-rc5)"; seed_python "$R_PY5"; seal_base "$R_PY5"
1418
+ commit_chg "$R_PY5" tests/test_a.py 'def test_a():\n assert 1 + 1 == 2'
1419
+ reset_fakes; export FAKE_PYTEST_RC=5
1420
+ run_gate "$R_PY5" gate
1421
+ want_rc_in "D14: pytest rc 5 is never green" 1 4
1422
+ want_no_out "D14: no 'passed' claim on an empty collection" 'Ran .* passed\.|tests verified'
1423
+
1424
+ # D24: a bare pyproject.toml is a Python-PROJECT marker, not a pytest-SUITE
1425
+ # marker — without a pytest signal in a manifest and without test_*.py files,
1426
+ # the py adapter must not claim the repo.
1427
+ R_PYNO="$(mk_repo py-nosignal)"
1428
+ printf '[project]\nname = "fixture-lib"\n' > "$R_PYNO/pyproject.toml"
1429
+ mkdir -p "$R_PYNO/src"; printf 'def f():\n return 1\n' > "$R_PYNO/src/lib.py"
1430
+ seal_base "$R_PYNO"
1431
+ commit_chg "$R_PYNO" src/lib.py 'def f():\n return 2'
1432
+ reset_fakes; run_gate "$R_PYNO" gate
1433
+ want_rc 7 "D24: pytest-signal-free pyproject.toml is not a py detection"
1434
+ want_no_call "D24: pytest never invoked" 'pytest '
1435
+
1436
+ # D19 [G1-6]: multi-stack — an empty adapter cannot launder or sink the other.
1437
+ R_MS="$(mk_repo multi-stack)"; seed_python "$R_MS"
1438
+ seed_go "$R_MS" "$TESTROOT/go-graph-ms"
1439
+ seal_base "$R_MS"
1440
+ commit_chg "$R_MS" src/mod.py 'def f():\n return 3'
1441
+ reset_fakes; export FAKE_GO_DIR="$TESTROOT/go-graph-ms"
1442
+ run_gate "$R_MS" gate
1443
+ want_rc 0 "D19a: py-only diff in a py+go repo exits 0"
1444
+ want_call "D19a: pytest ran" 'pytest '
1445
+ # Both adapters produce honest FILE counts on this path — `ran=-` (opaque
1446
+ # tally) is a counted-path lie and must not satisfy this row.
1447
+ want_out "D19a: the run is counted non-empty" 'AFFECTED_GATE ran=[1-9][0-9]* of=[1-9][0-9]*'
1448
+ commit_chg "$R_MS" internal/leaf/leaf.go 'package leaf\n\nfunc Leaf() int { return 3 }'
1449
+ reset_fakes; export FAKE_PYTEST_RC=1 FAKE_GO_DIR="$TESTROOT/go-graph-ms" FAKE_GO_EXPECT="internal/leaf cmd/app"
1450
+ run_gate "$R_MS" gate
1451
+ want_rc 1 "D19b: one failing stack dominates the aggregate (1 > 4 > 6 > 0)"
1452
+
1453
+ # ============================ budget + CI ======================================
1454
+
1455
+ # D20a [P6]: CI workflow detected + budget expires → rc 6 BUDGET_DEFERRED.
1456
+ R_BUD="$(mk_repo budget-ci)"; seed_js "$R_BUD"; seed_ci "$R_BUD"
1457
+ write_config "$R_BUD" '{"budget_seconds": 1}'
1458
+ seal_base "$R_BUD"
1459
+ commit_chg "$R_BUD" src/util.ts 'export const u = 1;'
1460
+ reset_fakes; export FAKE_VITEST_SLEEP=5
1461
+ run_gate "$R_BUD" gate
1462
+ want_rc 6 "D20a: expired budget with CI detected defers"
1463
+ want_out "D20a: deferral marker" 'AFFECTED_GATE_BUDGET_DEFERRED'
1464
+ want_field "D20a: ci field names the workflow" ci '.*\.yml|.*workflows.*'
1465
+
1466
+ # D20b [G1-8]: a failure BEFORE expiry wins over deferral. A 1 s budget makes
1467
+ # this racy (gate startup can eat the deadline before the run starts), so this
1468
+ # arm gets a larger budget — the failure still lands first, deterministically.
1469
+ R_BUDF="$(mk_repo budget-fail-first)"; seed_js "$R_BUDF"; seed_ci "$R_BUDF"
1470
+ write_config "$R_BUDF" '{"budget_seconds": 120}'
1471
+ seal_base "$R_BUDF"
1472
+ commit_chg "$R_BUDF" src/util.ts 'export const u = 1;'
1473
+ reset_fakes; export FAKE_VITEST_RC=1
1474
+ run_gate "$R_BUDF" gate
1475
+ want_rc 1 "D20b: failure before budget expiry is rc 1"
1476
+ want_no_out "D20b: no deferral when a failure already landed" 'AFFECTED_GATE_BUDGET_DEFERRED'
1477
+
1478
+ # D20c [G1-4]: no CI detected → the budget is ignored and the run completes.
1479
+ R_NOCI="$(mk_repo budget-noci)"; seed_js "$R_NOCI"
1480
+ write_config "$R_NOCI" '{"budget_seconds": 1}'
1481
+ seal_base "$R_NOCI"
1482
+ commit_chg "$R_NOCI" src/util.ts 'export const u = 1;'
1483
+ reset_fakes; export FAKE_VITEST_SLEEP=3
1484
+ run_gate "$R_NOCI" gate
1485
+ want_rc 0 "D20c: budget ignored without CI — run completes"
1486
+ want_no_out "D20c: no deferral without CI" 'AFFECTED_GATE_BUDGET_DEFERRED'
1487
+ # `ci=none` is unconditional on every no-CI run's marker — pin the upfront
1488
+ # disclosure sentence itself, not the marker field.
1489
+ if grep -qiE 'no CI test workflow detected|budget not enforced' <<<"$OUT"; then
1490
+ pass "D20c: the no-CI disregard is disclosed"
1491
+ else
1492
+ fail "D20c: budget ignored without the upfront no-CI line"
1493
+ fi
1494
+
1495
+ # D20d: budget_ci: true forces deferral eligibility without a workflow file.
1496
+ R_BCI="$(mk_repo budget-ci-flag)"; seed_js "$R_BCI"
1497
+ write_config "$R_BCI" '{"budget_ci": true, "budget_seconds": 1}'
1498
+ seal_base "$R_BCI"
1499
+ commit_chg "$R_BCI" src/util.ts 'export const u = 1;'
1500
+ reset_fakes; export FAKE_VITEST_SLEEP=5
1501
+ run_gate "$R_BCI" gate
1502
+ want_rc 6 "D20d: budget_ci=true defers without a workflow file"
1503
+ want_out "D20d: deferral marker" 'AFFECTED_GATE_BUDGET_DEFERRED'
1504
+
1505
+ # D20e [trigger anchor]: `push`/`pull_request` as a SUBSTRING inside a run:
1506
+ # step is not a trigger declaration — `git push` and `pushd` in a deploy
1507
+ # workflow must not arm the budget. The workflow below carries a real
1508
+ # test-command shape too, so only the anchored `on:` check keeps this dark.
1509
+ R_CIPUSH="$(mk_repo ci-push-substring)"; seed_js "$R_CIPUSH"
1510
+ mkdir -p "$R_CIPUSH/.github/workflows"
1511
+ cat > "$R_CIPUSH/.github/workflows/deploy.yml" <<'YML'
1512
+ name: deploy
1513
+ on: workflow_dispatch
1514
+ jobs:
1515
+ deploy:
1516
+ runs-on: ubuntu-latest
1517
+ steps:
1518
+ - run: npm test
1519
+ - run: git push origin main
1520
+ - run: pushd dist && ./publish.sh
1521
+ YML
1522
+ write_config "$R_CIPUSH" '{"budget_seconds": 1}'
1523
+ seal_base "$R_CIPUSH"
1524
+ commit_chg "$R_CIPUSH" src/util.ts 'export const u = 1;'
1525
+ reset_fakes; export FAKE_VITEST_SLEEP=5
1526
+ run_gate "$R_CIPUSH" gate
1527
+ want_rc 0 "D20e: push-in-a-run-step is not a CI trigger — budget stays unarmed"
1528
+ want_no_out "D20e: no deferral without a declared trigger" 'AFFECTED_GATE_BUDGET_DEFERRED'
1529
+ want_field "D20e: ci field is none" ci 'none'
1530
+ want_out "D20e: the no-CI disregard is disclosed" 'no CI test workflow detected'
1531
+
1532
+ # D20f [trigger anchor, positive]: flow-style `on: [push]` IS a declaration.
1533
+ R_CIFLOW="$(mk_repo ci-flow)"; seed_js "$R_CIFLOW"
1534
+ mkdir -p "$R_CIFLOW/.github/workflows"
1535
+ cat > "$R_CIFLOW/.github/workflows/ci.yml" <<'YML'
1536
+ name: ci
1537
+ on: [push, workflow_dispatch]
1538
+ jobs:
1539
+ test:
1540
+ runs-on: ubuntu-latest
1541
+ steps:
1542
+ - run: npm test
1543
+ YML
1544
+ write_config "$R_CIFLOW" '{"budget_seconds": 1}'
1545
+ seal_base "$R_CIFLOW"
1546
+ commit_chg "$R_CIFLOW" src/util.ts 'export const u = 1;'
1547
+ reset_fakes; export FAKE_VITEST_SLEEP=5
1548
+ run_gate "$R_CIFLOW" gate
1549
+ want_rc 6 "D20f: flow-style on: [push] detects CI and defers"
1550
+ want_out "D20f: deferral marker" 'AFFECTED_GATE_BUDGET_DEFERRED'
1551
+ want_field "D20f: ci field names the workflow" ci '.*\.yml'
1552
+
1553
+ # D20g [trigger anchor, positive]: block-map `push:` key inside the `on:` block
1554
+ # — and a block-sequence `- pull_request` entry — both declare triggers.
1555
+ R_CIBLK="$(mk_repo ci-block)"; seed_js "$R_CIBLK"
1556
+ mkdir -p "$R_CIBLK/.github/workflows"
1557
+ cat > "$R_CIBLK/.github/workflows/ci.yml" <<'YML'
1558
+ name: ci
1559
+ on:
1560
+ push:
1561
+ branches: [main]
1562
+ jobs:
1563
+ test:
1564
+ runs-on: ubuntu-latest
1565
+ steps:
1566
+ - run: npm test
1567
+ YML
1568
+ write_config "$R_CIBLK" '{"budget_seconds": 1}'
1569
+ seal_base "$R_CIBLK"
1570
+ commit_chg "$R_CIBLK" src/util.ts 'export const u = 1;'
1571
+ reset_fakes; export FAKE_VITEST_SLEEP=5
1572
+ run_gate "$R_CIBLK" gate
1573
+ want_rc 6 "D20g: block-map push: trigger detects CI and defers"
1574
+ want_out "D20g: deferral marker" 'AFFECTED_GATE_BUDGET_DEFERRED'
1575
+
1576
+ R_CISEQ="$(mk_repo ci-seq)"; seed_js "$R_CISEQ"
1577
+ mkdir -p "$R_CISEQ/.github/workflows"
1578
+ cat > "$R_CISEQ/.github/workflows/ci.yml" <<'YML'
1579
+ name: ci
1580
+ on:
1581
+ - pull_request
1582
+ jobs:
1583
+ test:
1584
+ runs-on: ubuntu-latest
1585
+ steps:
1586
+ - run: npm test
1587
+ YML
1588
+ write_config "$R_CISEQ" '{"budget_seconds": 1}'
1589
+ seal_base "$R_CISEQ"
1590
+ commit_chg "$R_CISEQ" src/util.ts 'export const u = 1;'
1591
+ reset_fakes; export FAKE_VITEST_SLEEP=5
1592
+ run_gate "$R_CISEQ" gate
1593
+ want_rc 6 "D20h: block-sequence - pull_request trigger detects CI and defers"
1594
+ want_out "D20h: deferral marker" 'AFFECTED_GATE_BUDGET_DEFERRED'
1595
+
1596
+ # ============================ --full + G1-12 ===================================
1597
+
1598
+ # D21: --full on an adapter repo bypasses selection.
1599
+ reset_fakes; run_gate "$R_CONTROL" gate --full
1600
+ want_rc 0 "D21: --full on the vitest fixture exits 0"
1601
+ want_field "D21: mode=full" mode 'full'
1602
+ want_no_call "D21: --full never runs the related selector" 'related'
1603
+ # Bare tool-name legs are satisfied by the --version toolchain probe — pin the
1604
+ # run argv shape (the ledger is `name<TAB>argv`).
1605
+ want_call "D21: the full command ran" 'vitest run |npm test|pnpm test|yarn test|bun test'
1606
+
1607
+ # D22 [G1-12 must-PASS]: pnpm workspace, vitest config in a subdirectory,
1608
+ # non-default base branch (trunk, resolved via origin/HEAD).
1609
+ R_PNPM="$(mk_repo pnpm-ws trunk)"
1610
+ mkdir -p "$R_PNPM/packages/web/src" "$R_PNPM/node_modules/.bin"
1611
+ printf 'packages:\n - packages/*\n' > "$R_PNPM/pnpm-workspace.yaml"
1612
+ cat > "$R_PNPM/package.json" <<'JSON'
1613
+ { "name": "fixture-pnpm", "private": true, "scripts": { "test": "vitest run" } }
1614
+ JSON
1615
+ cat > "$R_PNPM/packages/web/package.json" <<'JSON'
1616
+ { "name": "@fx/web", "private": true, "scripts": { "test": "vitest run" } }
1617
+ JSON
1618
+ printf 'export default { test: {} };\n' > "$R_PNPM/packages/web/vitest.config.ts"
1619
+ printf 'export const w = 1;\n' > "$R_PNPM/packages/web/src/w.ts"
1620
+ printf 'test("w", () => {});\n' > "$R_PNPM/packages/web/src/w.test.ts"
1621
+ printf 'node_modules/\n' > "$R_PNPM/.gitignore"
1622
+ ln -sf "$BIN/vitest" "$R_PNPM/node_modules/.bin/vitest"
1623
+ seal_base "$R_PNPM"
1624
+ commit_chg "$R_PNPM" packages/web/src/util.ts 'export const u = 1;'
1625
+ reset_fakes; run_gate "$R_PNPM" gate
1626
+ want_rc 0 "D22: pnpm-workspace / subdir-config / trunk-base fixture exits 0"
1627
+ # A workspace may legitimately run the package's own test command rather than
1628
+ # `vitest related`; what is NOT acceptable is a green run with nothing invoked
1629
+ # or only a --version probe — pin the invocation shape, not the tool name.
1630
+ want_call "D22: a runner executed (related selection or the package test command)" 'vitest (run|related)|jest .*findRelatedTests|npm test|pnpm test|yarn test|bun test'
1631
+
1632
+ # F1–F3 [pm arms]: the lockfile-detected package manager supplies the exec
1633
+ # prefix when node_modules/.bin is absent — pnpm-lock.yaml → `pnpm exec`,
1634
+ # yarn.lock → `yarn <tool>`, bun.lock → `npx --no-install` (the no-`bun x`
1635
+ # rule: bunx auto-installs registry content, so it is never an exec prefix).
1636
+ # Assert the prefix on the RUN argv — the --version probe shares the prefix.
1637
+ seed_pm_repo() { # <name> <lockfile> — vitest stack, no .bin, pm lock committed
1638
+ local r; r="$(mk_repo "$1")"; seed_js_nobin "$r"
1639
+ printf '# fixture lock\n' > "$r/$2"
1640
+ seal_base "$r"; commit_chg "$r" src/util.ts 'export const u = 1;'
1641
+ printf '%s' "$r"
1642
+ }
1643
+ R_FPNPM="$(seed_pm_repo pm-pnpm pnpm-lock.yaml)"
1644
+ reset_fakes; run_gate "$R_FPNPM" gate
1645
+ want_rc 0 "F1: pnpm-lock fixture exits 0"
1646
+ want_call "F1: pnpm exec prefix on the related run" 'pnpm exec vitest related'
1647
+ want_call "F1: vitest ran through the prefix" 'vitest related'
1648
+
1649
+ R_FYARN="$(seed_pm_repo pm-yarn yarn.lock)"
1650
+ reset_fakes; run_gate "$R_FYARN" gate
1651
+ want_rc 0 "F2: yarn.lock fixture exits 0"
1652
+ want_call "F2: yarn prefix on the related run" 'yarn vitest related'
1653
+ want_call "F2: vitest ran through the prefix" 'vitest related'
1654
+
1655
+ R_FBUN="$(seed_pm_repo pm-bun bun.lock)"
1656
+ reset_fakes; run_gate "$R_FBUN" gate
1657
+ want_rc 0 "F3: bun.lock fixture exits 0"
1658
+ want_call "F3: bun resolves via npx --no-install, never bun x" 'npx --no-install vitest related'
1659
+ want_no_call "F3: bun x is never an exec prefix" 'bun x|bunx '
1660
+
1661
+ # ============================ env-hygiene + aux modes ==========================
1662
+
1663
+ # E1: forged budget env must NOT launder a deferral. A caller exporting
1664
+ # AFFECTED_BUDGET_ACTIVE=1 + a past AFFECTED_DEADLINE would make every bound
1665
+ # expire instantly on the OLD code (`${VAR:-0}` inherited env); the gate now
1666
+ # initialises both unconditionally and requires a detected CI for rc 6.
1667
+ R_FORGE="$(mk_repo env-forge)"; seed_js "$R_FORGE"; seal_base "$R_FORGE"
1668
+ commit_chg "$R_FORGE" src/util.ts 'export const u = 1;'
1669
+ reset_fakes
1670
+ ( cd "$R_FORGE" && env -u CLAUDE_PLUGIN_ROOT \
1671
+ AFFECTED_BUDGET_ACTIVE=1 AFFECTED_DEADLINE=1 \
1672
+ PATH="$BIN:$BASEPATH" FAKE_LOG="$CALLS" FAKE_DIR="$TESTROOT" \
1673
+ REAL_NODE="$REAL_NODE" REAL_PY="$REAL_PY" \
1674
+ bash "$SUT" ) > "$TESTROOT/outs/forge.out" 2>&1
1675
+ RC=$?; OUT="$(cat "$TESTROOT/outs/forge.out")"
1676
+ printf 'gate\t%s\t%s\n' "$RC" "$TESTROOT/outs/forge.out" >> "$RUNS"
1677
+ want_rc 0 "E1: forged budget env does not defer"
1678
+ want_no_out "E1: no deferral marker" 'AFFECTED_GATE_BUDGET_DEFERRED'
1679
+ want_call "E1: the run actually executed" 'vitest (run|related)'
1680
+
1681
+ # E2: --self-check outside a git repo names the real cause, not no-base-ref.
1682
+ reset_fakes
1683
+ mkdir -p "$TESTROOT/not-a-repo"
1684
+ ( cd "$TESTROOT/not-a-repo" && env -u CLAUDE_PLUGIN_ROOT \
1685
+ PATH="$BIN:$BASEPATH" FAKE_LOG="$CALLS" FAKE_DIR="$TESTROOT" \
1686
+ bash "$SUT" --self-check ) > "$TESTROOT/outs/norepo.out" 2>&1
1687
+ RC=$?; OUT="$(cat "$TESTROOT/outs/norepo.out")"
1688
+ printf 'selfcheck\t%s\t%s\n' "$RC" "$TESTROOT/outs/norepo.out" >> "$RUNS"
1689
+ want_rc 4 "E2: --self-check outside a repo refuses"
1690
+ want_out "E2: reason is not-a-repo" 'reason=not-a-repo'
1691
+
1692
+ # E3: --self-check downgrades `ok` when a detected stack's toolchain is missing
1693
+ # — a py repo on a PATH without pytest/python3 must not self-check green.
1694
+ TLBIN="$TESTROOT/bin-toolless"; mkdir -p "$TLBIN"
1695
+ for _t in git bash env grep find sed awk sort tr cut head tail cat date sleep \
1696
+ ps getconf sysctl mktemp rm wc dirname basename uname comm ls mkdir \
1697
+ chmod touch printf od jq node vitest jest nx turbo npm npx pnpm yarn bun; do
1698
+ _p="$(command -v "$_t" 2>/dev/null || true)"
1699
+ [[ -n "$_p" ]] && ln -sf "$_p" "$TLBIN/$_t"
1700
+ done
1701
+ R_SC_MISS="$(mk_repo selfcheck-missing)"; seed_python "$R_SC_MISS"; seal_base "$R_SC_MISS"
1702
+ reset_fakes
1703
+ run_gate_path "$TLBIN" "$R_SC_MISS" selfcheck --self-check
1704
+ want_rc 4 "E3: missing toolchain self-check refuses"
1705
+ want_out "E3: marker says toolchain-missing" 'AFFECTED_GATE_SELF_CHECK toolchain-missing|reason=toolchain-missing'
1706
+
1707
+ # E4: --help emits mode=help (contract vocabulary), not mode=none.
1708
+ reset_fakes; run_gate "$R_CONTROL" gate --help
1709
+ want_rc 0 "E4: --help exits 0"
1710
+ want_field "E4: mode is help" mode 'help'
1711
+
1712
+ # ============================ portability (source scan) ========================
1713
+ # The contract bans specific spellings in the gate source; scanned over the SUT and
1714
+ # every adapter lib it ships, with comment-only lines excluded.
1715
+
1716
+ _PORT_FILES=("$SUT")
1717
+ for _f in "$DIR"/lib/affected-tests/*.sh; do [[ -f "$_f" ]] && _PORT_FILES+=("$_f"); done
1718
+ port_hits=0
1719
+ # Banned spellings only: the self-match proc forms and the GNU-only flag shapes.
1720
+ # `timeout`/`nproc` are deliberately NOT banned — they are legitimate rungs of the
1721
+ # mandated affected_timeout / affected_ncpu chains; what the contract forbids is a
1722
+ # bare DEPENDENCY on them, which the helper-name assertions below pin instead.
1723
+ _BANNED='(pgrep|pkill)[[:space:]]+-[a-zA-Z]*f|ps[[:space:]][^|]*\|[[:space:]]*e?grep|sed[[:space:]][^|#]*-i([[:space:]]|$)|date[[:space:]]+-[a-zA-Z]*d|readlink[[:space:]]+-f|stat[[:space:]]+-[a-zA-Z]*c'
1724
+ for _f in "${_PORT_FILES[@]}"; do
1725
+ _hits="$(grep -avE '^[[:space:]]*#' "$_f" | grep -anE "$_BANNED" || true)"
1726
+ if [[ -n "$_hits" ]]; then port_hits=$((port_hits + 1)); fail "D23: portability violation in $_f: $(head -n 3 <<<"$_hits" | tr '\n' '|')"; fi
1727
+ done
1728
+ [[ "$port_hits" == "0" ]] && pass "D23: no banned spellings (self-match proc scans, GNU-only flags) in gate sources"
1729
+ # The mandated portability helpers exist under their contractual names
1730
+ # (common.sh: affected_ncpu, affected_running_gates, affected_timeout, affected_emit).
1731
+ _allsrc="$(cat "${_PORT_FILES[@]}" 2>/dev/null | tr -d "\0")"
1732
+ for _h in affected_ncpu affected_running_gates affected_timeout; do
1733
+ if grep -aq "$_h" <<<"$_allsrc"; then pass "D23b: helper $_h present"
1734
+ else fail "D23b: contractual helper $_h missing from gate sources"; fi
1735
+ done
1736
+ # finish() is the contractual single exit site; the emit side may be a named
1737
+ # helper or inline emission inside finish() — require finish() plus an emit path.
1738
+ if grep -aq 'finish()' <<<"$_allsrc" && grep -aqi 'emit' <<<"$_allsrc"; then
1739
+ pass "D23b: finish() exit site and emit path present"
1740
+ else
1741
+ fail "D23b: no finish() exit site or emit path found in gate sources"
1742
+ fi
1743
+ _head="$(sed -n '1,120p' "$SUT")"
1744
+ if grep -qw 'git' <<<"$_head" && grep -qE 'getconf|sysctl' <<<"$_head" && grep -qE 'timeout|gtimeout' <<<"$_head"; then
1745
+ pass "D23c: the script header lists its external binaries"
1746
+ else
1747
+ fail "D23c: header does not enumerate the external binaries it invokes"
1748
+ fi
1749
+ # Every `go list` invocation in the go adapter runs under a bound — an
1750
+ # unbounded list can park the gate on module resolution (network fetch).
1751
+ _golist_unb="$(grep -anE 'list -e' "$DIR/lib/affected-tests/go.sh" \
1752
+ | grep -av 'affected_timeout\|affected_bound' || true)"
1753
+ if [[ -z "$_golist_unb" ]]; then
1754
+ pass "D23d: every go list call is bounded (affected_timeout/affected_bound)"
1755
+ else
1756
+ fail "D23d: unbounded go list call in go.sh: $(head -n 3 <<<"$_golist_unb" | tr '\n' '|')"
1757
+ fi
1758
+ unset _f _hits _head _PORT_FILES port_hits _BANNED _allsrc _h _golist_unb
1759
+
1760
+ # ============================ cross-run contract ===============================
1761
+ # G1-10: every run path emits the marker — a second exit site that fires before the
1762
+ # emit leaves a run with no AFFECTED_GATE line, and these loops catch it.
1763
+ # ALL recorded kinds are swept (gate AND explain AND selfcheck) — explain and
1764
+ # self-check are exit paths too and must not slip a marker-less finish.
1765
+ _xfail=0
1766
+ while IFS=$'\t' read -r _kind _rc _of; do
1767
+ if ! grep -qE 'AFFECTED_GATE ' "$_of"; then
1768
+ _xfail=$((_xfail + 1)); fail "X1: run kind=$_kind rc=$_rc emitted no AFFECTED_GATE marker: $_of"
1769
+ fi
1770
+ done < "$RUNS"
1771
+ [[ "$_xfail" == "0" ]] && pass "X1: every run (gate/explain/selfcheck) emitted the AFFECTED_GATE marker (single finish() exit site)"
1772
+ _badword=0
1773
+ while IFS=$'\t' read -r _kind _rc _of; do
1774
+ if grep -qiE 'tests verified' "$_of"; then
1775
+ _badword=$((_badword + 1)); fail "X2: 'tests verified' wording emitted: $_of"
1776
+ fi
1777
+ done < "$RUNS"
1778
+ [[ "$_badword" == "0" ]] && pass "X2: no run ever says 'tests verified'"
1779
+ # G1-11: "the runner ran" verdicts read the invocation ledger, never exit codes
1780
+ # alone — prove the check itself bites on an empty ledger.
1781
+ : > "$TESTROOT/empty-ledger.log"
1782
+ if log_nonempty "$TESTROOT/empty-ledger.log"; then
1783
+ fail "X3: log_nonempty passed an empty ledger — the Guard-1-11 ledger check is blind"
1784
+ else
1785
+ pass "X3: log_nonempty refuses an empty ledger"
1786
+ fi
1787
+ printf 'x\n' > "$TESTROOT/nonempty-ledger.log"
1788
+ if log_nonempty "$TESTROOT/nonempty-ledger.log"; then
1789
+ pass "X3b: log_nonempty accepts a populated ledger"
1790
+ else
1791
+ fail "X3b: log_nonempty refuses a populated ledger"
1792
+ fi
1793
+ unset _kind _rc _of _xfail _badword
1794
+
1795
+ print_results "${AFFECTED_TESTS_MIN_ASSERTIONS:-60}"
1796
+
1797
+