@soleur/opencode 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (812) 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 +659 -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 +108 -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 +342 -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 +152 -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 +493 -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 +363 -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 +520 -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 +113 -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 +178 -0
  247. package/scripts/sync-opencode-adapter.ts +479 -0
  248. package/scripts/sync-pr-behind.sh +534 -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 +65 -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 +142 -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 +118 -0
  493. package/skills/fix-issue/references/exclude-label-jq-snippet.md +96 -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 +668 -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 +325 -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 +138 -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 +1621 -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-todo-structure.md +165 -0
  674. package/skills/review/references/risk-tier-and-fix-rounds.md +95 -0
  675. package/skills/review/references/semgrep-custom-rules.yaml +174 -0
  676. package/skills/review/references/ultrathink-phases.md +98 -0
  677. package/skills/review/references/wfs.md +6 -0
  678. package/skills/review/scripts/emit-review-trailer.sh +499 -0
  679. package/skills/review/scripts/ensure-semgrep.sh +99 -0
  680. package/skills/review/scripts/fix-round-seats.sh +197 -0
  681. package/skills/review/test/emit-review-trailer.test.sh +387 -0
  682. package/skills/review/test/ensure-semgrep.test.sh +111 -0
  683. package/skills/review/test/fix-round-seats.test.sh +237 -0
  684. package/skills/review/workflows/README.md +147 -0
  685. package/skills/review/workflows/review.workflow.js +667 -0
  686. package/skills/schedule/SKILL.md +796 -0
  687. package/skills/seo-aeo/SKILL.md +139 -0
  688. package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
  689. package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
  690. package/skills/ship/SKILL.md +3022 -0
  691. package/skills/ship/references/adr-ordinal-collision.md +47 -0
  692. package/skills/ship/references/affected-gate-contract.md +115 -0
  693. package/skills/ship/references/ci-run-diagnosis.md +29 -0
  694. package/skills/ship/references/ci-workflow-authoring.md +68 -0
  695. package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
  696. package/skills/ship/references/cto-ruling-armed-push.md +5 -0
  697. package/skills/ship/references/decision-challenges-render.md +71 -0
  698. package/skills/ship/references/detached-battery-verdict.md +13 -0
  699. package/skills/ship/references/feature-tweet-draft.md +42 -0
  700. package/skills/ship/references/followthrough-issue-body-template.md +47 -0
  701. package/skills/ship/references/followthrough-stub-template.sh +92 -0
  702. package/skills/ship/references/merge-queue-dequeue.md +39 -0
  703. package/skills/ship/references/operator-holds.md +149 -0
  704. package/skills/ship/references/pipeline-tally-render.md +43 -0
  705. package/skills/ship/references/pr-overlap.md +153 -0
  706. package/skills/ship/references/queue-mode.md +7 -0
  707. package/skills/ship/references/ready-run-wait.md +36 -0
  708. package/skills/ship/references/red-on-main-quarantine.md +110 -0
  709. package/skills/ship/references/register-update-pr-pattern.md +88 -0
  710. package/skills/ship/references/settle-then-admin-merge.md +130 -0
  711. package/skills/ship/references/supabase-column-probe.md +13 -0
  712. package/skills/ship/scripts/auto-close-scan.sh +76 -0
  713. package/skills/ship/scripts/battery-owed.sh +403 -0
  714. package/skills/ship/scripts/hold-gate-eval.sh +410 -0
  715. package/skills/ship/scripts/net-issue-flow.sh +665 -0
  716. package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
  717. package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
  718. package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
  719. package/skills/skill-creator/SKILL.md +299 -0
  720. package/skills/skill-creator/references/api-security.md +248 -0
  721. package/skills/skill-creator/references/authoring-levers.md +110 -0
  722. package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
  723. package/skills/skill-creator/references/best-practices.md +431 -0
  724. package/skills/skill-creator/references/common-patterns.md +639 -0
  725. package/skills/skill-creator/references/core-principles.md +193 -0
  726. package/skills/skill-creator/references/executable-code.md +188 -0
  727. package/skills/skill-creator/references/iteration-and-testing.md +509 -0
  728. package/skills/skill-creator/references/official-spec.md +193 -0
  729. package/skills/skill-creator/references/recommended-structure.md +180 -0
  730. package/skills/skill-creator/references/skill-structure.md +396 -0
  731. package/skills/skill-creator/references/using-scripts.md +122 -0
  732. package/skills/skill-creator/references/using-templates.md +120 -0
  733. package/skills/skill-creator/references/workflows-and-validation.md +541 -0
  734. package/skills/skill-creator/scripts/init_skill.py +322 -0
  735. package/skills/skill-creator/scripts/package_skill.py +110 -0
  736. package/skills/skill-creator/scripts/quick_validate.py +64 -0
  737. package/skills/skill-creator/templates/router-skill.md +73 -0
  738. package/skills/skill-creator/templates/simple-skill.md +34 -0
  739. package/skills/skill-creator/workflows/add-reference.md +96 -0
  740. package/skills/skill-creator/workflows/add-script.md +104 -0
  741. package/skills/skill-creator/workflows/add-template.md +83 -0
  742. package/skills/skill-creator/workflows/add-workflow.md +128 -0
  743. package/skills/skill-creator/workflows/audit-skill.md +159 -0
  744. package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
  745. package/skills/skill-creator/workflows/create-new-skill.md +207 -0
  746. package/skills/skill-creator/workflows/get-guidance.md +134 -0
  747. package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
  748. package/skills/skill-creator/workflows/verify-skill.md +226 -0
  749. package/skills/skill-security-scan/SKILL.md +145 -0
  750. package/skills/skill-security-scan/references/disclaimer.md +21 -0
  751. package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
  752. package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
  753. package/skills/skill-security-scan/references/override-mechanism.md +171 -0
  754. package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
  755. package/skills/skill-security-scan/references/regex-patterns.md +136 -0
  756. package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
  757. package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
  758. package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
  759. package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
  760. package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
  761. package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
  762. package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
  763. package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
  764. package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
  765. package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
  766. package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
  767. package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
  768. package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
  769. package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
  770. package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
  771. package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
  772. package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
  773. package/skills/skill-security-scan/scripts/lib.sh +197 -0
  774. package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
  775. package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
  776. package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
  777. package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
  778. package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
  779. package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
  780. package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
  781. package/skills/social-distribute/SKILL.md +485 -0
  782. package/skills/spec-templates/SKILL.md +274 -0
  783. package/skills/sync/SKILL.md +49 -0
  784. package/skills/test-browser/SKILL.md +464 -0
  785. package/skills/test-browser/references/feature-map.md +161 -0
  786. package/skills/test-fix-loop/SKILL.md +159 -0
  787. package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
  788. package/skills/triage/SKILL.md +402 -0
  789. package/skills/trigger-cron/SKILL.md +136 -0
  790. package/skills/trigger-cron/scripts/trigger.sh +198 -0
  791. package/skills/user-set-role/SKILL.md +147 -0
  792. package/skills/user-set-role/scripts/set-role.sh +218 -0
  793. package/skills/user-story-writer/SKILL.md +77 -0
  794. package/skills/ux-audit/SKILL.md +251 -0
  795. package/skills/ux-audit/references/finding.schema.json +62 -0
  796. package/skills/ux-audit/references/route-list.yaml +95 -0
  797. package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
  798. package/skills/ux-audit/scripts/bot-signin.ts +135 -0
  799. package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
  800. package/skills/work/SKILL.md +1479 -0
  801. package/skills/work/references/end-of-work-resume.md +7 -0
  802. package/skills/work/references/hook-test-main-cwd.md +24 -0
  803. package/skills/work/references/key-principles.md +83 -0
  804. package/skills/work/references/property-testing.md +122 -0
  805. package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
  806. package/skills/work/references/work-agent-teams.md +97 -0
  807. package/skills/work/references/work-lifecycle-parallel.md +147 -0
  808. package/skills/work/references/work-lockfile-bumps.md +97 -0
  809. package/skills/work/references/work-scratch-sandboxes.md +212 -0
  810. package/skills/work/references/work-subagent-fanout.md +61 -0
  811. package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
  812. package/skills/xcode-test/SKILL.md +360 -0
@@ -0,0 +1,1553 @@
1
+ ---
2
+ name: preflight
3
+ description: "This skill should be used when running pre-ship checks on migrations, security headers, and lockfiles."
4
+ ---
5
+
6
+ <!-- soleur-cloud-mode:start -->
7
+ **Cloud Mode (Devin):** before pipeline work run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/cloud-detect.sh"`. If `CLAUDE_PLUGIN_ROOT` is unset (cloud exec shells do not export it), resolve the root by IDENTITY, never by script basename: for `d` in `"$HOME/.local/share/devin/cli/plugins/cache"` and `/opt/.devin/plugins`, skip unless `[ -d "$d" ]`, then `MANIFEST="$(find "$d" -path '*/.claude-plugin/plugin.json' -exec grep -l '"name"[[:space:]]*:[[:space:]]*"soleur"' {} + 2>/dev/null | head -1)"`, `ROOT="${MANIFEST%/.claude-plugin/plugin.json}"` — one resolution, two consumers: `$ROOT/scripts/cloud-detect.sh` and `$ROOT/scripts/precommit-guard.sh`. (Shape check, not authentication: a planted `{"name":"soleur"}` dir passes — ADR-179 A11.) `local` or `not-local:no-devin-env` proceeds normally; any other `not-local:<reason>` applies `<plugin-root>/devin/INSTRUCTIONS.md` §Cloud Mode: emit the `--banner`, fan out sequentially with `Reviewed-Coverage: sequential-fallback` disclosure (never claim an independent review ran), `message_user` ack before any secrets read or production mutation, and run `precommit-guard.sh` before any `git commit` — hooks do not fire in cloud.
8
+ <!-- soleur-cloud-mode:end -->
9
+
10
+ <!-- grok-harness-invoke:start -->
11
+ **Grok Build (`plugins/soleur/lib/harness.ts` `invokeSkill()`):** Read this SKILL.md in this process and run it to completion. A one-segment `soleur:<name>` in this document names a SKILL — on Grok Build, Read `plugins/soleur/skills/<name>/SKILL.md` in this process; it is not a nested tool_use. A multi-segment id such as `soleur:<domain>:<name>` names an AGENT: spawn it, never Read it, and on Grok Build spawn_subagent takes the id with its colons replaced by hyphens (`agentIdToGrokSubagentType`). **Claude Code:** Skill tool for a skill (`soleur:<name>`), Task tool with `subagent_type` for an agent. Forbidden is executing a subset, not the Read.
12
+ <!-- grok-harness-invoke:end -->
13
+
14
+ # preflight Skill
15
+
16
+ **Purpose:** Validate technical readiness of code changes before a PR is created, catching the class of bugs that only appear in production context -- unapplied database migrations, CSP violations from injected scripts, and bare-repo stale file reads.
17
+
18
+ **CRITICAL: No command substitution.** Never use `$()` in Bash commands. When a step says "get value X, then use it in command Y", run them as **two separate Bash tool calls** -- first get the value, then use it literally in the next call. The one exception is a fenced block that says it is run as ONE Bash call (Step 1.4): its `$()` fetches a credential into a variable that the same block checks and then feeds to curl's stdin config, because a literal value pasted into a later call would sit in the transcript.
19
+
20
+ <!-- operator-typed-render:start -->
21
+ **Any message this skill PRINTS that tells the operator to run a skill or command renders at emit time.** The doc names it canonically (`soleur:<name>`, ADR-226); before printing, render it as the active harness's **operator-typed form** per `formatSkillInvocation` (`plugins/soleur/lib/harness.ts`), which owns the per-harness slash and sigil forms — the operator types that string into a fresh session where no routing contract is in context, so a bare canonical name is model-discretion there rather than a dispatch. This covers abort messages, `AskUserQuestion` prompts and options, `Display`/`echo` lines and resume prompts alike; an agent-read instruction stays canonical.
22
+ <!-- operator-typed-render:end -->
23
+
24
+ ## Headless Mode Detection
25
+
26
+ If `$ARGUMENTS` contains `--headless`, set `HEADLESS_MODE=true`. Strip `--headless` from `$ARGUMENTS` before processing remaining args.
27
+
28
+ When `HEADLESS_MODE=true`:
29
+
30
+ - On any FAIL: abort with error details, no prompt
31
+ - On all PASS/SKIP: continue silently. **Two exceptions, both Check 10 only, both always emitted:** **SKIP-DECLARED** — continue, but emit one line naming the declared credential scope (a verification waiver exists to be reviewable, so silencing it defeats it); and **SKIP-NOSANDBOX** — continue, but emit one line stating that Check 10 did not run at all on this host. The second is the more important of the two: it means a security-relevant gate is dark, and folded into the silent set it is indistinguishable from the gate having nothing to do.
32
+
33
+ **Check 13 headless contract.** Check 13 never prompts in headless mode, and headless is the default: treat the run as interactive only when the founder is present in this session (started without `--headless`, `CI` unset); when unsure, headless. A founder-stated check that FAILED, is INVALID, CHANGED-SINCE-APPROVAL, UNTRUSTED, BLOCK-REJECTED or `needs-your-eyes`, or that cannot run for want of a sandbox, is a FAIL recorded as `STOPPED-AWAITING-FOUNDER`; an agent never accepts, overrides or confirms on the founder's behalf. Always emitted, like Check 10's: the no-block banner and the no-sandbox line (`founder-check.py text no-block` / `text no-sandbox`).
34
+
35
+ **`--founder-check-baseline`.** If `$ARGUMENTS` contains `--founder-check-baseline`, strip it like `--headless`, **skip Checks 1–12 explicitly**, and run only Check 13 in baseline polarity (reference section 7). Invoked by `soleur:plan` at capture, before the freeze commit.
36
+
37
+ ## Phase 0: Context Detection
38
+
39
+ Run `git rev-parse --abbrev-ref HEAD` to get the current branch name.
40
+
41
+ **Branch safety check (defense-in-depth):** If the branch is `main` or `master`, abort immediately with: "Error: preflight cannot run on main/master. Checkout a feature branch first."
42
+
43
+ ### Step 0.1: Compute changed-file path-set (diff classifier)
44
+
45
+ Run `git diff --name-only origin/main...HEAD` ONCE up-front and cache the result so each path-gated check re-uses the same path-set instead of re-running diff. Skill convention forbids `$()` command substitution — write the diff to a tmpfile so checks can `grep -E` the predicate they care about.
46
+
47
+ Use `git rev-parse --git-dir` to resolve a writable tmp path that works in both regular checkouts and worktrees: in a worktree `.git` is a file (gitdir pointer), not a directory, so `> .git/<filename>` fails with `Not a directory (os error 20)`. The resolver returns the worktree's actual gitdir (e.g., `<bare>/worktrees/<name>/`):
48
+
49
+ ```bash
50
+ PREFLIGHT_TMP="$(git rev-parse --git-dir)"
51
+ git diff --name-only origin/main...HEAD > "$PREFLIGHT_TMP/preflight-diff-files.txt"
52
+ ```
53
+
54
+ All downstream `cat .git/<filename>` references in the predicates below must use `"$PREFLIGHT_TMP/<filename>"` instead. The pre-existing `.git/...` literals worked in non-worktree checkouts but silently broke in worktrees (where every preflight increasingly happens by default).
55
+
56
+ If the command fails (e.g., offline, no remote), every path-gated check falls back to its existing `git diff` form — operators do not need to handle this case explicitly. Between Phase 0 and Phase 1 nothing mutates the working tree or fetches `origin/main`, so the cache is valid for the duration of one preflight invocation. The cache lives under the per-worktree gitdir (user-owned, scoped to one worktree) — this isolates concurrent preflight runs across sibling worktrees and avoids the `/tmp/` symlink-clobber class that affects fixed-name shared paths on multi-user hosts.
57
+
58
+ **Fast-path SKIP overview.** For diffs whose path-set matches a recognized "guaranteed-SKIP" shape, the relevant check returns SKIP at its first step without further work. The predicates below are the existing inner predicates of each check — only the diff source is changed.
59
+
60
+ | Check | Fast-path SKIP predicate (against `"$PREFLIGHT_TMP/preflight-diff-files.txt"`) |
61
+ | --- | --- |
62
+ | 1 (Migrations) | Zero matches for `(^\|/)supabase/migrations/.*\.sql$`. |
63
+ | 2 (Sec headers) | Zero matches for `\.(tsx\|css\|html)$`, `middleware\.ts$`, `next\.config\.`, `\.tf$`, `Dockerfile`, `nginx`, `\.github/workflows/`. |
64
+ | 3 (Lockfiles) | Existing predicate (uses `--name-status`, status letters load-bearing — does NOT use the cached path-set; see Sharp Edges). |
65
+ | 4 (Env isolation) | Always runs (no fast-path SKIP). |
66
+ | 5 (Bundle host) | Zero matches for the listed Supabase client/validator paths, `Dockerfile`, `reusable-release.yml`, or `verify-required-secrets.sh`. |
67
+ | 6 (Brand-survival) | Zero matches for the canonical sensitive-path regex. |
68
+ | 7 (Canary) | `apps/web-platform/infra/ci-deploy.sh` not in path-set. |
69
+ | 8 (SW cache bump) | No `fix(`/`fix:`/`hotfix` commit subject AND zero client-bundle surface matches. |
70
+ | 9 (Node-only encodings) | Always runs (uses `git ls-files`, full-universe scan — does NOT use the cached path-set; see Sharp Edges). |
71
+ | 10 (Discoverability test) | Zero matches for the canonical sensitive-path regex (re-use Check 6 SSOT). |
72
+ | 11 (Register drift) | Zero matches for `(^\|/)apps/web-platform/supabase/migrations/.*\.sql$`, `(^\|/)apps/web-platform/server/workspace-resolver\.ts$`, or `(^\|/)knowledge-base/engineering/architecture/domain-model\.md$` (empty/missing cache → run, never SKIP). |
73
+ | 12 (Encryption posture) | Zero matches for `\.tf$`, `supabase/migrations/.*\.sql$`, `cloud-init.*\.ya?ml$`, `docker-compose.*\.ya?ml$` (empty/missing cache → run, never SKIP). |
74
+ | 13 (Founder check) | Never SKIPs on an empty cache: runs whenever a plan for this branch carries a `founder_check` block or freeze evidence (the resolver, not the diff cache, decides), else prints the no-block banner. |
75
+ | Not-Bare-Repo | Always runs. |
76
+
77
+ For PR #3488-class diffs (lockfile bumps + orphan-cleanup deletions), Checks 1, 2, 5, 6, 7, 8 fast-skip → Checks 3 (lockfile fires), 4 (env isolation always), 9 (always), Not-Bare-Repo (always) execute. Of those four, only Check 3 and Check 9 do "real work" against the diff; Check 4 and Not-Bare-Repo are constant-cost.
78
+
79
+ ## Phase 1: Run All Checks in Parallel
80
+
81
+ Run all checks below (plus the Not-Bare-Repo assertion) as parallel Bash tool calls. Each returns PASS, FAIL, or SKIP.
82
+
83
+ ### Assertion: Not-Bare-Repo
84
+
85
+ This assertion runs first conceptually (fail-fast) but executes in parallel with the checks.
86
+
87
+ ```bash
88
+ git rev-parse --is-bare-repository
89
+ ```
90
+
91
+ - If the result is `true`: **FAIL** -- "Running from bare repo root. Create a worktree first."
92
+ - If the result is `false`: **PASS**
93
+
94
+ ### Shared Plan-File Resolution
95
+
96
+ Both Check 6 (Brand-Survival Self-Review) and Check 10 (Discoverability Test Execution) need the same input: the PR body scrubbed of HTML comments + fenced code blocks, concatenated with the linked plan file (also scrubbed). This sub-section is the single source of truth for that resolution. **Mirrored consumers: Check 6 Step 6.4, Check 10 Step 10.3.** If a future PR changes the scrub/extract logic, edit here once — both consumers pick up the change automatically.
97
+
98
+ ```bash
99
+ # Step S.1: fetch PR body to a private temp file (umask 077; mktemp).
100
+ PR_BODY_FILE=$(umask 077 && mktemp -p "$(mktemp -d -t soleur-run.$$.XXXXXXXX)" preflight-pr-body.XXXXXXXX.md)
101
+ gh pr view --json body --jq .body > "$PR_BODY_FILE"
102
+ trap 'rm -f "$PR_BODY_FILE" "$COMBINED"; rmdir "$(dirname "$PR_BODY_FILE")" 2>/dev/null' EXIT
103
+
104
+ # Step S.2: strip HTML comments and fenced code blocks from PR body.
105
+ SCRUBBED_BODY=$(awk 'BEGIN{f=0} /^[[:space:]]*```/{f=!f; next} !f{print}' "$PR_BODY_FILE" \
106
+ | perl -0777 -pe 's/<!--.*?-->//gs')
107
+
108
+ # Step S.3: extract any linked plan file path (knowledge-base/project/plans/*.md).
109
+ PLAN_PATH=$(printf '%s' "$SCRUBBED_BODY" | grep -Eo 'knowledge-base/project/plans/[^[:space:])"`'\'']+\.md' | head -n 1 || true)
110
+
111
+ # Step S.4: build the combined check input.
112
+ COMBINED=$(mktemp -p "$(dirname "$PR_BODY_FILE")" preflight-combined.XXXXXXXX.md)
113
+ printf '%s\n' "$SCRUBBED_BODY" > "$COMBINED"
114
+ if [[ -n "$PLAN_PATH" && -f "$PLAN_PATH" ]]; then
115
+ awk 'BEGIN{f=0} /^[[:space:]]*```/{f=!f; next} !f{print}' "$PLAN_PATH" \
116
+ | perl -0777 -pe 's/<!--.*?-->//gs' >> "$COMBINED"
117
+ fi
118
+ ```
119
+
120
+ If `gh pr view` fails (no PR exists for the current branch), the caller returns **SKIP** — Shared Plan-File Resolution itself does not decide; it provides `$COMBINED` (and `$PLAN_PATH`) for the caller.
121
+
122
+ ### Check 1: DB Migration Status
123
+
124
+ **Step 1.1: Detect new migration files in this branch.**
125
+
126
+ Re-use the cached path-set from Phase 0 Step 0.1 (`"$PREFLIGHT_TMP/preflight-diff-files.txt"`):
127
+
128
+ ```bash
129
+ grep -E '(^|/)supabase/migrations/.*\.sql$' "$PREFLIGHT_TMP/preflight-diff-files.txt"
130
+ ```
131
+
132
+ The regex anchors are intentional: `(^|/)` accepts both top-level (`supabase/migrations/X.sql`) and nested-under-app-dir paths (`apps/web-platform/supabase/migrations/X.sql`); `.*\.sql$` accepts files at any depth under the migrations directory (matching git pathspec's `*` which crosses `/`). Verify at edit time via `_v=$(mktemp -d -t soleur-run.$$.XXXXXXXX) && a=$(mktemp -p "$_v") && b=$(mktemp -p "$_v") && git diff --name-only origin/main...HEAD -- '*/supabase/migrations/*.sql' supabase/migrations/*.sql > "$a" && grep -E '(^|/)supabase/migrations/.*\.sql$' "$PREFLIGHT_TMP/preflight-diff-files.txt" > "$b" && diff -u "$a" "$b"`.
133
+
134
+ If no migration files are found (grep rc=1), return **SKIP**.
135
+
136
+ **Step 1.1b: Detect documented prd-apply deferral (early SKIP).**
137
+
138
+ If migration files are found, check whether the PR has explicitly
139
+ deferred prd-apply. Two valid signals:
140
+
141
+ ```bash
142
+ # (a) Migration checklist documents prd-apply as pending/deferred.
143
+ ckl=$(git ls-files 'knowledge-base/project/specs/feat-*/migration-checklist.md' | head -1)
144
+ if [[ -n "$ckl" ]] && grep -qiE '^## prd apply\s*[-—]+\s*(pending|deferred|done)' "$ckl"; then
145
+ defer_signal="checklist:$(basename $(dirname "$ckl"))/migration-checklist.md"
146
+ fi
147
+
148
+ # (b) PR body has `Tracks #N` companion to a `prd apply`/`prd migration` line
149
+ # with a labeled deferred-automation issue (re-use the operator-step-gate shape).
150
+ pr_body=$(gh pr view --json body --jq .body 2>/dev/null || true)
151
+ if printf '%s' "$pr_body" | grep -qiE '(prd[-_ ]apply|prd[-_ ]migration).*(Tracks|Refs) #[0-9]+'; then
152
+ defer_signal="pr-body-tracks"
153
+ fi
154
+ ```
155
+
156
+ If `$defer_signal` is non-empty AND the migration-checklist's
157
+ `## prd apply` heading reads **done** (post-apply record), Check 1
158
+ proceeds to Step 1.4 to verify columns actually exist.
159
+
160
+ If `$defer_signal` is non-empty AND the heading reads **pending** or
161
+ **deferred**, return **SKIP** with note: `"prd apply deferred by
162
+ plan/PR — $defer_signal. Check 1 will re-verify post-merge via the
163
+ release workflow's verify-migrations job."` This closes the
164
+ false-positive FAIL class where a plan explicitly sequences the
165
+ prd apply in lockstep with another in-flight PR (e.g., AC-LEGAL-FLIP
166
+ gate). The signal is documented + auditable — operator cannot
167
+ silently skip Check 1 without leaving a paper trail.
168
+
169
+ If `$defer_signal` is empty, fall through to Step 1.2 (the original
170
+ unapplied-migration FAIL path is the correct response).
171
+
172
+ **Why:** PR #4225 (feat-team-workspace-multi-user) — preflight FAIL
173
+ on Check 1 because prd migrations were deferred per
174
+ migration-checklist.md (legal-PR lockstep gate); the headless
175
+ `soleur:ship` halted the pipeline on a known-deferred state. This SKIP
176
+ path honors documented deferrals while keeping the gate active for
177
+ undocumented cases.
178
+
179
+ **Step 1.2: Parse migration SQL for table/column pairs.**
180
+
181
+ For each migration file found in Step 1.1, extract table and column names:
182
+
183
+ ```bash
184
+ grep -iE 'ADD COLUMN|CREATE TABLE' <migration_file>
185
+ ```
186
+
187
+ Parse the output to extract `<table> <column>` pairs. The project uses `ALTER TABLE public.<table> ADD COLUMN IF NOT EXISTS <column> <type>` patterns.
188
+
189
+ **Step 1.3: Check the Supabase credentials exist in Doppler (presence only).**
190
+
191
+ Run these as two separate Bash calls. They print nothing secret: the values are fetched again, inside the one Bash call of Step 1.4, because the Bash tool keeps no shell state between calls.
192
+
193
+ ```bash
194
+ doppler secrets get NEXT_PUBLIC_SUPABASE_URL -p soleur -c prd --plain >/dev/null && echo "NEXT_PUBLIC_SUPABASE_URL present"
195
+ ```
196
+
197
+ ```bash
198
+ doppler secrets get SUPABASE_SERVICE_ROLE_KEY -p soleur -c prd --plain >/dev/null && echo "SUPABASE_SERVICE_ROLE_KEY present"
199
+ ```
200
+
201
+ If either credential is missing, return **SKIP** with note: "Supabase credentials not available in Doppler prd config."
202
+
203
+ **Step 1.4: Verify each column via Supabase REST API.**
204
+
205
+ For each table/column pair extracted in Step 1.2, issue a read-only GET request. Run the whole block as ONE Bash call (it fetches its own credentials; nothing carries over from Step 1.3):
206
+
207
+ ```bash
208
+ SUPABASE_URL="$(doppler secrets get NEXT_PUBLIC_SUPABASE_URL -p soleur -c prd --plain)"
209
+ SERVICE_ROLE_KEY="$(doppler secrets get SUPABASE_SERVICE_ROLE_KEY -p soleur -c prd --plain)"
210
+ # argv shows in /proc/<pid>/cmdline, so the key rides curl's stdin config. Bash only; `bash -x` would print the key.
211
+ (LC_ALL=C; case "$SERVICE_ROLE_KEY" in ''|*[!A-Za-z0-9._~+/=-]*) exit 1;; esac) || { echo "SERVICE_ROLE_KEY unset or not [A-Za-z0-9._~+/=-] (refused, not sent)" >&2; exit 1; }
212
+ curl --disable --proto '=https' -m 30 -fsS --config - "${SUPABASE_URL}/rest/v1/<table>?select=<column>&limit=1" \
213
+ < <(printf 'header = "apikey: %s"\nheader = "Authorization: Bearer %s"\n' "$SERVICE_ROLE_KEY" "$SERVICE_ROLE_KEY")
214
+ ```
215
+
216
+ - A **200 response** (even empty `[]`) confirms the column exists -- migration is applied.
217
+ - A **400 response** with "column does not exist" confirms the migration was **NOT applied**.
218
+
219
+ **IMPORTANT:** Use only GET requests. The service role key has full write access -- never issue POST, PUT, PATCH, or DELETE.
220
+
221
+ **Result:**
222
+
223
+ - **PASS** -- No migrations in PR, or all migrations verified as applied
224
+ - **FAIL** -- Unapplied migration found (column query returned 400)
225
+ - **SKIP** -- No migration files in PR, or credentials unavailable
226
+
227
+ ### Check 2: Security Headers and Parity
228
+
229
+ **Step 2.1: Detect relevant file changes.**
230
+
231
+ Re-use the cached path-set from Phase 0 Step 0.1:
232
+
233
+ ```bash
234
+ cat "$PREFLIGHT_TMP/preflight-diff-files.txt"
235
+ ```
236
+
237
+ Check if any changed files match these patterns: `.tsx`, `.css`, `.html`, `middleware.ts`, `next.config.*`, `.tf`, `Dockerfile`, `nginx*`, `.github/workflows/*`.
238
+
239
+ If no relevant files changed, return **SKIP** (fast-path — no Doppler fetch needed when the diff has no client-bundle, infra, or workflow surface).
240
+
241
+ **Step 2.2: Get production URL from Doppler.**
242
+
243
+ ```bash
244
+ doppler secrets get NEXT_PUBLIC_APP_URL -p soleur -c prd --plain
245
+ ```
246
+
247
+ If no URL is available, return **SKIP** with note: "Production URL not available."
248
+
249
+ **Step 2.3: Fetch response headers.**
250
+
251
+ Fetch headers from the root page (not `/health` -- the health endpoint skips CSP):
252
+
253
+ ```bash
254
+ curl -sI <PRODUCTION_URL>/
255
+ ```
256
+
257
+ **Step 2.4: Validate mandatory security headers.**
258
+
259
+ Check the response against the project's security header policy derived from `apps/web-platform/lib/security-headers.ts`:
260
+
261
+ | Header | Expected Value | Severity if Missing |
262
+ |--------|---------------|-------------------|
263
+ | Content-Security-Policy | Contains `strict-dynamic` + nonce | FAIL |
264
+ | X-Frame-Options | `DENY` | FAIL |
265
+ | X-Content-Type-Options | `nosniff` | FAIL |
266
+ | Strict-Transport-Security | `max-age=63072000; includeSubDomains; preload` | FAIL |
267
+ | Referrer-Policy | `strict-origin-when-cross-origin` | PASS (non-critical) |
268
+ | Permissions-Policy | Contains `camera=(), microphone=()` | PASS (non-critical) |
269
+ | Cross-Origin-Opener-Policy | `same-origin` | PASS (non-critical) |
270
+ | Cross-Origin-Resource-Policy | `same-origin` | PASS (non-critical) |
271
+ | X-DNS-Prefetch-Control | `on` | PASS (non-critical) |
272
+
273
+ **Step 2.5: Validate CSP directive structure.**
274
+
275
+ CSP is generated per-request in middleware.ts with a per-request nonce via `crypto.randomUUID()`. Validate the CSP structure, not the specific nonce value:
276
+
277
+ - Verify `strict-dynamic` is present in `script-src`
278
+ - Verify no standalone `unsafe-inline` without `strict-dynamic` override
279
+ - Parse CSP as directive-level tokens
280
+
281
+ **Notes:**
282
+
283
+ - Cloudflare-injected headers (`cf-ray`, `server: cloudflare`) are expected in production -- ignore them.
284
+ - The `/health` endpoint explicitly skips CSP in middleware -- always fetch `/` for the full header set.
285
+ - **Limitation (v1):** This check validates the current production deployment, not the branch under review. It catches existing header regressions but cannot detect regressions introduced by the current PR until after deployment. Preview deployments would enable pre-merge header validation (deferred to v2).
286
+
287
+ **Result:**
288
+
289
+ - **PASS** -- All critical headers present and valid (CSP, X-Frame-Options, X-Content-Type-Options, HSTS)
290
+ - **FAIL** -- Any critical header missing or invalid
291
+ - **SKIP** -- No relevant file changes or no production URL available
292
+
293
+ ### Check 3: Lockfile Consistency
294
+
295
+ Since ADR-191 this repository carries exactly ONE lockfile per directory
296
+ (`package-lock.json`); `bun.lock` is deleted and [lint-dual-lockfile.sh](../../../../scripts/lint-dual-lockfile.sh) fails if
297
+ one reappears. The sibling-consistency logic below therefore no longer fires here — it is
298
+ retained because this skill also runs against tenant repositories, which may legitimately
299
+ still carry dual lockfiles.
300
+
301
+ **Step 3.1: Detect lockfile modifications in this branch.**
302
+
303
+ ```bash
304
+ git diff --name-status origin/main...HEAD -- '*/bun.lock' '*/package-lock.json' 'bun.lock' 'package-lock.json'
305
+ ```
306
+
307
+ This returns status letters (M=modified, A=added, D=deleted) alongside file paths. If no output, return **SKIP**.
308
+
309
+ Only lockfiles with status **M** (modified) trigger the consistency check. Added (A) or deleted (D) lockfiles are one-time structural changes that do not require sibling updates.
310
+
311
+ **Step 3.2: For each modified lockfile, verify its sibling.**
312
+
313
+ For each lockfile with status M in the Step 3.1 output:
314
+
315
+ 1. Extract the directory path (e.g., `apps/web-platform` from `apps/web-platform/bun.lock`). For root-level lockfiles (`bun.lock` or `package-lock.json` with no path prefix), use the repository root directory.
316
+ 2. Determine the sibling: if the modified file is `bun.lock`, the sibling is `package-lock.json` (and vice versa).
317
+ 3. Check if the sibling exists in the working tree (separate Bash call):
318
+
319
+ ```bash
320
+ test -f <directory>/package-lock.json && echo "exists" || echo "missing"
321
+ ```
322
+
323
+ 4. If the sibling does NOT exist (single-lockfile directory), skip this file -- no consistency check needed.
324
+ 5. If the sibling exists (dual-lockfile directory), check whether the sibling also appears in the Step 3.1 output (any status: M, A, or D). If the sibling is NOT in the diff at all, report **FAIL**: "`<directory>/` modified `bun.lock` but not `package-lock.json`. Both lockfiles must be updated together (see AGENTS.md dual-lockfile rule). Run `npm install` in `<directory>/` to regenerate `package-lock.json`." (If `package-lock.json` was modified without `bun.lock`, say: "Run `bun install` in `<directory>/` to regenerate `bun.lock`.")
325
+
326
+ If multiple files fail, report each one. Any single failure means the overall check result is FAIL.
327
+
328
+ **Result:**
329
+
330
+ - **PASS** -- All modified lockfiles in dual-lockfile directories have consistent sibling updates
331
+ - **FAIL** -- One or more dual-lockfile directories have a modified lockfile without its sibling updated (message names each directory and missing file)
332
+ - **SKIP** -- No lockfile changes in this branch
333
+
334
+ ### Check 4: Environment Isolation
335
+
336
+ **Always runs (no path-pattern gate).** Enforces `hr-dev-prd-distinct-supabase-projects`: dev and prd Doppler configs must resolve to different Supabase project refs.
337
+
338
+ **Step 4.1: Fetch dev and prd Supabase URLs.**
339
+
340
+ Run as separate Bash calls (no command substitution per skill convention):
341
+
342
+ ```bash
343
+ doppler secrets get NEXT_PUBLIC_SUPABASE_URL -p soleur -c dev --plain
344
+ ```
345
+
346
+ ```bash
347
+ doppler secrets get NEXT_PUBLIC_SUPABASE_URL -p soleur -c prd --plain
348
+ ```
349
+
350
+ If either call fails or returns empty, return **SKIP** with note: "Doppler unavailable or NEXT_PUBLIC_SUPABASE_URL unset."
351
+
352
+ **Step 4.2: Resolve project ref for each URL.**
353
+
354
+ The single chokepoint is the canonical-hostname regex `^[a-z0-9]{20}\.supabase\.co$`. Both branches below MUST converge on a hostname matching that regex before Step 4.3 runs. The canonical resolver lives at `apps/web-platform/scripts/lib/supabase-ref-resolver.sh` (`resolve_supabase_ref`); the same shape is mirrored in `apps/web-platform/lib/supabase/resolve-ref.ts` for TS callers and consumed by `.github/workflows/reusable-release.yml` via `source`. The sub-bullets below describe Check 4's wrapper semantics on top of the resolver (e.g., the A-record-only FAIL path is a Check 4 policy, not a resolver behavior).
355
+
356
+ 1. Strip the `https://` prefix and any trailing path; keep the bare host.
357
+ 2. If the bare host already matches the canonical regex, use it directly.
358
+ 3. Otherwise (custom domain), resolve via `dig`:
359
+
360
+ ```bash
361
+ dig +short CNAME <host>
362
+ ```
363
+
364
+ Capture the exit code separately. Strict-mode resilience: do not pipe through `|| true` — that masks SERVFAIL/network errors as "no record" and silently disables the gate. Branch on the captured rc:
365
+
366
+ - `rc == 0` and output non-empty: candidate hostname is the CNAME target (strip trailing dot).
367
+ - `rc == 0` and output empty: no CNAME exists. Fall back to `dig +short A <host>`. If the A-record resolves to a Supabase IP range, **FAIL** with: "Custom domain `<host>` uses A-record-only Supabase routing. Check 4 cannot prove project ref. Configure CNAME-based custom domain or temporarily set Doppler `<config>.NEXT_PUBLIC_SUPABASE_URL` to the bare `<ref>.supabase.co` form for the isolation check." A-records are rare for Supabase custom domains; failing is correct because SKIPping fails-open the security gate.
368
+ - `rc != 0`: SERVFAIL, NXDOMAIN, network error, etc. Return **SKIP** with diagnostic: "dig exit `<rc>` for `<host>` — DNS resolution unavailable; isolation check inconclusive." (SKIP only when the diagnostic is genuinely undetermined; A-record-only is determined and FAILs.)
369
+ - `dig` not installed (the resolver script exits 1 with "'dig' is not installed"): do NOT SKIP — resolve the CNAME with `resolvectl query --type=CNAME <host>` (systemd hosts) and feed the target through the same canonical-regex check; a SKIP here fails the security gate open on a tooling gap, not a DNS answer. Measured on PR #8354 (`api.soleur.ai` → `<ref>.supabase.co` via resolvectl).
370
+ 4. Verify the resulting hostname matches `^[a-z0-9]{20}\.supabase\.co$`. If it does not, **FAIL** with: "Resolved hostname `<host>` is not a canonical Supabase project endpoint. Refusing to compare on a non-canonical name (subdomain-bypass guard)." This catches inputs like `<ref>.supabase.co.evil.com` that pass step 1 but fail the anchored regex.
371
+
372
+ The 20-char first label of a canonical hostname IS the project ref — extract via the literal first label or by stripping `.supabase.co`.
373
+
374
+ **Step 4.3: Compare project refs.**
375
+
376
+ If `dev_ref == prd_ref`, **FAIL** with: "Environment isolation violation: dev and prd resolve to the same Supabase project ref `<ref>`. See issue #2887."
377
+
378
+ Otherwise **PASS**.
379
+
380
+ **Result:**
381
+
382
+ - **PASS** -- dev and prd resolve to distinct project refs
383
+ - **FAIL** -- refs match (single-DB blast radius), hostname is not a canonical Supabase endpoint, or custom domain uses A-record-only routing
384
+ - **SKIP** -- Doppler unavailable, NEXT_PUBLIC_SUPABASE_URL unset in either config, or DNS resolution failed (`dig` rc != 0)
385
+
386
+ ### Check 5: Production Bundle Supabase Host
387
+
388
+ **Path-gated:** only runs when the cached path-set from Phase 0 Step 0.1 (`"$PREFLIGHT_TMP/preflight-diff-files.txt"`) contains any of `apps/web-platform/lib/supabase/client.ts`, `apps/web-platform/lib/supabase/validate-url.ts`, `apps/web-platform/lib/supabase/validate-anon-key.ts`, `apps/web-platform/Dockerfile`, `.github/workflows/reusable-release.yml`, or `apps/web-platform/scripts/verify-required-secrets.sh`. Otherwise return **SKIP** with note: "No build-arg surface changes detected." The path predicate is identical to the original `git diff --name-only origin/main...HEAD` form — only the diff source is changed.
389
+
390
+ **Note:** Complements Check 4. Check 4 enforces Doppler dev/prd isolation; Check 5 covers the GitHub-repo-secrets surface that feeds the prod Docker build (`secrets.NEXT_PUBLIC_SUPABASE_URL` and `secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY` in `reusable-release.yml`). The two sources can drift — see `knowledge-base/project/learnings/bug-fixes/2026-04-28-oauth-supabase-url-test-fixture-leaked-into-prod-build.md` (URL class) and `knowledge-base/project/learnings/bug-fixes/2026-04-28-anon-key-test-fixture-leaked-into-prod-build.md` (anon-key class).
391
+
392
+ **Step 5.1: Discover the candidate chunk set.**
393
+
394
+ Webpack chunking is not stable across releases — the inlined Supabase init may live in the login page chunk, in a numeric shared chunk (`/_next/static/chunks/8237-*.js`), or in a layout chunk. Hardcoding a single path produces SKIP-on-chunking-change, which silently disables the gate (issue #3010). Discover the candidate set dynamically by enumerating every chunk URL the login HTML references — that is the authoritative "what /login loads" surface.
395
+
396
+ Run as separate Bash calls (no command substitution per skill convention). Each block
397
+ re-derives `PREFLIGHT_TMP` — it is `$(git rev-parse --git-dir)`, so it is stable across
398
+ separate Bash calls (which do NOT inherit env) AND distinct per worktree, which is what a
399
+ later call needs to find these artifacts by name without colliding with a sibling session:
400
+
401
+ ```bash
402
+ PREFLIGHT_TMP="$(git rev-parse --git-dir)"
403
+ curl -fsSL --max-time 10 -A "Mozilla/5.0" https://app.soleur.ai/login -o "$PREFLIGHT_TMP/preflight-login.html"
404
+ ```
405
+
406
+ ```bash
407
+ PREFLIGHT_TMP="$(git rev-parse --git-dir)"
408
+ grep -oE '/_next/static/chunks/[^"]+\.js' "$PREFLIGHT_TMP/preflight-login.html" | awk '!seen[$0]++' | head -20 > "$PREFLIGHT_TMP/preflight-candidates.txt"
409
+ ```
410
+
411
+ The cap of 20 is generous (current prod loads 13 chunks); if ever hit on a future release, prefer raising the cap over reverting to the hardcoded login-chunk path. The grep matches both `<script src=...>` and `<link rel=preload href=...>` references — both are valid candidates.
412
+
413
+ If the `curl` fails (rc != 0) or `"$PREFLIGHT_TMP/preflight-candidates.txt"` is empty, return **SKIP** with note: "Could not fetch /login HTML or could not locate any /_next/static/chunks references."
414
+
415
+ **Step 5.2: Probe each candidate chunk for Supabase shapes.**
416
+
417
+ The Supabase host string and the inlined anon-key JWT may live in DIFFERENT chunks (verified 2026-04-29 against current prod: `8237-*.js` carries the JWT but contains zero `supabase.co` host strings). Track `host_union` (every chunk's supabase-host hits) and `jwt_chunk` (the first chunk with a JWT) independently. Always traverse the full candidate list — bail-early would skip chunks that may carry a placeholder-host leak (matrix row 6).
418
+
419
+ This block is operator-executed under the skill's `set -euo pipefail` convention. Failed `curl` per chunk and `grep` rc=1 on no-match must NOT abort the loop — the gate-level SKIP/FAIL decision is made from the accumulated state at end-of-loop, not from per-iteration rc.
420
+
421
+ ```bash
422
+ PREFLIGHT_TMP="$(git rev-parse --git-dir)"
423
+ mkdir -p "$PREFLIGHT_TMP/preflight-chunks"
424
+ host_union=""
425
+ jwt_chunk=""
426
+ while IFS= read -r chunk_path; do
427
+ # Defense-in-depth: validate chunk_path is a clean Next.js static-chunks subpath
428
+ # before interpolating into the curl URL (no `..`, no `@`, no `?`, no whitespace).
429
+ [[ "$chunk_path" =~ ^/_next/static/chunks/[A-Za-z0-9_/().-]+\.js$ ]] || continue
430
+ base=$(basename "$chunk_path")
431
+ curl -fsSL --max-time 10 --max-filesize 5242880 "https://app.soleur.ai${chunk_path}" -o "$PREFLIGHT_TMP/preflight-chunks/${base}" || continue
432
+ hosts=$(grep -oE 'https?://([a-z0-9.-]*supabase\.co|api\.soleur\.ai)' "$PREFLIGHT_TMP/preflight-chunks/${base}" | sort -u || true)
433
+ if [[ -n "$hosts" ]]; then
434
+ host_union="${host_union}${hosts}"$'\n'
435
+ fi
436
+ if [[ -z "$jwt_chunk" ]]; then
437
+ jwt=$(grep -oE 'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' "$PREFLIGHT_TMP/preflight-chunks/${base}" | head -1 || true)
438
+ if [[ -n "$jwt" ]]; then
439
+ jwt_chunk="$PREFLIGHT_TMP/preflight-chunks/${base}"
440
+ fi
441
+ fi
442
+ done < "$PREFLIGHT_TMP/preflight-candidates.txt"
443
+
444
+ printf '%s' "$host_union" | sort -u
445
+ printf 'jwt_chunk=%s\n' "${jwt_chunk:-<none>}"
446
+ ```
447
+
448
+ The redirected-stdin form (`< "$PREFLIGHT_TMP/preflight-candidates.txt"`) is required — piping (`cat ... | while read`) scopes loop variables to a subshell and loses `host_union` / `jwt_chunk` at loop exit. The `--max-filesize 5242880` (5 MB) cap defends against a misbehaving CDN response filling tmpfs across 20 fetches. The strict path regex rejects `..`, `@`, `?`, and whitespace before any string interpolation into the curl URL — a defense-in-depth gate even though the source HTML is served by our own CDN. Full traversal (no early-break) ensures placeholder-host leaks in late-candidate chunks are still detected (matrix row 6).
449
+
450
+ **Step 5.3: Assert canonical shape.**
451
+
452
+ The `host_union` from Step 5.2 must contain at least one host matching `^https://([a-z0-9]{20}\.supabase\.co|api\.soleur\.ai)$` and zero placeholder hosts (`test.supabase.co`, `placeholder.supabase.co`, `example.supabase.co`, `localhost`, `0.0.0.0`).
453
+
454
+ **Step 5.4: Decode and assert JWT claims from `jwt_chunk`.**
455
+
456
+ Pre-condition: `jwt_chunk` is non-empty after Step 5.2's traversal. If `jwt_chunk` is empty AND `host_union` is non-empty (canonical Supabase host found but no JWT in any of the 20 candidate chunks), return **FAIL** with note: "Supabase host found but no JWT in any of 20 candidate chunks — bundle is structurally inconsistent (host without key) and indicates a build regression." If `jwt_chunk` is empty AND `host_union` is empty (the full traversal yielded nothing), return **SKIP** with note: "Supabase init not present in any of the 20 candidate chunks loaded by /login — possible deeper Webpack restructure or app-shell split. Investigate manually (probe /dashboard or other authed routes)."
457
+
458
+ ```bash
459
+ JWT=$(grep -oE 'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' "$jwt_chunk" | head -1)
460
+ PAYLOAD=$(printf '%s' "$JWT" | cut -d. -f2)
461
+ PAD=$(( (4 - ${#PAYLOAD} % 4) % 4 ))
462
+ if [[ $PAD -gt 0 ]]; then PADDED="$PAYLOAD$(printf '=%.0s' $(seq 1 $PAD))"; else PADDED="$PAYLOAD"; fi
463
+ JSON=$(printf '%s' "$PADDED" | tr '_-' '/+' | base64 -d 2>/dev/null)
464
+ iss=$(printf '%s' "$JSON" | jq -er '.iss // ""') || { echo "FAIL: JWT payload not parseable as JSON (.iss missing or invalid)"; exit 1; }
465
+ role=$(printf '%s' "$JSON" | jq -er '.role // ""') || { echo "FAIL: JWT payload missing .role (security-gate fail-closed)"; exit 1; }
466
+ ref=$(printf '%s' "$JSON" | jq -er '.ref // ""') || { echo "FAIL: JWT payload missing .ref (security-gate fail-closed)"; exit 1; }
467
+ # Log-injection guard: strip C0 controls (NUL..US), DEL, and Unicode line separators
468
+ # (U+2028, U+2029) before any echo. Defends against ANSI escape sequences, terminal
469
+ # control chars, and GitHub Actions `::cmd::` smuggling via newlines or U+2028.
470
+ sanitize() { printf '%s' "$1" | LC_ALL=C tr -d '\000-\037\177' | LC_ALL=C sed $'s/\xe2\x80\xa8//g; s/\xe2\x80\xa9//g'; }
471
+ iss_safe=$(sanitize "$iss")
472
+ role_safe=$(sanitize "$role")
473
+ ref_safe=$(sanitize "$ref")
474
+ printf 'iss=%s role=%s ref=%s\n' "$iss_safe" "$role_safe" "$ref_safe"
475
+ ```
476
+
477
+ `jq -er` (raise on null/error) plus the explicit FAIL-on-non-zero-rc enforces fail-closed semantics on every claim (`iss`, `role`, `ref`) if the JWT regex matches a `eyJ...` literal that is not a valid base64-encoded JSON or is missing a required claim. The `sanitize()` helper strips C0 controls (`\x00`–`\x1f`), DEL (`\x7f`), and Unicode line separators (U+2028, U+2029) before any echo — `jq -r` does not escape control characters, so a crafted JWT could otherwise smuggle ANSI escapes (e.g., `\x1b[2J` to clear the operator's terminal), `::notice::PASS` GitHub Actions annotations via `\n`, or U+2028/2029 line breaks invisible to `${var//$'\n'/}` (precedent: `2026-04-28-anon-key-test-fixture-leaked-into-prod-build` Session Error #6).
478
+
479
+ The decoded claims MUST satisfy: `iss == "supabase"`, `role == "anon"`, `ref` matches `^[a-z0-9]{20}$`, and `ref` is not in the placeholder set (`test*`, `placeholder*`, `example*`, `service*`, `local*`, `dev*`, `stub*`).
480
+
481
+ **Result (eight-row decision matrix):**
482
+
483
+ Per `knowledge-base/project/learnings/2026-04-27-preflight-security-gates-skip-vs-fail-defaults.md` — Check 5 is an invariant gate, so SKIP only when truly indeterminate; FAIL when partial-observation contradicts the invariant.
484
+
485
+ | Host union | JWT discovered | Result | Rationale |
486
+ | --- | --- | --- | --- |
487
+ | Login HTML fetch failed (Step 5.1) | n/a | **SKIP** | Truly indeterminate — operator cannot run Check 5 against an unreachable origin. |
488
+ | Login HTML fetched but zero `<script src>` matches | n/a | **SKIP** | Truly indeterminate — bundle structure unrecognizable. |
489
+ | ≥1 canonical Supabase host AND JWT discovered AND JWT claims canonical | yes (canonical) | **PASS** | Invariant proven across (possibly different) chunks. |
490
+ | ≥1 canonical Supabase host AND JWT discovered AND JWT claims non-canonical (placeholder ref, role≠anon, iss≠supabase) | yes (broken) | **FAIL** | Invariant DISPROVEN — leak detected; rotate the corresponding GitHub repo secret. |
491
+ | ≥1 canonical Supabase host AND no JWT after full 20-traversal | no | **FAIL** | Bundle is structurally inconsistent (host without key); invariant cannot hold. |
492
+ | ≥1 placeholder host (`test.supabase.co`, `placeholder.supabase.co`, etc.) anywhere in any examined chunk | any | **FAIL** | Placeholder URL leaked into the bundle (the original PR #2975 class). |
493
+ | Zero Supabase host references AND zero JWTs after full 20-traversal | no | **SKIP** | Truly indeterminate — Supabase init not reachable from `/login`. Investigate manually (probe `/dashboard` or other authed routes). |
494
+ | JWT regex matches but base64 decode / `jq` parse fails | invalid | **FAIL** | Discovered structure looks like a JWT but cannot be parsed — fail-closed. |
495
+
496
+ SKIP-on-chunking-change (the issue #3010 failure mode) is gone: a chunking change that moves the JWT to a different chunk now PASSes via Step 5.2's traversal. A future "simplify the result block" commit that collapses row 5 ("host without JWT after traversal") back to SKIP would silently re-introduce the fail-open class that #2887/#2903 already paid for — see the 2026-04-27 learning.
497
+
498
+ ### Check 6: Brand-Survival Self-Review
499
+
500
+ Enforces `hr-weigh-every-decision-against-target-user-impact`: PRs that touch credentials, auth, data, payments, or user-owned resources must declare a `## User-Brand Impact` section (with a valid threshold) in the PR body. The section is the ship-time signal that the framing question was answered before the change reached production.
501
+
502
+ **Step 6.1: Detect sensitive-path diff.**
503
+
504
+ The canonical sensitive-path regex (single source of truth, mirrored verbatim in Check 10 Step 10.1, `plugins/soleur/skills/deepen-plan/SKILL.md` Phase 4.6 Step 2, and `plugins/soleur/skills/review/scripts/fix-round-seats.sh` `SENSITIVE_PATH_RE`):
505
+
506
+ ```bash
507
+ SENSITIVE_PATH_RE='^(apps/web-platform/(server|supabase|app/api|middleware\.ts$)|apps/web-platform/lib/(stripe|auth|byok|security-headers|csp|log-sanitize|safe-session|safe-return-to|supabase)|apps/web-platform/lib/(legal|auth)/|apps/[^/]+/infra/|.+/doppler[^/]*\.(yml|yaml|sh)$|\.github/workflows/.*(doppler|secret|token|deploy|release|version-bump|web-platform|infra-validation|cla|cf-token|linkedin-token).*\.ya?ml$)'
508
+
509
+ set -uo pipefail
510
+ grep -E "$SENSITIVE_PATH_RE" "$PREFLIGHT_TMP/preflight-diff-files.txt"
511
+ ```
512
+
513
+ Run as two separate Bash calls (assignment, then the `grep`). The path-set comes from the cached file written by Phase 0 Step 0.1 — the regex itself is byte-identical to the form previously consumed by `git diff --name-only origin/main...HEAD | grep -E "$SENSITIVE_PATH_RE"`. The regex covers every Next.js attack surface (`middleware.ts`, every `app/api/**` route, security/CSP/auth/session/legal libraries, infra Terraform under `apps/*/infra/`, all Doppler-aware shell scripts, and every credential-handling workflow even when `doppler` is not in the filename).
514
+
515
+ If `grep` exits non-zero (no match), return **SKIP** with note: "No sensitive paths touched."
516
+
517
+ **Step 6.2: Resolve the canonical compliance source (Shared Plan-File Resolution).**
518
+
519
+ Call **Shared Plan-File Resolution** (above Check 1). It sets `$PR_BODY_FILE`, `$SCRUBBED_BODY`, `$PLAN_PATH`, and `$COMBINED` for this check to consume. If `gh pr view` fails (no PR exists for the current branch), return **SKIP** with note: "No PR available — section validation deferred to next preflight run after PR creation."
520
+
521
+ The `## User-Brand Impact` section may live in the PR body itself (typical for short PRs) OR in a plan file referenced from the PR body (typical for plans authored via `soleur:plan`). Both signals are valid per `plugins/soleur/skills/review/SKILL.md` `<conditional_agents>` block. Shared Plan-File Resolution produces a `$COMBINED` input that contains both — scrubbed of HTML comments and fenced code blocks so a markdown example inside ` ``` ` cannot fool a substring match.
522
+
523
+ **Step 6.4: Check for the section heading.**
524
+
525
+ ```bash
526
+ grep -q '^## User-Brand Impact' "$COMBINED"
527
+ ```
528
+
529
+ If absent, return **FAIL** with: "Sensitive-path diff detected but neither PR body nor linked plan file contains a `## User-Brand Impact` section. Add the section per `${CLAUDE_PLUGIN_ROOT}/skills/plan/references/plan-issue-templates.md`."
530
+
531
+ **Step 6.5: Validate threshold and scope-out — anchored, not substring.**
532
+
533
+ Extract the threshold line. Require canonical bullet form (`- **Brand-survival threshold:** …`) so a free-text sentence containing the words "single-user incident" cannot pass:
534
+
535
+ ```bash
536
+ THRESHOLD_LINE=$(grep -E '^[[:space:]]*[-*][[:space:]]+\*\*Brand-survival threshold:\*\*' "$COMBINED" | head -n 1)
537
+ ```
538
+
539
+ If `$THRESHOLD_LINE` is empty, return **FAIL** with: "User-Brand Impact section present but the canonical bullet `- **Brand-survival threshold:** <label>` is missing. Use exactly the bullet form from `plan-issue-templates.md`."
540
+
541
+ Match the label as a discrete token. The value must follow the `**Brand-survival threshold:**` marker as an isolated word (optionally backticked) terminated by a value-boundary character (end-of-line, period, comma, semicolon, em-dash, or hyphen surrounded by spaces). This permits trailing commentary like `` `single-user incident` — explanation… `` while rejecting embedded substrings like "this is not a single-user incident":
542
+
543
+ ```bash
544
+ # Boundary regex: end-of-line OR punctuation/dash that indicates "value ends here, commentary follows"
545
+ BOUNDARY='($|[[:space:]]*[.,;]|[[:space:]]+[—–-][[:space:]])'
546
+ if [[ "$THRESHOLD_LINE" =~ \*\*Brand-survival[[:space:]]+threshold:\*\*[[:space:]]+\`?single-user[[:space:]]+incident\`?$BOUNDARY ]]; then
547
+ RESULT=PASS_INCIDENT
548
+ elif [[ "$THRESHOLD_LINE" =~ \*\*Brand-survival[[:space:]]+threshold:\*\*[[:space:]]+\`?aggregate[[:space:]]+pattern\`?$BOUNDARY ]]; then
549
+ RESULT=PASS_AGGREGATE
550
+ elif [[ "$THRESHOLD_LINE" =~ \*\*Brand-survival[[:space:]]+threshold:\*\*[[:space:]]+\`?none\`?$BOUNDARY ]]; then
551
+ RESULT=NEEDS_SCOPEOUT
552
+ else
553
+ echo "FAIL: threshold value not recognized. The value must immediately follow \`**Brand-survival threshold:**\` and be one of: \`single-user incident\` | \`aggregate pattern\` | \`none\` (terminated by end-of-line, punctuation, or em-dash + space)."
554
+ exit 1
555
+ fi
556
+ ```
557
+
558
+ If `RESULT=NEEDS_SCOPEOUT`, look for the scope-out bullet — require a non-empty `reason:` (the `\S` after `reason:` is what enforces "explain yourself"):
559
+
560
+ ```bash
561
+ grep -Eq 'threshold:[[:space:]]*none,[[:space:]]*reason:[[:space:]]*\S' "$COMBINED"
562
+ ```
563
+
564
+ - Match (rc 0): **PASS** — operator has justified why the touched sensitive path is not user-impacting.
565
+ - No match: **FAIL** with: "Sensitive-path diff with `threshold: none` requires a `threshold: none, reason: <one-sentence>` scope-out bullet (with a non-empty reason) inside the User-Brand Impact section."
566
+
567
+ If `RESULT=PASS_INCIDENT` or `RESULT=PASS_AGGREGATE`: **PASS**.
568
+
569
+ **Headless mode behaviour:** On **FAIL**, abort with the error details (no prompt). On **PASS** or **SKIP**, continue silently.
570
+
571
+ **Interactive mode behaviour:** On **FAIL**, present the failure reason and offer **AskUserQuestion** with options:
572
+
573
+ 1. "Fill in the section now" — prompt the operator for the three required lines (artifact / vector / threshold), append to the PR body via `gh pr edit --body-file -`, re-run Check 6.
574
+ 2. "Add scope-out note" — if the threshold should defensibly be `none`, prompt for the one-sentence reason, append `threshold: none, reason: <reason>` to the section, re-run Check 6.
575
+ 3. "Abort — fix elsewhere" — stop the pipeline; operator handles in another tool.
576
+
577
+ **Result:**
578
+
579
+ - **PASS** — No sensitive paths touched, OR section present with `single-user incident`/`aggregate pattern` threshold, OR section present with `none` threshold AND a valid scope-out note.
580
+ - **FAIL** — Sensitive-path diff with missing or empty User-Brand Impact section; OR `none` threshold without scope-out; OR missing/invalid threshold line.
581
+ - **SKIP** — No sensitive paths touched, OR no PR exists yet (defer to post-PR run).
582
+
583
+ ### Check 8: Service Worker Cache Bump on Client-Bundle Regression Fix
584
+
585
+ **Path-gated:** only runs when the cached path-set from Phase 0 Step 0.1 (`"$PREFLIGHT_TMP/preflight-diff-files.txt"`) AND the commit log together satisfy BOTH a regression-fix marker (a commit subject starting with `fix(`, `fix:`, or `hotfix`) AND any file under `apps/web-platform/lib/supabase/`, `apps/web-platform/sentry.client.config.ts`, `apps/web-platform/lib/auth/`, `apps/web-platform/lib/byok/`, or `apps/web-platform/components/error-boundary-view.tsx`. Otherwise return **SKIP** with note: "No client-bundle regression-fix surface detected." The path predicate is identical to the original `git diff --name-only origin/main...HEAD` form — only the diff source is changed.
586
+
587
+ **Rationale:** Content-hashed Next.js chunks normally invalidate cache automatically — new content → new filename → SW cache miss → fresh fetch. But the SW at `apps/web-platform/public/sw.js` uses a cache-first strategy under a single `CACHE_NAME` for `/_next/static/**`, and old broken chunks remain cached under their old filenames until either (a) browser eviction, or (b) the activate handler purges them via a `CACHE_NAME` bump. After PR #3014 deployed v0.58.0 with a corrected validator, users still saw the dashboard error.tsx because their SW was serving cached PR #3007 chunks. Without bumping `CACHE_NAME`, a regression fix to the inlined client bundle relies on every user manually clearing site data — unacceptable for an auth-tree outage.
588
+
589
+ **Step 8.1: Detect regression-fix commit subject.**
590
+
591
+ ```bash
592
+ git log origin/main..HEAD --pretty=%s | grep -iE '^(fix\(|fix:|hotfix)' | head -1
593
+ ```
594
+
595
+ If empty, return **SKIP** with note: "No fix(...) commit on branch."
596
+
597
+ **Step 8.2: Detect client-bundle surface in diff.**
598
+
599
+ Re-use the cached path-set from Phase 0 Step 0.1:
600
+
601
+ ```bash
602
+ grep -E '^apps/web-platform/(lib/(supabase|auth|byok)/|sentry\.client\.config\.ts|components/error-boundary-view\.tsx)' "$PREFLIGHT_TMP/preflight-diff-files.txt" | head -1
603
+ ```
604
+
605
+ The regex is byte-identical to the form previously piped from `git diff --name-only origin/main...HEAD`. If empty, return **SKIP** with note: "No client-bundle surface touched."
606
+
607
+ **Step 8.3: Compare `CACHE_NAME` against `origin/main`.**
608
+
609
+ Run as separate Bash calls (no command substitution per skill convention):
610
+
611
+ ```bash
612
+ git show origin/main:apps/web-platform/public/sw.js 2>/dev/null | grep -oE 'CACHE_NAME[[:space:]]*=[[:space:]]*"[^"]+"' | head -1
613
+ ```
614
+
615
+ ```bash
616
+ grep -oE 'CACHE_NAME[[:space:]]*=[[:space:]]*"[^"]+"' apps/web-platform/public/sw.js | head -1
617
+ ```
618
+
619
+ If the two values are byte-equal, **FAIL** with: "Client-bundle regression fix detected on branch but `CACHE_NAME` in `apps/web-platform/public/sw.js` was not bumped. Old chunks remain cached for users with an active SW registration. Bump the suffix (e.g., `v2` → `v3`) so the activate handler purges stale caches on next page load."
620
+
621
+ If the values differ (suffix bumped), **PASS**.
622
+
623
+ **Result:**
624
+
625
+ - **PASS** — `CACHE_NAME` bumped relative to `origin/main`, OR no client-bundle regression-fix surface detected.
626
+ - **FAIL** — client-bundle regression fix on branch with unchanged `CACHE_NAME`. Bump the suffix to force-purge the stale-cache class.
627
+ - **SKIP** — no `fix(...)` commit, OR no client-bundle surface in the diff.
628
+
629
+ ### Check 9: Node-Only Encodings Banned in Client-Bundle Paths
630
+
631
+ **Always runs (no path-pattern gate).** Enforces the rule that any module imported into the Next.js client bundle must use browser-safe APIs at module load. Node's `Buffer.from(s, "base64url")` (added in Node 16) is the canonical example: it works in vitest's default Node env AND in SSR, but the `buffer@5.x` polyfill webpack ships throws `TypeError: Unknown encoding: base64url` in browsers. A test that runs the validator only in Node passes; production hydration crashes on the first authenticated visit.
632
+
633
+ **Rationale:** PR #3007's `assertProdSupabaseAnonKey` introduced `Buffer.from(middle, "base64url")` inside `validate-anon-key.ts`, which `lib/supabase/client.ts` imports at module load. PR #3014's added tests also ran in Node env and missed it. The deployed bundle threw on every page load — dashboard AND login. Layer 1 canary missed it (HTML-only). Layer 3 missed it (bash `base64 -d` is unrelated to webpack's Buffer polyfill). The only true source-level gate is to ban the encoding token.
634
+
635
+ **Step 9.1: Build the candidate file list.**
636
+
637
+ ```bash
638
+ git ls-files \
639
+ 'apps/web-platform/lib/**/*.ts' \
640
+ 'apps/web-platform/lib/**/*.tsx' \
641
+ 'apps/web-platform/components/**/*.ts' \
642
+ 'apps/web-platform/components/**/*.tsx' \
643
+ 'apps/web-platform/app/**/*.ts' \
644
+ 'apps/web-platform/app/**/*.tsx' \
645
+ 'apps/web-platform/hooks/**/*.ts' \
646
+ 'apps/web-platform/hooks/**/*.tsx' \
647
+ | grep -v -E '/(server|api)/' \
648
+ | grep -v -E '\.test\.(ts|tsx)$' \
649
+ | grep -v -E '\.spec\.(ts|tsx)$'
650
+ ```
651
+
652
+ **Step 9.2: Grep candidates for banned tokens (excluding comment lines).**
653
+
654
+ ```bash
655
+ grep -nE 'Buffer\.from\([^)]*"base64url"' <files> \
656
+ | grep -vE ':[[:space:]]*(//|\*)' \
657
+ | grep -vE '`[^`]*Buffer\.from\([^`]*"base64url"[^`]*`'
658
+ ```
659
+
660
+ The first filter drops single-line `// ...` comments and JSDoc `* ...` lines. The second drops backtick-quoted references inside markdown-style code spans (which can occur in TSDoc/JSDoc bodies that don't start with `* `). The remaining matches are real call sites. <!-- markdownlint-disable-line MD038 -->
661
+
662
+ If output is non-empty, **FAIL** with a per-file listing: "Node-only encoding `base64url` in client-bundle path `<file>:<line>`. Replace with browser-safe `atob` + `base64.padEnd(...)` per `apps/web-platform/lib/supabase/validate-anon-key.ts` post-fix pattern, OR move the file behind a `lib/server/` boundary if it does not need the client bundle."
663
+
664
+ **Step 9.3: Ban-list extension policy.**
665
+
666
+ The current ban-list (extend as new classes are discovered):
667
+
668
+ - `Buffer.from(_, "base64url")` — covered above
669
+ - Future additions go here with a **Why:** pointer to the learning file
670
+
671
+ **Result:**
672
+
673
+ - **PASS** — no banned token found in any client-bundle path.
674
+ - **FAIL** — at least one banned token found (file + line listed).
675
+ - **SKIP** — never; this check has no path gate (it always scans the canonical candidate list).
676
+
677
+ ### Check 10: Discoverability Test Execution
678
+
679
+ **Path-gated** on the canonical sensitive-path regex (single source of truth; re-use Check 6 Step 6.1's `SENSITIVE_PATH_RE`). The path predicate runs against `"$PREFLIGHT_TMP/preflight-diff-files.txt"` (cached in Phase 0 Step 0.1). Otherwise return **SKIP** with note: "No sensitive paths touched — no Observability block required."
680
+
681
+ **Rationale:** `hr-observability-as-plan-quality-gate` mandates a `discoverability_test.command` that runs WITHOUT SSH. Plan Phase 2.9 and deepen-plan Phase 4.7 enforce field presence; neither runs the command. PR #4148 shipped with `curl https://web-platform.soleur.ai/api/inngest` — a typo'd hostname that fails DNS resolution. Five gates passed; the operator caught it. This check closes the "declared-verifiable but unverified" gap.
682
+
683
+ Invariant gate per `knowledge-base/project/learnings/2026-04-27-preflight-security-gates-skip-vs-fail-defaults.md`: SKIP only when truly indeterminate; FAIL when the invariant ("the documented command actually works against the live world") is contradicted.
684
+
685
+ **Reference implementation.** `plugins/soleur/test/lib/discoverability-test-parser.ts` mirrors the parser + classifier in TypeScript so the decision states can be unit-tested without subshells. The bash below IS the production runtime — the TS file is for tests. If they drift, the bash wins.
686
+
687
+ **Step 10.1: Sensitive-path gate (re-use Check 6 SSOT).**
688
+
689
+ ```bash
690
+ set -uo pipefail
691
+ # SSOT: see Check 6 Step 6.1; this literal MUST stay byte-identical.
692
+ # Mirrored consumers: Check 6 Step 6.1, deepen-plan/SKILL.md Phase 4.6 Step 2, review/scripts/fix-round-seats.sh.
693
+ SENSITIVE_PATH_RE='^(apps/web-platform/(server|supabase|app/api|middleware\.ts$)|apps/web-platform/lib/(stripe|auth|byok|security-headers|csp|log-sanitize|safe-session|safe-return-to|supabase)|apps/web-platform/lib/(legal|auth)/|apps/[^/]+/infra/|.+/doppler[^/]*\.(yml|yaml|sh)$|\.github/workflows/.*(doppler|secret|token|deploy|release|version-bump|web-platform|infra-validation|cla|cf-token|linkedin-token).*\.ya?ml$)'
694
+ grep -E "$SENSITIVE_PATH_RE" "$PREFLIGHT_TMP/preflight-diff-files.txt"
695
+ ```
696
+
697
+ If `grep` exits non-zero, return **SKIP** with note: "No sensitive paths touched."
698
+
699
+ **Step 10.2: Resolve the plan file (Shared Plan-File Resolution).**
700
+
701
+ Call **Shared Plan-File Resolution** (above Check 1). The output `$COMBINED` contains the scrubbed PR body concatenated with the scrubbed plan file (when a `knowledge-base/project/plans/*.md` link is present). `$PLAN_PATH` is the resolved path or empty.
702
+
703
+ If `$COMBINED` is empty (no PR available — `gh pr view` failed), return **SKIP** with note: "No PR available — Check 10 deferred to next preflight run after PR creation."
704
+
705
+ If `$PLAN_PATH` is empty (sensitive-path diff but no plan link in PR body), return **SKIP** with note: "Sensitive-path diff but no plan file referenced from PR body. Cannot extract discoverability_test.command. (If the PR uses inline Observability in the PR body, copy the plan file into the body via a `knowledge-base/project/plans/` link.)"
706
+
707
+ **Path-traversal hardening (defense-in-depth).** A malicious PR body could link to `knowledge-base/project/plans/../../../etc/passwd.md` (or a symlink) and trick the awk reader into following arbitrary paths whose content is then parsed for a `discoverability_test.command` — turning any `.md`-suffixed file the operator can read into an execution oracle. Refuse anything that resolves outside the canonical plans directory:
708
+
709
+ ```bash
710
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
711
+ RESOLVED_PLAN_PATH="$(realpath -e "$PLAN_PATH" 2>/dev/null || true)"
712
+ case "$RESOLVED_PLAN_PATH" in
713
+ "$REPO_ROOT/knowledge-base/project/plans/"*) ;;
714
+ *)
715
+ echo "FAIL: plan path '$PLAN_PATH' does not resolve under $REPO_ROOT/knowledge-base/project/plans/. Refusing to read."
716
+ exit 1
717
+ ;;
718
+ esac
719
+ ```
720
+
721
+ **Step 10.3: Extract the `## Observability` block from the plan file.**
722
+
723
+ ```bash
724
+ PREFLIGHT_TMP="$(git rev-parse --git-dir)"
725
+ # ‼️ ANCHOR the heading match. `/^## Observability/` is a PREFIX match, so it
726
+ # also matches `## Observability layer citation` — a section name that
727
+ # `hr-observability-layer-citation` actively encourages, making the collision
728
+ # systemic rather than incidental. When both sections exist, the unanchored form
729
+ # extracts the FIRST (the prose citation), finds no `discoverability_test`, and
730
+ # FAILs with "no command could be parsed" — a false FAIL on a plan that is
731
+ # perfectly well-formed. Verified against #6698's plan: unanchored extracted 47
732
+ # lines of the wrong section; anchored reaches the real block.
733
+ # Separate Bash calls do not share variables, so re-assert the plan path here: awk
734
+ # given an EMPTY filename skips it and reads stdin, which hangs the call (#8705).
735
+ [[ -n "${PLAN_PATH:-}" && -f "$PLAN_PATH" ]] || { echo "SKIP: no readable plan file (PLAN_PATH='${PLAN_PATH:-}') — re-run Shared Plan-File Resolution in this call."; exit 0; }
736
+ awk '/^## Observability$/{ino=1; next} /^## /{if (ino) exit} ino' "$PLAN_PATH" > "$PREFLIGHT_TMP/preflight-observability.txt"
737
+ test -s "$PREFLIGHT_TMP/preflight-observability.txt" || { echo "FAIL: Plan touches sensitive paths but '## Observability' block is missing. See hr-observability-as-plan-quality-gate."; exit 1; }
738
+ ```
739
+
740
+ If the block is missing, return **FAIL** with: "Sensitive-path diff but plan file `<PLAN_PATH>` is missing the `## Observability` block. See `hr-observability-as-plan-quality-gate`. Add the section per `${CLAUDE_PLUGIN_ROOT}/skills/plan/references/plan-issue-templates.md`."
741
+
742
+ **Step 10.4: Extract `discoverability_test.command` and `expected_output`.**
743
+
744
+ The plan-template schema (`plan-issue-templates.md` §Observability →
745
+ `discoverability_test`) defines:
746
+
747
+ ```yaml
748
+ discoverability_test:
749
+ command: # one command an operator can run LOCALLY (no ssh)
750
+ expected_output: # canonical "everything OK" output
751
+ credentials_required: # OPTIONAL — see Step 10.4; declares a probe with no
752
+ # unauthenticated substitute, which Check 10 then SKIPs
753
+ # without executing
754
+ ```
755
+
756
+ Plans use TWO shapes — strict YAML (**Form A**, canonical) AND looser prose with a fenced code block followed by `Expected output: …` (**Form B**, PR #4148 shape). The parser MUST accept both forms.
757
+
758
+ **Form A — strict YAML:**
759
+
760
+ ```yaml
761
+ discoverability_test:
762
+ command: curl -fsS ... https://app.soleur.ai/api/inngest
763
+ expected_output: "200"
764
+ ```
765
+
766
+ Form A accepts all three YAML scalar shapes for `command:`:
767
+
768
+ | Shape | Header | Continuations joined with |
769
+ | --- | --- | --- |
770
+ | **inline** | `command: curl …` | — (value is on the key line) |
771
+ | **inline quoted** | `command: "…"` / `'…'` | — (value is on the key line; decoded, see below) |
772
+ | **block** | `command: \|`, `\|-`, `\|+` | newline |
773
+ | **folded** | `command: >`, `>-`, `>+` | space |
774
+
775
+ An **inline quoted** scalar is decoded once, by the parser, and nowhere else: `"…"` decodes
776
+ `\"` and `\\` only, and every other backslash sequence (`\n`, `\t`, …) passes through
777
+ byte-for-byte, so a shell `\n` never becomes a newline that Step 10.5 rejects. `'…'` decodes
778
+ `''` only. The closing quote is the first unescaped one, and only space, tab or CR
779
+ (optionally then a whitespace-led `# comment`) may follow it. An empty pair (`""`, `''`), an
780
+ unterminated or mismatched pair, or other trailing text stays unchanged. Block and folded
781
+ content is never decoded. `expected_output` and `credentials_required` are not YAML-decoded:
782
+ their reads below strip one symmetric quote pair and decode no escapes.
783
+
784
+ Block and folded headers may carry a trailing `# comment`. Scalar extent follows YAML
785
+ indent semantics: a continuation is any non-empty line indented **more** than the
786
+ `command:` key, and the first line indented **≤** the key ends the scalar. The parser is
787
+ [`./scripts/parse-form-a.awk`](./scripts/parse-form-a.awk) — a real file rather than an
788
+ inlined program, so the awk/TS parity harness executes the production runtime directly
789
+ instead of regex-scraping this prose. That file is authoritative; the TypeScript mirror in
790
+ `plugins/soleur/test/lib/discoverability-test-parser.ts` is non-authoritative.
791
+
792
+ **Form B — prose + fenced block:**
793
+
794
+ ```markdown
795
+ - **discoverability_test.command:**
796
+ ```bash
797
+ curl -fsS -o /dev/null -w "%{http_code}\n" --max-time 10 https://app.soleur.ai/api/inngest
798
+ ```
799
+
800
+ Expected output: `200` (or `401` with HMAC challenge). Anything else = absent.
801
+
802
+ ```
803
+
804
+ Detection: find the first `discoverability_test` line in the Observability block; from that point, locate the first fenced code block — its contents are the command. Then locate the first line matching `^[[:space:]]*Expected output:` (case-insensitive) — its value is the expected.
805
+
806
+ ```bash
807
+ PREFLIGHT_TMP="$(git rev-parse --git-dir)"
808
+ # Form A first (anchored YAML key — strongest signal).
809
+ #
810
+ # The parser lives in a real file so the parity harness can execute it.
811
+ #
812
+ # Both Check 10 operands resolve through the loader token (ADR-179, #7453): the loader
813
+ # substitutes it with the INSTALLED plugin root at delivery. Never the git root — after a
814
+ # `gh pr checkout` that is the REVIEWED PARTY's tree (ADR-179's rejected option (d)). Under
815
+ # this check's `set -u` an unset token aborts at the assignment ("unbound variable"); an empty one
816
+ # expands to a root-anchored `/skills/...` path that the `test -r` below refuses.
817
+ #
818
+ # Hard-fail on a load error. `awk -f <missing>` exits 2 with EMPTY stdout, and
819
+ # `set -uo pipefail` does NOT abort on it (command-substitution rc is discarded), so a
820
+ # missing parser would leave $CMD empty and Form B would silently parse a DIFFERENT
821
+ # command. Never fall through.
822
+ #
823
+ # LC_ALL=C pins the parse to bytes: under a UTF-8 gawk `[[:space:]]` matches U+2028, so the
824
+ # same plan would parse differently on different operator hosts.
825
+ FORM_A_AWK="${CLAUDE_PLUGIN_ROOT}/skills/preflight/scripts/parse-form-a.awk"
826
+ test -r "$FORM_A_AWK" || { echo "FAIL: Check 10 parser missing at $FORM_A_AWK (plugin root unresolved? export CLAUDE_PLUGIN_ROOT=<the installed soleur plugin root>, never a path inside this repository)"; exit 1; }
827
+ CMD=$(LC_ALL=C awk -f "$FORM_A_AWK" "$PREFLIGHT_TMP/preflight-observability.txt")
828
+ AWK_RC=$?
829
+ if [[ "$AWK_RC" -ne 0 ]]; then
830
+ # `$?` here would report the `!`-inverted status (always 0) — capture the real
831
+ # rc from the command substitution before emitting it.
832
+ echo "FAIL: Check 10 Form A parser errored (awk rc=${AWK_RC:-unknown}); refusing to fall through to Form B."
833
+ exit 1
834
+ fi
835
+
836
+ EXPECTED=$(awk '
837
+ /^[[:space:]]*expected_output:/ { sub(/^[[:space:]]*expected_output:[[:space:]]*/, ""); print; exit }
838
+ ' "$PREFLIGHT_TMP/preflight-observability.txt")
839
+
840
+ # Fallback to Form B (fenced block under `discoverability_test.command:` prose).
841
+ # Skip leading `#` comment lines inside the fence — operator prose comments
842
+ # are NOT part of the command (mirrors the TS reference impl). Without this
843
+ # strip, the FIRST executable bash line is treated as the command and any
844
+ # leading `# comment` line silently runs as bash via `bash -c`.
845
+ if [[ -z "$CMD" ]]; then
846
+ CMD=$(awk '
847
+ /discoverability_test/ { found=1 }
848
+ found && /^[[:space:]]*```/ { fence=!fence; if (!fence && lines>0) exit; next }
849
+ found && fence && /^[[:space:]]*#/ { next }
850
+ found && fence { print; lines++ }
851
+ ' "$PREFLIGHT_TMP/preflight-observability.txt")
852
+ fi
853
+
854
+ if [[ -z "$EXPECTED" ]]; then
855
+ EXPECTED=$(grep -iE '^[[:space:]]*(\*\*)?Expected output:(\*\*)?' "$PREFLIGHT_TMP/preflight-observability.txt" | head -1 | sed -E 's/^[[:space:]]*(\*\*)?Expected output:(\*\*)?[[:space:]]*//I')
856
+ fi
857
+ ```
858
+
859
+ **Normalize `$CMD` ONCE, here, before anything else reads it.**
860
+
861
+ Every downstream consumer — the probe-verb gate, the shell-active reject, and the sandboxed
862
+ exec — must see the SAME string. Gating one form while executing another is a laundering
863
+ gap, and normalizing inside the gate alone produced exactly that: the gate judged the
864
+ normalized command while Step 10.5 rejected the raw one on its embedded newline, so a Form A
865
+ block scalar carrying a leading `#` comment failed the runtime even though the gate (and the
866
+ TypeScript mirror) accepted it. The mirror's `normalizeCommand()` is this. The parity
867
+ harness once compared only the GATE, so this divergence was invisible to it. The test suite
868
+ now also runs the parse fence above and this fence under bash and compares the `$CMD` they
869
+ leave, the string the gate, the reject and the exec all receive (#7548). It does not run
870
+ the gate, the reject or the exec themselves.
871
+
872
+ ```bash
873
+ # Drop full-line `#` comments and blank lines, then trim. A `#` inside a quoted
874
+ # argument is untouched — only whole comment LINES match. Verified across the
875
+ # 642-command corpus: 6 commands change (all block scalars with leading comment
876
+ # lines, the shape this fixes) and no quoted `#` is altered.
877
+ CMD="$(printf '%s' "$CMD" | sed -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d')"
878
+ CMD="${CMD#"${CMD%%[![:space:]]*}"}"
879
+ CMD="${CMD%"${CMD##*[![:space:]]}"}"
880
+ # Quoted inline scalars are decoded ONCE, by parse-form-a.awk; a strip here would double-decode `command: "'x'"` into `x`.
881
+ # preflight-discoverability-test.test.ts slices this fence and the FORM_A_AWK= fence out of this file and runs them (E rows, #7453): keep both anchors unique and no other bash fence between the two.
882
+ # Version skew: this fence and parse-form-a.awk must come from ONE plugin root. New SKILL.md + old awk leaves the quotes in, so the exec fails rc=127; old SKILL.md + new awk decodes twice.
883
+ ```
884
+
885
+ If `$CMD` is empty after both attempts, return **FAIL** with: "Plan `<PLAN_PATH>` declares an Observability block but no `discoverability_test.command` could be parsed. See `${CLAUDE_PLUGIN_ROOT}/skills/plan/references/plan-issue-templates.md` §Observability."
886
+
887
+ **Reject SSH commands** (defense-in-depth):
888
+
889
+ ```bash
890
+ # Match on a DEQUOTED COPY, as probe-verb-gate.sh does for the verb: bash resolves `\ssh`,
891
+ # `'s''sh'` and `"ssh"` to the same binary. Never strip from the string that is executed.
892
+ CMD_NOQ="${CMD//[\"\'\\]/}"
893
+ if [[ "$CMD_NOQ" =~ (^|[[:space:]]|/)ssh([[:space:]]|$) ]]; then
894
+ echo "FAIL: discoverability_test.command contains ssh; rule violation per hr-observability-as-plan-quality-gate."
895
+ exit 1
896
+ fi
897
+ ```
898
+
899
+ **Read `credentials_required` and honour a declared-credentialed probe** (ADR-175 Layer 3):
900
+
901
+ Some probes verify a property that has **no unauthenticated substitute** — a warehouse
902
+ query API with no anonymous form, for instance. Before #7393 those had only two outcomes:
903
+ FAIL (verify nothing) or rewrite to a weaker property (verify the wrong thing). The
904
+ optional `discoverability_test.credentials_required` sub-field is the sanctioned third:
905
+ declare the scope and the justification, and Check 10 **skips without executing**.
906
+
907
+ Read it with a flat sub-field `awk`, exactly as `expected_output` already is — do **not**
908
+ extend [`./scripts/parse-form-a.awk`](./scripts/parse-form-a.awk), which is pinned
909
+ byte-exactly against its TypeScript mirror by the P1/P2/P3 parity harness. The flat read
910
+ inherits `expected_output`'s pre-existing limitation (two `discoverability_test`
911
+ sub-blocks could confuse it); that is unchanged here.
912
+
913
+ The read is SCOPED TO THE `discoverability_test:` SUB-BLOCK, and that scoping is
914
+ load-bearing rather than tidiness. It NARROWS the bypass class without closing
915
+ it — a `credentials_required:` line inside the command's own block scalar is
916
+ still honoured, so a multi-line command can waive itself. Closing that needs a
917
+ YAML-aware parse this flat `awk` deliberately avoids. `expected_output` is read with a flat
918
+ whole-block scan and survives it, because a stray `expected_output:` only changes
919
+ what a probe is compared against. This field **skips execution entirely**, so the
920
+ same flat read is a bypass: measured, one `credentials_required:` line under
921
+ `liveness_signal:` — or in prose anywhere in the section — waived a perfectly
922
+ runnable unauthenticated `curl` probe. Three further shapes declare nothing and
923
+ are rejected rather than honoured: a `#`-comment-only value (the documented
924
+ template ships one, so an agent copying the canonical schema produced a
925
+ valid-looking declaration whose "scope" was the template's own comment), a bare
926
+ `>`/`|` block indicator, and an empty value.
927
+
928
+ ```bash
929
+ # Log-injection guard, defined HERE rather than in Step 10.5. Every value echoed
930
+ # below ($CREDS_REQ, $PROBE_VERB) is derived from PR-authored plan
931
+ # text, which this check's own design treats as adversarial — and Step 10.5's
932
+ # copy is ~250 lines further down, so these paths could not call it even in
933
+ # principle. U+2028/U+2029 are NOT in [[:space:]] under LC_ALL=C, so they survive
934
+ # tokenization into $PROBE_VERB and reach the operator's terminal; ESC bytes do
935
+ # too. Same helper and same reasoning as Check 5 (which cites the precedent).
936
+ # Step 10.5 redefines it identically; the redefinition is harmless.
937
+ sanitize() { printf '%s' "$1" | LC_ALL=C tr -d '\000-\037\177' | LC_ALL=C sed $'s/\xe2\x80\xa8//g; s/\xe2\x80\xa9//g'; }
938
+
939
+ # awk walks only the indented children of `discoverability_test:` — never the
940
+ # whole Observability block. `tolower()` rather than bash's ${var,,} because the
941
+ # latter is bash 4.0+ and this block runs ~200 lines BEFORE the bwrap probe, so
942
+ # on a bash 3.2 host (stock macOS) ${var,,} is a parse error long before Check 10
943
+ # could reach its graceful SKIP.
944
+ CREDS_REQ=$(awk '
945
+ /^[[:space:]]*discoverability_test:/ { in_dt=1; parent=match($0, /[^ \t]/); next }
946
+ in_dt && /^[[:space:]]*$/ { next }
947
+ in_dt {
948
+ ind = match($0, /[^ \t]/)
949
+ if (ind <= parent) { exit }
950
+ if ($0 ~ /^[[:space:]]*credentials_required:/) {
951
+ sub(/^[[:space:]]*credentials_required:[[:space:]]*/, ""); print; exit
952
+ }
953
+ }
954
+ ' "$PREFLIGHT_TMP/preflight-observability.txt")
955
+ # Trim, THEN strip a symmetric quote pair. Order and completeness both matter:
956
+ # stripping only `"` and never trimming meant `credentials_required: 'placeholder'`
957
+ # and `credentials_required: TODO␠␠` both reached the SKIP-DECLARED branch while
958
+ # the TypeScript mirror correctly FAILed them — so the placeholder superset fix
959
+ # was undone in the runtime of record by one apostrophe, invisibly to the suite.
960
+ CREDS_REQ="${CREDS_REQ#"${CREDS_REQ%%[![:space:]]*}"}"
961
+ CREDS_REQ="${CREDS_REQ%"${CREDS_REQ##*[![:space:]]}"}"
962
+ # Drop a YAML trailing comment (` #…` after whitespace) BEFORE the quote strip, or
963
+ # `"TODO" # fill later` keeps its comment, fails the pair match and waives the check.
964
+ # A value OPENING with a quote keeps everything through its first matching close quote.
965
+ case "$CREDS_REQ" in
966
+ \"*|\'*)
967
+ CR_Q="${CREDS_REQ:0:1}"; CR_BODY="${CREDS_REQ:1}"; CR_BODY="${CR_BODY%%"$CR_Q"*}"
968
+ CR_TAIL="${CREDS_REQ:$(( ${#CR_BODY} + 2 ))}"
969
+ if [[ "$CREDS_REQ" == "$CR_Q$CR_BODY$CR_Q"* && "$CR_TAIL" =~ ^[[:space:]]+# ]]; then
970
+ CREDS_REQ="$CR_Q$CR_BODY$CR_Q"
971
+ fi ;;
972
+ *) CREDS_REQ="${CREDS_REQ%%[[:space:]]#*}"
973
+ CREDS_REQ="${CREDS_REQ%"${CREDS_REQ##*[![:space:]]}"}" ;;
974
+ esac
975
+ case "$CREDS_REQ" in
976
+ \"*\") CREDS_REQ="${CREDS_REQ#\"}"; CREDS_REQ="${CREDS_REQ%\"}" ;;
977
+ \'*\') CREDS_REQ="${CREDS_REQ#\'}"; CREDS_REQ="${CREDS_REQ%\'}" ;;
978
+ esac
979
+ # A comment-only value, a bare block/fold indicator, or an empty value declares
980
+ # nothing. Treat all as absent so the normal path runs. The CHOMPED indicators
981
+ # (`>-` `>+` `|-` `|+`) are included because #6772 — the change cited as the
982
+ # reason authors reach for indicators at all — is exactly what made those forms
983
+ # first-class for `command:`, so they are the likelier typo, not the rarer one.
984
+ case "$CREDS_REQ" in
985
+ "#"*|">"|">-"|">+"|"|"|"|-"|"|+") CREDS_REQ="" ;;
986
+ esac
987
+
988
+ if [[ -n "$CREDS_REQ" ]]; then
989
+ # Placeholder text is a FAIL. This set is a SUPERSET of deepen-plan §4.7's —
990
+ # they were disjoint on exactly the wrong members, so `placeholder` and
991
+ # `manual operator check` (the repo's canonical "says nothing" markers) were
992
+ # GRANTING the waiver.
993
+ CREDS_REQ_LC=$(printf '%s' "$CREDS_REQ" | tr '[:upper:]' '[:lower:]')
994
+ if [[ "$CREDS_REQ_LC" =~ ^(todo|tbd|n/a|na|none|tktk|placeholder|manual\ operator\ check|\.\.\.|\<.*\>)$ ]]; then
995
+ echo "FAIL: discoverability_test.credentials_required is a placeholder (\"$(sanitize "$CREDS_REQ")\"). State the credential scope and why no unauthenticated probe verifies the same property, or remove the field."
996
+ exit 1
997
+ fi
998
+ echo "SKIP-DECLARED: discoverability_test declares credentials_required — $(sanitize "$CREDS_REQ"). Check 10 did NOT execute the command: running it inside the Step 10.5 sandbox would fail for lack of credentials and prove nothing about the property under test."
999
+ exit 0
1000
+ fi
1001
+ ```
1002
+
1003
+ `SKIP-DECLARED` is its **own** terminal, never folded into ordinary `SKIP`, and it carries
1004
+ the declared scope verbatim into the Phase 2 aggregate row. That is deliberate: this is the
1005
+ cheapest path to a non-FAIL for any probe whose verb Check 10 cannot run — though
1006
+ not the cheapest overall: a tautological probe (`printf 200` against
1007
+ `expected_output: "200"`) reaches PASS and is counted by none of the three
1008
+ counterweights below. That gap is pre-existing, not introduced here, but the
1009
+ superlative was wrong as written. For the declared path specifically, this is and in `soleur:one-shot` the
1010
+ same agent authors the declaration and runs the gate. Left invisible it would convert
1011
+ Check 10 from a verification gate into self-certification. The three mechanical
1012
+ counterweights are the distinct terminal, the committed corpus baseline count in
1013
+ `plugins/soleur/test/preflight-discoverability-test.test.ts` (so each new adoption is a
1014
+ reviewable diff line rather than silent drift), and the checklist entry in
1015
+ `soleur:engineering:review:observability-coverage-reviewer` §Step 6.
1016
+
1017
+ Note what the waiver is and is not: it is a **verification waiver**, not an execution
1018
+ bypass. The declared path never executes, so no verb reaches the sandbox. The waiver does
1019
+ genuinely span *any* verb — that is why it is counted rather than reordered.
1020
+
1021
+ **Probe-verb gate** (ADR-175 Layer 2, revised — schema validation, not a security control):
1022
+
1023
+ The gate lives in [`./scripts/probe-verb-gate.sh`](./scripts/probe-verb-gate.sh) rather than
1024
+ inline here, for the same reason [`./scripts/parse-form-a.awk`](./scripts/parse-form-a.awk)
1025
+ is a real file: a parity harness must be able to **execute the production runtime** instead
1026
+ of regex-scraping this prose. That is not hypothetical — while this gate was inlined with a
1027
+ hand-maintained TypeScript mirror, two parser asymmetries shipped green inside the very PR
1028
+ that created the mirror (a Form B command kept its markdown indentation, so the effective
1029
+ verb was `""`; a Form A block scalar kept a leading `#`, so it was `"#"`). Content pins on
1030
+ the allowlist literal cannot detect behavioural drift.
1031
+
1032
+ ```bash
1033
+ PROBE_GATE="${CLAUDE_PLUGIN_ROOT}/skills/preflight/scripts/probe-verb-gate.sh"
1034
+ test -r "$PROBE_GATE" || { echo "FAIL: Check 10 probe-verb gate missing at $PROBE_GATE"; exit 1; }
1035
+ if ! PROBE_REJECT="$(bash "$PROBE_GATE" "$CMD")"; then
1036
+ echo "FAIL: $(sanitize "$PROBE_REJECT")"
1037
+ exit 1
1038
+ fi
1039
+ ```
1040
+
1041
+ **What this gate is, stated honestly.** It answers *"is this an executable command, and one
1042
+ Check 10's PATH can run?"* — schema validation on the `command:` field. It is **not** a
1043
+ security control and **not** a legibility control, and the earlier revision of this section
1044
+ claimed both.
1045
+
1046
+ It cannot be a security control, and the reason is structural rather than incidental: the
1047
+ gate's own sanctioned remedy is *"wrap it in a repo-relative script"* — i.e. `bash
1048
+ scripts/x.sh`, which is arbitrary in-sandbox code execution. Every shape it could reject is
1049
+ strictly **weaker** than the remedy it prescribes. Step 10.5 additionally runs
1050
+ `bash -c "$CMD"`, so every probe already *is* an inline program; a rule rejecting
1051
+ `python3 -c` while printing instructions to use `bash scripts/x.sh` rejects a spelling, not
1052
+ a capability. Measured on the plan corpus, the deleted arg rules were 50% false-positive
1053
+ and the deleted path rule fired once across the corpus, while false-rejecting
1054
+ `bun test … -p`, `python3 … --print json` and `node … -e prod`.
1055
+
1056
+ It is not a legibility control either: the full command sits in the PR diff regardless, and
1057
+ `git -c alias.…`, `bash scripts/x.sh` and `./anything` all read as innocuous while doing
1058
+ arbitrary things.
1059
+
1060
+ **What it measurably buys**: 34 entries in the plan corpus are prose in a `command:` field
1061
+ (*"Sentry issue search feature:…"*, *"Open Sentry → Issues → …"*). Those become a named FAIL
1062
+ at authoring time instead of an opaque `rc=127` at ship time. That value comes entirely
1063
+ from the bare verb list, which is why the arg and path rules were deleted rather than
1064
+ repaired.
1065
+
1066
+ **The allowlist is a corpus-frequency list, not a capability list.** `sh`, `dig` and
1067
+ `getent` are absent because they have **zero** corpus uses; `awk`, `sed` and `find` are
1068
+ absent for the same reason, **not** because they are uniquely dangerous. `git` is on the
1069
+ list and is a full execution vector — `git -c alias.x='!cmd' x` runs arbitrary commands —
1070
+ and so is `bash <script>`. Both are recorded in ADR-175 as worked examples that an
1071
+ allowlist entry is an authority grant which this gate does not bound.
1072
+
1073
+ **Every allowlist entry is an authority grant — do not describe either layer as though it
1074
+ closed the class.** Layer 1 (the Step 10.5 sandbox) bounds what a verb can *reach*, but
1075
+ `curl` retains network egress and `bash <script>` retains arbitrary in-sandbox code
1076
+ execution. Adding a verb therefore warrants the same scrutiny as an infra change, not a
1077
+ "legibility decision". Conversely the allowlist bounds *legibility*, not authority: it is
1078
+ the sandbox, not the verb list, that makes a script which self-wraps `doppler run -c prd`
1079
+ fail loudly instead of succeeding silently.
1080
+ <!-- SECURITY (at-mention auto-attach footgun): never write an at-sign immediately
1081
+ followed by a real home/absolute path (tilde-slash, dollar-HOME-slash, or a
1082
+ `/home` `/Users` `/root` `/etc` absolute) in ANY skill/agent/doc that loads into
1083
+ agent context: Claude Code's @-mention auto-attach resolves such a token to the
1084
+ real on-disk file and attaches its CONTENTS to the transcript. Observed
1085
+ 2026-07-22 during PR #6830's ship — a literal here resolved to the operator's
1086
+ live Doppler root token and auto-attached it. The guard
1087
+ .github/scripts/test/test-no-at-mention-credfile-footgun.sh enforces this repo-wide. -->
1088
+
1089
+ **What the retired denylist was, and why counting verbs was never enough.** Until #7393
1090
+ this gate was a denylist of ten credentialed CLIs (`doppler`, `gh`, `aws`, `supabase`,
1091
+ `stripe`, `hcloud`, `wrangler`, `terraform`, `flyctl`, `vercel`). It could not be complete:
1092
+ it never caught indirect invocation (`bash scripts/foo.sh` whose body self-wraps
1093
+ `doppler run -c prd`), nor any future vendor CLI, nor an absolute-path read of a credential
1094
+ file by a permitted verb. Measured during #7393's planning: with the whole filesystem
1095
+ readable, `awk 'BEGIN{system("…")}'` executed arbitrary commands past the shell-active
1096
+ reject, and a `curl --data-binary` against a home-rooted credential path carried no
1097
+ shell-active token at all. **The durable fix was never a longer list — it was moving the
1098
+ control into the filesystem layer (Step 10.5).**
1099
+
1100
+ **Step 10.5: Sanitize and run inside a filesystem-isolated sandbox, with a tight timeout.**
1101
+
1102
+ The block below uses a `text` fence (not `bash`) so the skill-security-scan
1103
+ calibration suite does not flag it as `shell-spawn-c-flag`. The runtime IS a
1104
+ shell-spawn — the **load-bearing** mitigation is the bubblewrap sandbox below
1105
+ (ADR-175 Layer 1); the Step 10.4 verb allowlist, the `ssh`/`$()`/backtick
1106
+ rejects and the 15s outer timeout are defense-in-depth. The plan-file source
1107
+ is trust-on-PR-review. See [`2026-05-20-preflight-check-10-discoverability-test-execution.md`](../../../../knowledge-base/project/learnings/best-practices/2026-05-20-preflight-check-10-discoverability-test-execution.md).
1108
+
1109
+ **Why a filesystem boundary and not an environment tweak.** `env -i` scrubs environment
1110
+ *variables*; it does not remove *files or sockets*. An ephemeral `$HOME` changes where a
1111
+ CLI **looks**, not what is **readable** — measured during #7393's planning,
1112
+ `wc -c < <the operator's home Doppler config>` returned a live 294-byte service token
1113
+ under `HOME=$(mktemp -d)`. The sandbox removes the credential stores as files:
1114
+
1115
+ | Probe, run inside the sandbox | Result |
1116
+ | --- | --- |
1117
+ | absolute-path read of the home Doppler config | `No such file or directory` |
1118
+ | glob read across `/home/*/…` | `No such file or directory` |
1119
+ | home SSH private key | `No such file or directory` |
1120
+ | `ls /run/user/<uid>/bus` (D-Bus → Secret Service keyring) | `No such file or directory` |
1121
+ | `awk 'BEGIN{system("<absolute-path read>")}'` | `cannot open` |
1122
+ | append to a tracked repo file | `Read-only file system` |
1123
+ | `curl https://soleur.ai/` | `200` |
1124
+ | `dig +short soleur.ai` | resolves |
1125
+ | `grep -c . AGENTS.md` | matches the host value |
1126
+
1127
+ The read-only repo bind is what closes the **write-back escalation**: without it a probe
1128
+ can install `.git/hooks/pre-commit`, which `soleur:ship` then executes seconds later with
1129
+ the operator's real `$HOME` — turning a few-second credential window into a full
1130
+ compromise.
1131
+
1132
+ ```text
1133
+ # Defense-in-depth: reject every shell-active token before run. The plan file
1134
+ # is trust-on-PR-review. NOTE: this regex blocks ONE SPELLING of chaining. Its
1135
+ # complement — the inline-program arg rules — was deleted in #7393, so
1136
+ # `python3 -c "__import__('os').system('curl …')"` now passes both this reject
1137
+ # and the verb gate with no rejected token present. Do NOT cite it as the control
1138
+ # against a malicious plan author; it is a typo-catcher. The Step 10.5 sandbox is
1139
+ # the control.
1140
+ # Rejects: ;, &&, ||, |, >, <, &, NEWLINE, parameter expansion ($VAR or ${VAR}),
1141
+ # command substitution $(), backticks, process substitution <(/>().
1142
+ #
1143
+ # SCOPE OF THE NEWLINE REJECT: it closes BLOCK-mode command chaining only. A block
1144
+ # scalar joins continuations with \n, which `bash -c` runs as separate statements —
1145
+ # verified before this was added: a second `touch` line in a block scalar executed.
1146
+ # It contributes ZERO coverage to folded scalars, which join with a SPACE and carry no
1147
+ # shell-active token; those are covered by Step 10.4's probe-verb gate. Do not
1148
+ # cite this reject as a mitigation for the folded-command class.
1149
+ if [[ "$CMD" =~ (\$\(|\`|\<\(|\>\(|\;|\&\&|\|\||\||\>|\<|\&|$'\n'|\$\{?[A-Za-z_]) ]]; then
1150
+ echo "FAIL: discoverability_test.command contains shell-active token; refusing to run."
1151
+ exit 1
1152
+ fi
1153
+
1154
+ # --- Sandbox construction (ADR-175 Layer 1) -------------------------------
1155
+ # The flags live in ONE array so the establishment probe below and the real run
1156
+ # cannot drift apart. MOUNT ORDER IS LOAD-BEARING: bwrap applies mounts in the
1157
+ # order given and this repo lives under /home, so `--tmpfs /home` MUST precede
1158
+ # `--ro-bind "$REPO_ROOT"`. Reversed, the tmpfs silently clobbers the repo bind
1159
+ # and every probe dies with `Can't chdir` — a failure that reads as a bwrap
1160
+ # incompatibility rather than an ordering bug. The repo's own agent sandbox
1161
+ # documents the same semantics (apps/web-platform/server/agent-runner-sandbox-config.ts,
1162
+ # #5733 / PR #5848): broad `--tmpfs` shadows an earlier bind.
1163
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
1164
+ # `--tmpfs /run` removes /run/systemd/resolve/stub-resolv.conf, which
1165
+ # /etc/resolv.conf symlinks to on systemd-resolved hosts. Without rebinding the
1166
+ # resolved target, EVERY curl probe returns rc=6 — indistinguishable from the
1167
+ # #4148 DNS-typo regression this check exists to catch.
1168
+ RESOLV="$(readlink -f /etc/resolv.conf)"
1169
+ # IN A WORKTREE, `.git` IS A FILE, NOT A DIRECTORY — it points at the git common
1170
+ # dir under the bare repo root, which is OUTSIDE $REPO_ROOT. Binding only
1171
+ # $REPO_ROOT therefore breaks every `git` probe with
1172
+ # `fatal: not a git repository`, and `git` is an allowlisted verb. Soleur's whole
1173
+ # workflow runs in worktrees, so this is the common case, not the edge case. The
1174
+ # bind stays READ-ONLY, so the `.git/hooks/pre-commit` write-back escalation is
1175
+ # still closed. Found by the #7393 execution replay — a static verb tally
1176
+ # structurally could not have detected it.
1177
+ # `--tmpfs /var/tmp` is a scratch dir, not a convenience: `/var` is not bound, so
1178
+ # without it `/var/tmp` does not exist inside the sandbox and every probe using
1179
+ # this repo's own documented scratch convention (`TMPDIR=/var/tmp`, per
1180
+ # scripts/test-all.sh) dies on `mktemp: No such file or directory`. bwrap creates
1181
+ # the `/var` parent implicitly, so nothing else from the host `/var` is exposed.
1182
+ GIT_COMMON_DIR="$(git rev-parse --path-format=absolute --git-common-dir)"
1183
+ GIT_BIND=()
1184
+ if [[ "$GIT_COMMON_DIR" != "$REPO_ROOT"/* ]]; then
1185
+ GIT_BIND=(--ro-bind "$GIT_COMMON_DIR" "$GIT_COMMON_DIR")
1186
+ fi
1187
+ BWRAP_ARGS=(
1188
+ --ro-bind /usr /usr --ro-bind /etc /etc
1189
+ --symlink usr/bin /bin --symlink usr/lib /lib
1190
+ --symlink usr/lib64 /lib64 --symlink usr/sbin /sbin
1191
+ --tmpfs /home --tmpfs /root --tmpfs /run --tmpfs /tmp --tmpfs /var/tmp
1192
+ --ro-bind "$RESOLV" "$RESOLV"
1193
+ "${GIT_BIND[@]}"
1194
+ --ro-bind "$REPO_ROOT" "$REPO_ROOT" --chdir "$REPO_ROOT"
1195
+ --dev /dev
1196
+ --unshare-all --share-net --die-with-parent --new-session
1197
+ )
1198
+
1199
+ # FAIL-CLOSED, with no unsandboxed fallback. A degraded sandbox that silently
1200
+ # reverted to the status quo would be worse than no sandbox at all, because this
1201
+ # skill would then claim a boundary that is not there.
1202
+ if ! command -v bwrap >/dev/null 2>&1; then
1203
+ # SOLEUR_* sentinel so this is COUNTABLE across the fleet, not one line of
1204
+ # local stdout. The PostToolUse Bash marker extractor
1205
+ # (apps/web-platform/server/git-lock-marker-telemetry.ts) re-emits SOLEUR_*
1206
+ # markers as structured pino -> journald -> Vector -> Better Stack, so a
1207
+ # silently-disabled Check 10 becomes queryable instead of invisible.
1208
+ # Deliberately NOT in WEDGE_RE: queryable, not paged.
1209
+ echo "SOLEUR_PREFLIGHT_CHECK10_NOSANDBOX reason=bwrap-absent uname=$(uname -s)"
1210
+ case "$(uname -s)" in
1211
+ Darwin) echo "SKIP-NOSANDBOX: Check 10 executes plan-declared commands only inside a bubblewrap (bwrap) sandbox. bubblewrap is Linux-only and has no macOS port, so Check 10 is PERMANENTLY disabled on this host — this is the steady state, not a transient condition. Refusing to run the probe unsandboxed. Tracked for a Darwin sandbox implementation; see ADR-175 Consequences." ;;
1212
+ *) echo "SKIP-NOSANDBOX: Check 10 executes plan-declared commands only inside a bubblewrap (bwrap) sandbox, and bwrap is not installed on this host. Refusing to run the probe unsandboxed. Install it with: sudo apt-get install -y bubblewrap (Debian/Ubuntu) or sudo dnf install -y bubblewrap (Fedora/RHEL)." ;;
1213
+ esac
1214
+ exit 0
1215
+ fi
1216
+
1217
+ # `--proc /proc` must DEGRADE, not fail. /proc cannot be mounted inside a nested
1218
+ # user namespace (a kernel restriction), and preflight runs inside exactly such a
1219
+ # context on the containerized one-shot pipeline path. The repo already pays for
1220
+ # this lesson: `enableWeakerNestedSandbox` in agent-runner-sandbox-config.ts
1221
+ # (#1557) skips `--proc /proc` for the same reason. A hard `--proc` here would
1222
+ # fail establishment in every containerized run and — because the design is
1223
+ # fail-closed — SKIP Check 10 for all of them, which reads exactly like the
1224
+ # fail-closed design working correctly. /proc is not part of what the boundary
1225
+ # protects, so dropping it does not weaken the control: measured, both forms keep
1226
+ # every credential store unreachable and both run curl/node/python3/grep.
1227
+ BWRAP_PROC=(--proc /proc)
1228
+ if ! bwrap "${BWRAP_PROC[@]}" "${BWRAP_ARGS[@]}" true 2>/dev/null; then
1229
+ BWRAP_PROC=()
1230
+ if ! bwrap "${BWRAP_ARGS[@]}" true 2>/dev/null; then
1231
+ # Report what was MEASURED, not a hypothesis (AP-021). The earlier message
1232
+ # named kernel.apparmor_restrict_unprivileged_userns without ever reading it,
1233
+ # while `2>/dev/null` discarded bwrap's own stderr — the only real evidence,
1234
+ # on a surface where nobody can re-run it by hand.
1235
+ BWRAP_ERR="$(bwrap "${BWRAP_ARGS[@]}" true 2>&1 >/dev/null | tail -c 200)"
1236
+ USERNS_MAX="$(cat /proc/sys/user/max_user_namespaces 2>/dev/null || echo unknown)"
1237
+ APPARMOR_RESTRICT="$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null || echo unset)"
1238
+ echo "SOLEUR_PREFLIGHT_CHECK10_NOSANDBOX reason=establish-failed userns_max=${USERNS_MAX} apparmor_restrict=${APPARMOR_RESTRICT}"
1239
+ echo "SKIP-NOSANDBOX: Check 10 could not establish the bwrap sandbox on this host. Measured: max_user_namespaces=${USERNS_MAX}, apparmor_restrict_unprivileged_userns=${APPARMOR_RESTRICT}. bwrap said: $(sanitize "$BWRAP_ERR"). Refusing to run the probe unsandboxed."
1240
+ exit 0
1241
+ fi
1242
+ fi
1243
+
1244
+ # Run with 15s wall-clock cap, capture stdout + exit code separately.
1245
+ # `env -i` SCRUBS Doppler/Supabase secrets from the child env. It is LOAD-BEARING,
1246
+ # not redundant — an earlier revision of this comment called it
1247
+ # "redundant-but-harmless" on the grounds that the on-disk stores are unmounted
1248
+ # anyway, and that reasoning is WRONG: bwrap does not scrub the environment
1249
+ # (measured — a variable set here survives into the sandbox verbatim), and
1250
+ # DOPPLER_TOKEN / GH_TOKEN / ANTHROPIC_API_KEY live in the environment ONLY, with
1251
+ # no on-disk store to unmount. With `curl` allowlisted and --share-net retaining
1252
+ # egress, dropping `-i` exfiltrates every credential in preflight's environment.
1253
+ # Pinned by an anchored assertion in the F2 test; do not weaken either.
1254
+ # PATH is restored explicitly so `curl`/`timeout` are still resolvable; HOME
1255
+ # points at the sandbox tmpfs.
1256
+ #
1257
+ # `</dev/null` is load-bearing: without it the probe inherits preflight's stdin,
1258
+ # so a command that reads stdin either consumes input the caller was still using
1259
+ # or blocks for the full 15s and reports as a timeout. Measured while building
1260
+ # #7393's execution replay, whose own read-loop the probes silently ate.
1261
+ DT_OUT=$(bwrap "${BWRAP_PROC[@]}" "${BWRAP_ARGS[@]}" /usr/bin/env -i PATH=/usr/local/bin:/usr/bin:/bin HOME=/tmp timeout 15s bash -c "$CMD" </dev/null 2>/dev/null; printf 'RC:%d' "$?")
1262
+ DT_RC="${DT_OUT##*RC:}"
1263
+ DT_STDOUT="${DT_OUT%RC:*}"
1264
+
1265
+ # Log-injection guard (re-use Check 5's sanitize() pattern).
1266
+ sanitize() { printf '%s' "$1" | LC_ALL=C tr -d '\000-\037\177' | LC_ALL=C sed $'s/\xe2\x80\xa8//g; s/\xe2\x80\xa9//g'; }
1267
+ DT_STDOUT_SAFE=$(sanitize "$DT_STDOUT")
1268
+
1269
+ # Trailing-newline normalization. `bash -c "echo 200"` returns "200\n"; the
1270
+ # matcher must compare without the trailing newline or "200" never matches.
1271
+ # sanitize() above strips C0 controls 0x00-0x1f including \n, so DT_STDOUT_SAFE
1272
+ # is already newline-free — but document the dependency explicitly so a future
1273
+ # sanitize() refactor that preserves \n does not silently break matching.
1274
+ ```
1275
+
1276
+ The 15-second cap is a hard ceiling. Plans typically prescribe `curl --max-time 10`; the 15s outer cap accommodates 10s curl + 5s DNS + handshake without giving the curl invocation infinite headroom if it lacks `--max-time`.
1277
+
1278
+ **What the sandbox does NOT close, named so no one cites it as closure.**
1279
+
1280
+ `--share-net` **retains the host network namespace** — it does not create a fresh one. So
1281
+ `127.0.0.1` inside the sandbox is the operator's real loopback. Measured on a development
1282
+ workstation from inside the shipped `BWRAP_ARGS`: the sandbox's netns inode is identical to
1283
+ PID 1's, and local Supabase Kong/PostgREST (54321), Postgres (54322) and Studio (54323) are
1284
+ all reachable. A plain `curl http://127.0.0.1:54321/…` passes all three layers.
1285
+
1286
+ The class is **host-loopback pivot**, and it is materially worse than the egress case the
1287
+ earlier revision of this paragraph named: it reaches services that are unauthenticated
1288
+ *precisely because* they are loopback-bound, so the sandbox's "removes the credential
1289
+ stores as files" claim is true for files and false for anything reachable over loopback.
1290
+
1291
+ Outbound egress remains too (`curl` is the dominant probe verb), so repo contents — and,
1292
+ via the read-only git common-dir bind, whole-history and local session state — can be sent
1293
+ to an arbitrary host. The repository is public, which bounds that half; it does not bound
1294
+ the loopback half.
1295
+
1296
+ Closing it needs `--unshare-net` plus a userspace network stack
1297
+ (`slirp4netns --disable-host-loopback` or `pasta`): `--unshare-net` alone kills all egress,
1298
+ which surfaces as `curl` rc=6 — **indistinguishable from the #4148 DNS-typo signal this
1299
+ check exists to catch**. Tracked as #7412, recorded as open in ADR-175 `## Consequences`.
1300
+
1301
+ A `127.0.0.1|localhost|::1` string reject is deliberately NOT used: it is bypassable in one
1302
+ token (`127.1`, `2130706433`, `0x7f000001`, a DNS A-record pointing at loopback, or any
1303
+ wrapped script), and adding it would let this skill claim a boundary that is not there —
1304
+ the denylist mistake ADR-175 exists to retire.
1305
+
1306
+ **Step 10.6: Decision matrix (14 post-parse rows, 1 PASS terminal).**
1307
+
1308
+ | # | State | Detection | Result | Rationale |
1309
+ | --- | --- | --- | --- | --- |
1310
+ | 1 | No PR linked plan file | `$PLAN_PATH` empty after Shared Plan-File Resolution | **SKIP** | Indeterminate — Check 6 will fire if a section is required; Check 10 cannot run without a plan file. |
1311
+ | 2 | Plan exists, no `## Observability` block | `awk` returns empty in Step 10.3 | **FAIL** | Sensitive-path diff requires an Observability block per `hr-observability-as-plan-quality-gate`. |
1312
+ | 3 | Block exists, no `discoverability_test.command` parsed | `$CMD` empty after both Form A + B attempts | **FAIL** | Rule violation — the load-bearing field of the schema is missing. |
1313
+ | 4 | Probe declares `credentials_required` (non-placeholder) | `$CREDS_REQ` non-empty and not a placeholder | **SKIP-DECLARED** | The property has no unauthenticated substitute. Executing it in the sandbox would fail for lack of credentials and prove nothing. Waives verification, not execution — the command never runs. |
1314
+ | 5 | `credentials_required` is placeholder text | `$CREDS_REQ` matches `TODO`/`TBD`/`N/A`/… | **FAIL** | A declaration that says nothing waives nothing (deepen-plan §4.7 placeholder machinery). |
1315
+ | 6 | Verb not on `PROBE_VERB_ALLOWLIST` | [probe-verb-gate.sh](./scripts/probe-verb-gate.sh) exits 1 | **FAIL** | Deny-by-default, with no path-shaped exemption. Schema validation, not a security control — see Step 10.4. The reason names both remedies (repo-relative script; `credentials_required`) and the allowlist-extension route. |
1316
+ | 7 | Sandbox unavailable or cannot be established | `bwrap` absent, or both `--proc` and no-`--proc` establishment attempts fail | **SKIP-NOSANDBOX** | Fail-closed. Never falls back to unsandboxed execution. Its OWN terminal: folded into ordinary SKIP, "the gate verified nothing" was byte-identical to "the gate had nothing to do". bwrap is Linux-only, so on macOS this is the steady state. Emits `SOLEUR_PREFLIGHT_CHECK10_NOSANDBOX` so it is countable across the fleet. |
1317
+ | 8 | Command DNS-fails | `$DT_RC == 6` (curl: "Could not resolve host") | **FAIL** | The hostname-typo class — the exact #4148 regression. |
1318
+ | 9 | Command times out | `$DT_RC == 28` (curl) OR `$DT_RC == 124` (timeout(1)) | **FAIL** | Endpoint unreachable; DNS resolved but no response in 15s. |
1319
+ | 10 | Command requires creds not in Doppler (auth-gated probe) | `$DT_RC == 22` AND HTTP 401/403 AND `$EXPECTED` does NOT explicitly list 401/403 | **SKIP** | Auth-gated probe with no operator creds; surface diagnostic suggesting a `credentials_required` declaration or a Doppler-fetched probe variant. |
1320
+ | 10b | Command not found in the sandbox | `$DT_RC == 127` | **FAIL** | The probe's program is not on the sandbox `PATH` (`/usr/local/bin:/usr/bin:/bin`), which is deliberately NOT widened — that would be an authority grant inside a change that narrows authority. Wrap the probe in a repo-relative script, or use an allowlisted verb. Reported as its own state because falling through to row 11 blamed "expectation drift" for a cause the code measured exactly. |
1321
+ | 11 | Command returns a code/output the plan's `expected_output` does NOT include | NO token of `$EXPECTED` is present in `$DT_STDOUT_SAFE` (see the semantics paragraph below; this cell read `$DT_STDOUT_SAFE` not present in `$EXPECTED` until #8412 — backwards, and contradicted by both the paragraph and the shipped `matchExpected` in `plugins/soleur/test/lib/discoverability-test-parser.ts`) | **FAIL** | Plan's expectation drifted from production reality. |
1322
+ | 12 | Command returns expected output | All other paths — `$DT_RC == 0` AND stdout matches `$EXPECTED` | **PASS** | Invariant proven by live execution inside the sandbox. |
1323
+
1324
+ **Expected-output matching semantics.** When `$EXPECTED` is a comma-separated or "or"-joined list (e.g., `200 or 401`, `200, 401`, `["200","401"]`), tokenize on `,|\s+or\s+|\bor\b|[\`"\[\]/]+` and treat as a list. Match if any token is a non-empty substring of `$DT_STDOUT_SAFE`. When `$EXPECTED` is a single value, substring-match. The tokenizer accepts both `200` and `"200"`.
1325
+
1326
+ **Step 10.7: Headless mode behaviour.**
1327
+
1328
+ On **FAIL**, abort with the diagnostic table (command, exit code, sanitized stdout, expected). On **PASS** or **SKIP**, continue silently. On **SKIP-DECLARED**, continue but **emit one line** naming the declared scope verbatim — this terminal exists to be seen, and silencing it would defeat the drift control it was built for. On **SKIP-NOSANDBOX**, continue but **emit one line** plus the `SOLEUR_PREFLIGHT_CHECK10_NOSANDBOX` sentinel: the check did not run, and that fact must reach both the operator and the fleet-level marker pipeline.
1329
+
1330
+ **Step 10.8: Interactive mode behaviour.**
1331
+
1332
+ On **FAIL**, present the failure reason + sanitized command + diagnostic and offer **AskUserQuestion**:
1333
+
1334
+ 1. "Fix the plan's `discoverability_test.command` now" — open the plan file at the line of the `discoverability_test:` key. Re-run Check 10.
1335
+ 2. "Skip — temporarily defer (logs a trim-tracker issue)" — `gh issue create --label 'priority/p3-low,chore'` with the command, the exit code and the `expected_output` as the body. **Never include `$DT_STDOUT_SAFE` in the issue body.** `sanitize()` strips only C0 controls, not secrets or PII: a probe's stdout routinely carries log rows with user emails, client IPs, and bearer/`dp.ct.*` tokens, and this repository is PUBLIC. The captured stdout stays in the local run output for the operator; it does not get published. Continue the preflight run with this check noted as DEFERRED.
1336
+ 3. "Abort — fix elsewhere" — stop the pipeline.
1337
+
1338
+ **Result:**
1339
+
1340
+ - **PASS** — Sensitive-path diff with valid plan-linked Observability block AND command ran inside the sandbox AND output matches `expected_output`.
1341
+ - **FAIL** — Sensitive-path diff with any of: missing Observability block, missing `discoverability_test.command`, command requires SSH, verb not on `PROBE_VERB_ALLOWLIST` (no path-shaped exemption), placeholder `credentials_required`, command contains shell substitution, DNS failure, timeout, or output mismatch.
1342
+ - **SKIP** — No sensitive paths touched, OR no PR available, OR no plan file linked from PR body, OR command is auth-gated with no operator creds.
1343
+ - **SKIP-NOSANDBOX** — The bwrap sandbox is unavailable or could not be established (fail-closed — never an unsandboxed fallback). Reported distinctly from **SKIP** because "the check could not run" and "the check had nothing to do" are different facts, and only one of them means a security gate is dark. Emits a `SOLEUR_*` sentinel so it is countable across the fleet rather than one line of local stdout.
1344
+ - **SKIP-DECLARED** — The plan declares a non-placeholder `discoverability_test.credentials_required`. The command was **not executed**; the declared scope is surfaced verbatim. Reported distinctly from **SKIP** everywhere (headless line, Phase 2 aggregate row) because it is a verification waiver a reviewer must be able to see.
1345
+
1346
+ ### Check 7: Canary Probe Set Covers Authenticated Surface
1347
+
1348
+ **Path-gated:** only runs when the cached path-set from Phase 0 Step 0.1 (`"$PREFLIGHT_TMP/preflight-diff-files.txt"`) contains `apps/web-platform/infra/ci-deploy.sh`. Otherwise return **SKIP** with note: "ci-deploy.sh untouched." The path predicate is identical to the original `git diff --name-only origin/main...HEAD` form — only the diff source is changed.
1349
+
1350
+ **Rationale:** The legacy canary probed only `/health`, which is middleware-bypassed and never imports `lib/supabase/client.ts`. A broken inlined `NEXT_PUBLIC_SUPABASE_*` value would pass canary and ship to prod (PR #3014 incident class). The canary contract — documented in `knowledge-base/engineering/operations/runbooks/canary-probe-set.md` — requires probes for every public route (`/login`) AND auth-gated entry (`/dashboard`) PLUS a body-content sentinel rejection.
1351
+
1352
+ **Step 7.1: Assert /dashboard probe presence.**
1353
+
1354
+ ```bash
1355
+ grep -c '/dashboard' apps/web-platform/infra/ci-deploy.sh
1356
+ ```
1357
+
1358
+ The count MUST be ≥ 1.
1359
+
1360
+ **Step 7.2: Assert /login probe presence.**
1361
+
1362
+ ```bash
1363
+ grep -c '/login' apps/web-platform/infra/ci-deploy.sh
1364
+ ```
1365
+
1366
+ The count MUST be ≥ 1.
1367
+
1368
+ **Step 7.3: Assert structured-marker body-content rejection.**
1369
+
1370
+ ```bash
1371
+ grep -F 'data-error-boundary=' apps/web-platform/infra/ci-deploy.sh
1372
+ ```
1373
+
1374
+ The grep MUST exit 0 — the canary must reject any rendered HTML containing the `data-error-boundary` attribute emitted by `components/error-boundary-view.tsx`. The structured marker survives copy edits; without it, a copy change would silently disable the rollback gate (PR #3014 lesson).
1375
+
1376
+ **Result:**
1377
+
1378
+ - **PASS** — all three greps satisfy their conditions.
1379
+ - **FAIL** — any of: `/dashboard` missing from canary, `/login` missing from canary, or the error-sentinel body check absent. Operator must restore the layered probe before re-running preflight.
1380
+ - **SKIP** — `apps/web-platform/infra/ci-deploy.sh` was not modified in this branch.
1381
+
1382
+ ### Check 11: Domain-Model Register Drift
1383
+
1384
+ Enforces the domain-model register's maintenance contract (#5871): a PR that changes a
1385
+ business rule must not leave the register with a **stale citation** (a cited migration/symbol
1386
+ that no longer resolves). Consumes the deterministic analyzer `domain-model-drift.sh`
1387
+ (#5754, ADR-076). The register (`knowledge-base/engineering/architecture/domain-model.md`) is a
1388
+ **curated subset**, so "undocumented source facts" is ~every un-curated table by design — it is
1389
+ **advisory-only, never a FAIL input**. This check gates on **stale citations only**.
1390
+
1391
+ **Step 11.1 — diff-scope (fast-path SKIP).** SKIP unless the cached path-set
1392
+ (`"$PREFLIGHT_TMP/preflight-diff-files.txt"`) matches a business-rule surface OR the register itself:
1393
+
1394
+ ```bash
1395
+ DIFF="$PREFLIGHT_TMP/preflight-diff-files.txt"
1396
+ # Fail-safe: never SKIP on a missing/empty cache — recompute inline, then run if still empty.
1397
+ if [[ ! -s "$DIFF" ]]; then git diff --name-only origin/main...HEAD > "$DIFF" 2>/dev/null || true; fi
1398
+ if [[ -s "$DIFF" ]] && ! grep -qE '(^|/)apps/web-platform/supabase/migrations/.*\.sql$|(^|/)apps/web-platform/server/workspace-resolver\.ts$|(^|/)knowledge-base/engineering/architecture/domain-model\.md$' "$DIFF"; then
1399
+ echo "SKIP — no business-rule surface (migration/workspace-resolver/register) in diff"; exit 0
1400
+ fi
1401
+ ```
1402
+
1403
+ The register-file literal is load-bearing: a register-only edit can introduce a stale citation.
1404
+ Check 11 reads `origin/main...HEAD` (PR-independent), so it **RUNS pre-PR** — do NOT "fix" it to
1405
+ SKIP-on-no-PR (that would fail it open).
1406
+
1407
+ **Step 11.2 — run + parse the stale sub-count** (line-anchored; the raw exit code is NOT the signal):
1408
+
1409
+ ```bash
1410
+ bash "${CLAUDE_PLUGIN_ROOT}/scripts/domain-model-drift.sh" drift --repo . \
1411
+ --register knowledge-base/engineering/architecture/domain-model.md > "$PREFLIGHT_TMP/register-drift.txt" 2>&1; rc=$?
1412
+ stale=$(grep -oE '^## Stale register citations \([0-9]+\)' "$PREFLIGHT_TMP/register-drift.txt" | head -1 | grep -oE '[0-9]+')
1413
+ stale=${stale:-0}
1414
+ ```
1415
+
1416
+ The `^` anchor + `head -1` guarantee a single integer from the canonical column-0 header — an
1417
+ unanchored grep could match a verbatim-SQL predicate line echoing the substring (multiline capture
1418
+ breaks the numeric test).
1419
+
1420
+ **Result:**
1421
+
1422
+ - **PASS** — `rc == 0` (register clean). The "Undocumented source facts (M)" count is surfaced by the
1423
+ advisory review note, never here.
1424
+ - **FAIL** — `stale > 0`: "domain-model register has $stale stale citation(s) — the register cites a
1425
+ file/symbol that no longer resolves. Fix the cited row(s), or run `soleur:sync domain-model`. If a
1426
+ citation backticks a *filename*, unbacktick it (known citation-parser false-positive — see
1427
+ `knowledge-base/project/learnings/best-practices/2026-07-01-domain-model-register-curation-citation-parser-and-grep-validation.md`)."
1428
+ - **FAIL** — `rc == 2` (analyzer error / unanalyzable source): "register-drift check could not run
1429
+ (analyzer exit 2) — inspect `--repo`/jq/migrations dir; NOT a drift finding."
1430
+ - **FAIL** — `rc == 3` (secret-refuse): "domain-model analyzer refused to emit — a secret-shaped
1431
+ substring was found in extracted structural text. Likely a recently-changed migration column/value
1432
+ matching `sk_test`/`ghp_`/`AKIA…`/`-----BEGIN` (a benign column name can false-positive). Inspect the
1433
+ newest `apps/web-platform/supabase/migrations/*.sql`."
1434
+ - **SKIP** — no business-rule surface (or the register) in the diff.
1435
+
1436
+ ### Check 12: Encryption Posture
1437
+
1438
+ Enforces the encryption-posture ledger's maintenance contract (ADR-140): every persistent
1439
+ store and cross-component connection Soleur operates must carry a ledger row whose cited
1440
+ evidence resolves to real code, and any accepted plaintext/unverified-TLS exception must carry
1441
+ an open, unexpired tracking issue. Consumes the deterministic analyzer
1442
+ `lint-encryption-posture.py` (repo-root `scripts/`) — this check does NOT reimplement its PASS/FAIL logic in
1443
+ prose; it shells out and relays the exit code.
1444
+
1445
+ **Step 12.1 — diff-scope (fast-path SKIP).** SKIP unless the cached path-set
1446
+ (`"$PREFLIGHT_TMP/preflight-diff-files.txt"`) matches a store-class or connection-class surface:
1447
+
1448
+ ```bash
1449
+ DIFF="$PREFLIGHT_TMP/preflight-diff-files.txt"
1450
+ # Fail-safe: never SKIP on a missing/empty cache — recompute inline, then run if still empty.
1451
+ if [[ ! -s "$DIFF" ]]; then git diff --name-only origin/main...HEAD > "$DIFF" 2>/dev/null || true; fi
1452
+ if [[ -s "$DIFF" ]] && ! grep -qE '\.tf$|supabase/migrations/.*\.sql$|cloud-init.*\.ya?ml$|docker-compose.*\.ya?ml$' "$DIFF"; then
1453
+ echo "SKIP — no store-class or connection-class surface (*.tf / migration / cloud-init / docker-compose) in diff"; exit 0
1454
+ fi
1455
+ ```
1456
+
1457
+ This mirrors "I do not apply", never "I cannot prove it" (`2026-04-27-preflight-security-gates-skip-vs-fail-defaults.md`) — an empty/missing diff cache falls through to run, it never SKIPs by default.
1458
+
1459
+ **Step 12.2 — run the Layer A sweep:**
1460
+
1461
+ ```bash
1462
+ python3 scripts/lint-encryption-posture.py --repo-sweep > "$PREFLIGHT_TMP/encryption-posture.txt" 2>&1; rc=$?
1463
+ ```
1464
+
1465
+ **Result:**
1466
+
1467
+ - **PASS** — `rc == 0`. Every store/connection in the repo has a ledger row whose cited evidence
1468
+ resolves to real code, and every accepted exception carries an open, unexpired tracking issue.
1469
+ - **FAIL** — `rc != 0`: relay the script's own output (`cat "$PREFLIGHT_TMP/encryption-posture.txt"`)
1470
+ verbatim — the classes it can report include an unledgered store, an unresolvable citation, an
1471
+ expired or untracked exception, or a store/connection disclosed as encrypted while its ledger row
1472
+ says otherwise. Do not re-derive the verdict from this SKILL.md — the script is the single source
1473
+ of truth for the pass/fail decision.
1474
+ - **SKIP** — the cached path-set contains zero `.tf` / store-class / connection-class paths.
1475
+
1476
+ ### Check 13: Founder-Stated Check
1477
+
1478
+ Runs the check a founder wrote in their own words, approved as an exact command, and froze before work started (ADR-275). The decisions live in [founder-check.py](./scripts/founder-check.py); this check relays them and never re-derives a verdict in prose. The wrapper, the outcome tables and the prompts are in [check-13-founder-check.md](${CLAUDE_PLUGIN_ROOT}/skills/preflight/references/check-13-founder-check.md).
1479
+
1480
+ 1. **Resolve.** Run `founder-check.py verify` (reference section 1). `NO-BLOCK` is SKIP with the pinned banner; a missing block with freeze evidence is FAIL, never SKIP; a `FAIL` outcome is `BLOCK-REJECTED`; `UNTRUSTED` is a FAIL that never runs (the command and its author are shown); `CHANGED-SINCE-APPROVAL` stops and is decided in Phase 2.
1481
+ 2. **Run only through Step 10.5.** The approved command runs in the Step 10.5 sandbox via the reference's wrapper, which reads it from a file `verify` wrote and runs `true` first as the sandbox-health control. Check 13 declares no sandbox of its own and never executes the command any other way. Output the wrapper does not map is INVALID, never a pass.
1482
+ 3. **Classify and record.** `founder-check.py classify` is the one chokepoint and reads the verify record, not argv; `founder-check.py log` is the only writer of outcomes and prints the metadata-only `SOLEUR_FOUNDER_CHECK_RESULT` marker. After the last row, `founder-check.py commit-log` stages and commits only the log as `founder-check: log`.
1483
+ 4. **Say only what is true.** A pass reads `founder-check.py text aggregate-pass` in the aggregate row, beside the full sentence from `founder-check.py text pass`. Every founder-facing sentence is printed from `founder-check.py text <key>`, never typed from this file.
1484
+
1485
+ A block present with no sandbox (`founder-check.py text no-sandbox`) is a FAIL in every mode: a gate that went dark must not read as green.
1486
+
1487
+ ## Phase 2: Aggregate Go/No-Go Report
1488
+
1489
+ After all checks complete, aggregate results into a structured report:
1490
+
1491
+ ```markdown
1492
+ ## Preflight Results
1493
+
1494
+ | Check | Result | Details |
1495
+ |-------|--------|---------|
1496
+ | Not-Bare-Repo | PASS/FAIL | <details> |
1497
+ | DB Migration Status | PASS/FAIL/SKIP | <details> |
1498
+ | Security Headers | PASS/FAIL/SKIP | <details> |
1499
+ | Lockfile Consistency | PASS/FAIL/SKIP | <details> |
1500
+ | Environment Isolation | PASS/FAIL/SKIP | <details> |
1501
+ | Production Bundle Supabase Host | PASS/FAIL/SKIP | <details> |
1502
+ | Brand-Survival Self-Review | PASS/FAIL/SKIP | <details> |
1503
+ | Canary Probe Set Covers Auth Surface | PASS/FAIL/SKIP | <details> |
1504
+ | SW Cache Bump on Client-Bundle Fix | PASS/FAIL/SKIP | <details> |
1505
+ | Node-Only Encodings Banned in Client-Bundle | PASS/FAIL | <details> |
1506
+ | Discoverability Test Execution | PASS/FAIL/SKIP/SKIP-DECLARED/SKIP-NOSANDBOX | <details> — on SKIP-DECLARED, quote the declared `credentials_required` scope verbatim so the waiver is visible in the aggregate; on SKIP-NOSANDBOX, state plainly that Check 10 did not execute on this host and why |
1507
+ | Founder-Stated Check | ran-returned-success/FOUNDER-CONFIRMED/FAIL/SKIP/OVERRIDDEN | `founder-check.py text aggregate-pass` (never a bare PASS) or `text aggregate-judgement`, the exact command and expected string beside it; roll-up per reference section 4 |
1508
+
1509
+ **Overall: PASS / FAIL**
1510
+ ```
1511
+
1512
+ On OVERRIDDEN add the matching `founder-check.py text overridden-failed` / `overridden-invalid` / `overridden-changed` / `overridden-rejected` line. With no block add nothing.
1513
+
1514
+ ### If any FAIL
1515
+
1516
+ **Headless mode:** Abort with: "Preflight FAILED. See results above. Fix the issues and re-run `soleur:ship`."
1517
+
1518
+ **Interactive mode:** Present findings table, then use **AskUserQuestion tool**:
1519
+
1520
+ - Question: "Preflight found issues. How to proceed?"
1521
+ - Options:
1522
+ 1. "Fix and retry" -- fix the issues, then re-run preflight from Phase 1
1523
+ 2. "Abort" -- stop the pipeline
1524
+
1525
+ **Founder check (Check 13) prompts run here**, after the parallel checks finish: retry, restore or change the check, or continue anyway (recorded as `OVERRIDDEN`, with a reason and the cause it overrides) — reference section 5.
1526
+
1527
+ ### If all PASS or SKIP
1528
+
1529
+ Print the summary table and continue.
1530
+
1531
+ ## Preflight Complete
1532
+
1533
+ Preflight validation passed. Return control to the calling orchestrator.
1534
+
1535
+ ## Sharp Edges
1536
+
1537
+ - **Check 13 reuses Step 10.5 by wrapper, never by copy.** Step 10.5 is a fence an agent follows, so reuse means running it inside a command substitution with `CMD` set. A second `BWRAP_ARGS` array would fork the sandbox; `preflight-check10-suite-integrity.test.sh` asserts the Check 13 section declares none. Check 13 is not path-gated like Check 10, so it widens what runs on a checked-out PR head: the authorship anchor in `founder-check.py verify` (`UNTRUSTED`, a FAIL that never runs) is what keeps a contributor's self-consistent plan from executing on the operator's machine. It makes an override legible and attributable; it does not authenticate the founder. Pins fix the named script only, not what it loads.
1538
+ - **Quad-SSOT for `SENSITIVE_PATH_RE`.** The literal lives at Check 6 Step 6.1, Check 10 Step 10.1, `plugins/soleur/skills/deepen-plan/SKILL.md` Phase 4.6 Step 2, AND `plugins/soleur/skills/review/scripts/fix-round-seats.sh` (ADR-267). All four MUST stay byte-identical — `plugins/soleur/test/review-tier-parity.test.ts` pins the script copy against this file. The Check 10 regression test (`plugins/soleur/test/preflight-discoverability-test.test.ts`) asserts ≥2 matches in `preflight/SKILL.md`; the canonical grep is `grep -cF "SENSITIVE_PATH_RE='^(apps/web-platform" plugins/soleur/skills/preflight/SKILL.md plugins/soleur/skills/deepen-plan/SKILL.md`. `grep -cF` is substring-based and tolerates the 2-space indentation difference between top-level (preflight) and markdown-bullet (deepen-plan) contexts — keep AC2's grep un-anchored.
1539
+ - **Shared Plan-File Resolution is a SSOT.** Both Check 6 Step 6.2 and Check 10 Step 10.2 call it. A future PR that changes the scrub/extract logic must edit the shared sub-section once — both consumers pick it up. Do NOT copy-paste the logic back into a caller block.
1540
+ - **Check 10 parser duality (Form A YAML vs Form B prose+fence).** PR #4148 used Form B; the canonical template uses Form A. Both must be accepted OR Check 10 silently SKIPs on currently-valid plans. The TS reference impl at `plugins/soleur/test/lib/discoverability-test-parser.ts` exercises both forms across every fixture in `plugins/soleur/test/fixtures/preflight-check-10/`.
1541
+ - **Rule order in `parse-form-a.awk` IS the bug (#6772).** The inline rule `/^[[:space:]]*command:/` matches EVERY `command:` line, including `command: >-` and `command: |`. If it is ever moved ahead of the fold/block header rules it returns the literal indicator, which then self-rejects against Step 10.5's shell-active `>` branch — a check that cannot parse its input, reporting a shell injection the plan does not have. AC1 and fixtures F1–F3 are the pins; do not drop them when refactoring.
1542
+ - **Anchoring the header regex is a bug generator.** A bare `$` anchor made `command: >- # note` fall through to inline and reproduce #6772 exactly. Any future tightening of the header must keep the comment-tolerant `(#.*)?$` tail and re-run the F1–F3 comment column.
1543
+ - **`indent()` returns 0 on a blank line**, which is `<= key` for every scalar. The blank-line skip rule MUST stay above the indent terminator or every scalar ends at its first blank line (fixture N6).
1544
+ - **`env -i` scrubs environment *variables*, never *files or sockets* — and an ephemeral `$HOME` is the same mistake wearing a better name.** `env -i` cannot remove an on-disk credential store, and setting `HOME` to a temp dir only changes where a CLI *looks*: measured during #7393, an absolute-path read of the operator's home Doppler config returned a live 294-byte service token under `HOME=$(mktemp -d)`, and a glob read across `/home/*/…` did too. Never cite either as a mitigation for a credential-bearing command. What covers that class is the Step 10.5 **filesystem boundary** (bwrap: `/home`, `/root`, `/run` replaced with tmpfs; repo bound read-only) — the verb allowlist in Step 10.4 bounds *legibility and maintenance*, not authority.
1545
+ - **Probe the invariant, not the proxy — this gate's own history is the cautionary tale.** #7393's first design measured `doppler configure get token`'s **exit code** (0 even with no token) and read it as "credential still reachable". Corrected to token length plus a live read, it then measured **credential discovery** and reported it as **credential reachability** — and shipped a Sharp Edge congratulating itself for escaping exactly that trap, one section above the place it committed it. Any claim about credential reachability must be tested by reading the file **at its absolute path, from inside the real execution environment**.
1546
+ - **A fail-closed gate can fail closed for the wrong reason, and it looks identical to working.** `--proc /proc` cannot be mounted inside a nested user namespace (the repo already knows this — `enableWeakerNestedSandbox`, #1557). A hard `--proc` would fail sandbox establishment in every containerized run, and the fail-closed design would then SKIP Check 10 everywhere — a blanket disablement whose telemetry is indistinguishable from correct operation. Whenever a gate's failure mode is "skip", enumerate the environments where establishment fails *for reasons unrelated to the threat*, and **degrade** rather than skip in those.
1547
+ - **`--tmpfs /home` must precede `--ro-bind "$REPO_ROOT"`.** bwrap applies mounts in order and this repo lives under `/home`. Reversed, the tmpfs silently clobbers the repo bind and every probe dies with `Can't chdir` — a failure that reads as a bwrap incompatibility rather than an ordering bug. The agent sandbox's module header documents the same tmpfs-shadows-earlier-binds semantics (#5733 / PR #5848); two independent discoveries of one rule.
1548
+ - **`--tmpfs /run` breaks DNS on systemd-resolved hosts.** The resolver config at /etc/resolv.conf is a symlink into the systemd-resolved stub under /run; tmpfs'ing `/run` removes the target, and every `curl` probe then returns rc=6 — **indistinguishable from the #4148 DNS-typo regression Check 10 exists to catch**. Bind the resolved target (`readlink -f` the resolver config) after the tmpfs. Written unquoted deliberately: the skill-security-scan `path-traversal-absolute` rule matches a quote or backtick immediately followed by a system path, so presenting these as code literals turns explanatory prose into a REVIEW finding.
1549
+ - **`awk`, `sed` and `find` are `bash -c` in disguise.** Measured: `awk 'BEGIN{system("…")}'` executes arbitrary commands and passes the shell-active reject (no `;`, `&&`, `|`, `$(`). Rejecting `bash -c` while allowlisting `awk` would be incoherent, so all three are excluded — as are `python3 -c`, `node -e`, `bun -e`, GNU `sed`'s `e` flag, and `find -exec`.
1550
+ - **`git ls-files --error-unmatch` is not a review oracle.** It interrogates the **PR-head index** — the attacker's own branch — and preflight runs *before* merge. "Tracked" and "reviewed" are different properties. That is why the program-path rule here is a pure string rule and the sandbox, not a tracking check, is what contains the script.
1551
+ - **The shell-active reject does not bound a folded command.** Folding joins with a space, so a folded scalar has no `;`/`|`/`$()` by construction and passes the reject automatically — it can append *arguments* but never chain a command. That makes fold safer than block for injection, but it also means no token in that reject set constrains what a folded command *is*. Reasoning "the reject will catch it" about a folded command is reasoning about the wrong gate; the verb allowlist and the sandbox are the gates that apply.
1552
+ - **`bash -c "$CMD"` stdout always ends in `\n`.** The matcher MUST normalize trailing newlines (via `sanitize()` or `${var%$'\n'}`) before substring comparison, or `expected_output: 200` fails when production correctly emits `200\n`.
1553
+ - **The `\b` word-boundary trap.** Bash `[[ $x =~ \bssh \b ]]` matches `ssh ` only when whitespace is on BOTH sides; trailing-EOF or trailing-newline `ssh ` does NOT match. Always use `(^|[[:space:]])ssh([[:space:]]|$)` — the canonical Check 10 reject form — when checking for `ssh ` in operator-facing prose. <!-- markdownlint-disable-line MD038 -->