@soleur/opencode 0.0.0-stage → 3.354.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (832) hide show
  1. package/.claude-plugin/agents.manifest.json +476 -0
  2. package/.claude-plugin/plugin.json +42 -0
  3. package/README.md +44 -2
  4. package/agents/community/.gitkeep +0 -0
  5. package/agents/engineering/cto.md +87 -0
  6. package/agents/engineering/design/ddd-architect.md +95 -0
  7. package/agents/engineering/discovery/agent-finder.md +214 -0
  8. package/agents/engineering/discovery/functional-discovery.md +182 -0
  9. package/agents/engineering/infra/infra-security.md +151 -0
  10. package/agents/engineering/infra/platform-strategist.md +109 -0
  11. package/agents/engineering/infra/terraform-architect.md +171 -0
  12. package/agents/engineering/prompt-engineer.md +37 -0
  13. package/agents/engineering/research/best-practices-researcher.md +115 -0
  14. package/agents/engineering/research/framework-docs-researcher.md +93 -0
  15. package/agents/engineering/research/git-history-analyzer.md +53 -0
  16. package/agents/engineering/research/learnings-researcher.md +285 -0
  17. package/agents/engineering/research/repo-research-analyst.md +118 -0
  18. package/agents/engineering/review/agent-native-reviewer.md +279 -0
  19. package/agents/engineering/review/architecture-strategist.md +61 -0
  20. package/agents/engineering/review/code-quality-analyst.md +92 -0
  21. package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
  22. package/agents/engineering/review/data-integrity-guardian.md +82 -0
  23. package/agents/engineering/review/data-migration-expert.md +98 -0
  24. package/agents/engineering/review/deployment-verification-agent.md +183 -0
  25. package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
  26. package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
  27. package/agents/engineering/review/legacy-code-expert.md +55 -0
  28. package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
  29. package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
  30. package/agents/engineering/review/performance-oracle.md +130 -0
  31. package/agents/engineering/review/security-sentinel.md +167 -0
  32. package/agents/engineering/review/semgrep-sast.md +36 -0
  33. package/agents/engineering/review/test-design-reviewer.md +150 -0
  34. package/agents/engineering/review/user-impact-reviewer.md +70 -0
  35. package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
  36. package/agents/finance/budget-analyst.md +29 -0
  37. package/agents/finance/cfo.md +44 -0
  38. package/agents/finance/financial-reporter.md +30 -0
  39. package/agents/finance/revenue-analyst.md +30 -0
  40. package/agents/legal/clo.md +115 -0
  41. package/agents/legal/legal-compliance-auditor.md +110 -0
  42. package/agents/legal/legal-document-generator.md +134 -0
  43. package/agents/marketing/analytics-analyst.md +25 -0
  44. package/agents/marketing/brand-architect.md +181 -0
  45. package/agents/marketing/cmo.md +76 -0
  46. package/agents/marketing/conversion-optimizer.md +19 -0
  47. package/agents/marketing/copywriter.md +20 -0
  48. package/agents/marketing/fact-checker.md +70 -0
  49. package/agents/marketing/growth-strategist.md +145 -0
  50. package/agents/marketing/paid-media-strategist.md +28 -0
  51. package/agents/marketing/pricing-strategist.md +17 -0
  52. package/agents/marketing/programmatic-seo-specialist.md +25 -0
  53. package/agents/marketing/retention-strategist.md +23 -0
  54. package/agents/marketing/seo-aeo-analyst.md +164 -0
  55. package/agents/operations/coo.md +50 -0
  56. package/agents/operations/ops-advisor.md +78 -0
  57. package/agents/operations/ops-provisioner.md +101 -0
  58. package/agents/operations/ops-research.md +52 -0
  59. package/agents/operations/service-automator.md +278 -0
  60. package/agents/product/business-validator.md +206 -0
  61. package/agents/product/competitive-intelligence.md +89 -0
  62. package/agents/product/cpo.md +63 -0
  63. package/agents/product/design/ux-design-lead.md +209 -0
  64. package/agents/product/spec-flow-analyzer.md +122 -0
  65. package/agents/sales/cro.md +44 -0
  66. package/agents/sales/deal-architect.md +29 -0
  67. package/agents/sales/outbound-strategist.md +29 -0
  68. package/agents/sales/pipeline-analyst.md +29 -0
  69. package/agents/support/cco.md +48 -0
  70. package/agents/support/community-manager.md +570 -0
  71. package/agents/support/ticket-triage.md +70 -0
  72. package/bin/soleur-opencode.js +29 -0
  73. package/commands/go.md +660 -0
  74. package/commands/help.md +273 -0
  75. package/commands/sync.md +977 -0
  76. package/commands/version.md +103 -0
  77. package/hooks/antigravity-hooks.json +84 -0
  78. package/hooks/antigravity-session-start.sh +15 -0
  79. package/hooks/bare-root-cwd-guard.sh +131 -0
  80. package/hooks/browser-snapshot-credential-guard.sh +200 -0
  81. package/hooks/codex-session-start.sh +9 -0
  82. package/hooks/compaction-state.sh +525 -0
  83. package/hooks/destructive-command-guard.sh +1387 -0
  84. package/hooks/devin-session-start.sh +90 -0
  85. package/hooks/ext-filter-detect.sh +171 -0
  86. package/hooks/hooks.json +133 -0
  87. package/hooks/lib/hook-tool-kind.sh +24 -0
  88. package/hooks/lib/log-rotation.sh +176 -0
  89. package/hooks/lib/shell-argv.pl +576 -0
  90. package/hooks/memory-backstop.sh +1114 -0
  91. package/hooks/operator-stage-approval.sh +391 -0
  92. package/hooks/stop-hook.sh +367 -0
  93. package/hooks/unkept-promise-hook.sh +383 -0
  94. package/hooks/version-banner.sh +127 -0
  95. package/hooks/welcome-hook.sh +50 -0
  96. package/install.js +29 -0
  97. package/lib/agent-registry.ts +154 -0
  98. package/lib/c4-canonical-cli.mjs +34 -0
  99. package/lib/c4-canonical.mjs +46 -0
  100. package/lib/c4-from-components.ts +554 -0
  101. package/lib/go-routing.ts +81 -0
  102. package/lib/grok-inspect-contract.ts +311 -0
  103. package/lib/harness-model-map.ts +101 -0
  104. package/lib/harness-parity.ts +726 -0
  105. package/lib/harness-tool-map.ts +180 -0
  106. package/lib/harness.ts +716 -0
  107. package/lib/heartbeat-live-reconcile.ts +1153 -0
  108. package/lib/heartbeat-manifest.ts +361 -0
  109. package/lib/kb-coverage.ts +252 -0
  110. package/lib/pr-merge-poll.ts +103 -0
  111. package/lib/workflow-fidelity.ts +388 -0
  112. package/opencode/INSTRUCTIONS.md +171 -0
  113. package/opencode/agents/engineering/cto.md +10 -0
  114. package/opencode/agents/engineering/design/ddd-architect.md +10 -0
  115. package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
  116. package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
  117. package/opencode/agents/engineering/infra/infra-security.md +10 -0
  118. package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
  119. package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
  120. package/opencode/agents/engineering/prompt-engineer.md +10 -0
  121. package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
  122. package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
  123. package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
  124. package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
  125. package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
  126. package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
  127. package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
  128. package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
  129. package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
  130. package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
  131. package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
  132. package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
  133. package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
  134. package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
  135. package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
  136. package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
  137. package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
  138. package/opencode/agents/engineering/review/performance-oracle.md +10 -0
  139. package/opencode/agents/engineering/review/security-sentinel.md +10 -0
  140. package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
  141. package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
  142. package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
  143. package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
  144. package/opencode/agents/finance/budget-analyst.md +10 -0
  145. package/opencode/agents/finance/cfo.md +10 -0
  146. package/opencode/agents/finance/financial-reporter.md +10 -0
  147. package/opencode/agents/finance/revenue-analyst.md +10 -0
  148. package/opencode/agents/legal/clo.md +10 -0
  149. package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
  150. package/opencode/agents/legal/legal-document-generator.md +10 -0
  151. package/opencode/agents/marketing/analytics-analyst.md +10 -0
  152. package/opencode/agents/marketing/brand-architect.md +10 -0
  153. package/opencode/agents/marketing/cmo.md +10 -0
  154. package/opencode/agents/marketing/conversion-optimizer.md +10 -0
  155. package/opencode/agents/marketing/copywriter.md +10 -0
  156. package/opencode/agents/marketing/fact-checker.md +10 -0
  157. package/opencode/agents/marketing/growth-strategist.md +10 -0
  158. package/opencode/agents/marketing/paid-media-strategist.md +10 -0
  159. package/opencode/agents/marketing/pricing-strategist.md +10 -0
  160. package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
  161. package/opencode/agents/marketing/retention-strategist.md +10 -0
  162. package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
  163. package/opencode/agents/operations/coo.md +10 -0
  164. package/opencode/agents/operations/ops-advisor.md +10 -0
  165. package/opencode/agents/operations/ops-provisioner.md +10 -0
  166. package/opencode/agents/operations/ops-research.md +10 -0
  167. package/opencode/agents/operations/service-automator.md +10 -0
  168. package/opencode/agents/product/business-validator.md +10 -0
  169. package/opencode/agents/product/competitive-intelligence.md +10 -0
  170. package/opencode/agents/product/cpo.md +10 -0
  171. package/opencode/agents/product/design/ux-design-lead.md +10 -0
  172. package/opencode/agents/product/spec-flow-analyzer.md +10 -0
  173. package/opencode/agents/sales/cro.md +10 -0
  174. package/opencode/agents/sales/deal-architect.md +10 -0
  175. package/opencode/agents/sales/outbound-strategist.md +10 -0
  176. package/opencode/agents/sales/pipeline-analyst.md +10 -0
  177. package/opencode/agents/support/cco.md +10 -0
  178. package/opencode/agents/support/community-manager.md +10 -0
  179. package/opencode/agents/support/ticket-triage.md +10 -0
  180. package/opencode/commands/go.md +10 -0
  181. package/opencode/commands/help.md +10 -0
  182. package/opencode/commands/sync.md +10 -0
  183. package/opencode/commands/version.md +11 -0
  184. package/opencode/opencode.json +8 -0
  185. package/opencode/plugin.ts +818 -0
  186. package/package.json +36 -4
  187. package/scripts/admin-merge-ready.sh +545 -0
  188. package/scripts/admin-merge-ready.test.sh +753 -0
  189. package/scripts/affected-tests.sh +908 -0
  190. package/scripts/affected-tests.test.sh +1797 -0
  191. package/scripts/alpha-metrics.sh +78 -0
  192. package/scripts/alpha-metrics.test.sh +115 -0
  193. package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
  194. package/scripts/audit-flag-flip.sh +107 -0
  195. package/scripts/check-red-on-main.sh +316 -0
  196. package/scripts/ci-head-verdict.sh +411 -0
  197. package/scripts/cloud-detect.sh +193 -0
  198. package/scripts/delegation-verify.sh +657 -0
  199. package/scripts/delegation-verify.test.sh +1424 -0
  200. package/scripts/deploy-arm.sh +475 -0
  201. package/scripts/domain-model-drift.sh +419 -0
  202. package/scripts/emit-decision.sh +144 -0
  203. package/scripts/emit-decision.test.sh +144 -0
  204. package/scripts/ext-filter-divergence-corpus.txt +15 -0
  205. package/scripts/ext-filter-divergence.sh +189 -0
  206. package/scripts/generate-c4-from-components.ts +442 -0
  207. package/scripts/grok-fidelity-gate.sh +68 -0
  208. package/scripts/grok-pre-push-gate.sh +232 -0
  209. package/scripts/harness-discovery-smoke.ts +906 -0
  210. package/scripts/harness-parity-census.ts +92 -0
  211. package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
  212. package/scripts/install-opencode-adapter.ts +509 -0
  213. package/scripts/lib/affected-tests/common.sh +365 -0
  214. package/scripts/lib/affected-tests/go.sh +157 -0
  215. package/scripts/lib/affected-tests/js.sh +188 -0
  216. package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
  217. package/scripts/lib/affected-tests/python.sh +185 -0
  218. package/scripts/lib/context-sanitize.sh +49 -0
  219. package/scripts/lib/domain-model-lib.sh +180 -0
  220. package/scripts/lib/ext-filter-grammar.tsv +34 -0
  221. package/scripts/lib/operator-script.sh +1236 -0
  222. package/scripts/lib/proc.sh +580 -0
  223. package/scripts/lib/session-state.sh +748 -0
  224. package/scripts/lib/tmp-classify.sh +843 -0
  225. package/scripts/lib/wrapper-boundary.sh +47 -0
  226. package/scripts/lint-ratchet.py +1547 -0
  227. package/scripts/monitor-pr-checks.sh +441 -0
  228. package/scripts/pipeline-tally.sh +756 -0
  229. package/scripts/pipeline-tally.test.sh +740 -0
  230. package/scripts/plugin-version.sh +528 -0
  231. package/scripts/precommit-guard.sh +115 -0
  232. package/scripts/precommit-guard.test.sh +254 -0
  233. package/scripts/reconcile-live-heartbeats.ts +705 -0
  234. package/scripts/render-c4-model.sh +217 -0
  235. package/scripts/resolve-git-root.sh +55 -0
  236. package/scripts/resolve-regenerable-conflicts.sh +363 -0
  237. package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
  238. package/scripts/ruff-requirements.txt +22 -0
  239. package/scripts/run-in-pid-namespace.sh +98 -0
  240. package/scripts/run-in-pid-namespace.test.sh +290 -0
  241. package/scripts/scan-ancestor-signal-helpers.py +556 -0
  242. package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
  243. package/scripts/setup-ralph-loop.sh +238 -0
  244. package/scripts/sync-cursor-name-map.ts +316 -0
  245. package/scripts/sync-grok-agent-compat.ts +188 -0
  246. package/scripts/sync-npm-package.ts +165 -0
  247. package/scripts/sync-opencode-adapter.ts +498 -0
  248. package/scripts/sync-pr-behind.sh +647 -0
  249. package/scripts/sync-pr-behind.test.sh +252 -0
  250. package/scripts/taste-profile-update.sh +180 -0
  251. package/scripts/taste-profile-update.test.sh +167 -0
  252. package/scripts/write-budget-marker.sh +23 -0
  253. package/scripts/write-kb-coverage.ts +113 -0
  254. package/skills/admin-ip-refresh/SKILL.md +81 -0
  255. package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
  256. package/skills/agent-browser/SKILL.md +698 -0
  257. package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
  258. package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
  259. package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
  260. package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
  261. package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
  262. package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
  263. package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
  264. package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
  265. package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
  266. package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
  267. package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
  268. package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
  269. package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
  270. package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
  271. package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
  272. package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
  273. package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
  274. package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
  275. package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
  276. package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
  277. package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
  278. package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
  279. package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
  280. package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
  281. package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
  282. package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
  283. package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
  284. package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
  285. package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
  286. package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
  287. package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
  288. package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
  289. package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
  290. package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
  291. package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
  292. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
  293. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
  294. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
  295. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
  296. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
  297. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
  298. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
  299. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
  300. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
  301. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
  302. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
  303. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
  304. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
  305. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
  306. package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
  307. package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
  308. package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
  309. package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
  310. package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
  311. package/skills/agent-native-architecture/SKILL.md +479 -0
  312. package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
  313. package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
  314. package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
  315. package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
  316. package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
  317. package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
  318. package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
  319. package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
  320. package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
  321. package/skills/agent-native-architecture/references/product-implications.md +478 -0
  322. package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
  323. package/skills/agent-native-architecture/references/self-modification.md +294 -0
  324. package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
  325. package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
  326. package/skills/agent-native-audit/SKILL.md +300 -0
  327. package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
  328. package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
  329. package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
  330. package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
  331. package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
  332. package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
  333. package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
  334. package/skills/architecture/SKILL.md +436 -0
  335. package/skills/architecture/references/adr-template.md +154 -0
  336. package/skills/architecture/references/likec4-reference.md +136 -0
  337. package/skills/architecture/references/nfr-reference.md +126 -0
  338. package/skills/archive-kb/SKILL.md +86 -0
  339. package/skills/archive-kb/scripts/archive-kb.sh +300 -0
  340. package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
  341. package/skills/atdd-developer/SKILL.md +65 -0
  342. package/skills/brainstorm/SKILL.md +701 -0
  343. package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
  344. package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
  345. package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
  346. package/skills/brainstorm/references/ui-surface-terms.md +42 -0
  347. package/skills/brainstorm-techniques/SKILL.md +231 -0
  348. package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
  349. package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
  350. package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
  351. package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
  352. package/skills/campaign-calendar/SKILL.md +145 -0
  353. package/skills/cf-token-scope/SKILL.md +164 -0
  354. package/skills/cf-token-scope/references/widen-playbook.md +114 -0
  355. package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
  356. package/skills/changelog/SKILL.md +142 -0
  357. package/skills/code-to-prd/SKILL.md +66 -0
  358. package/skills/code-to-prd/references/banner-template.md +12 -0
  359. package/skills/code-to-prd/references/prd-template.md +41 -0
  360. package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
  361. package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
  362. package/skills/code-to-prd/test/fixture/.env.example +7 -0
  363. package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
  364. package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
  365. package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
  366. package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
  367. package/skills/code-to-prd/test/fixture/package.json +6 -0
  368. package/skills/cohort-status/SKILL.md +62 -0
  369. package/skills/community/SKILL.md +210 -0
  370. package/skills/community/scripts/bsky-community.sh +644 -0
  371. package/skills/community/scripts/bsky-setup.sh +631 -0
  372. package/skills/community/scripts/community-router.sh +93 -0
  373. package/skills/community/scripts/discord-community.sh +487 -0
  374. package/skills/community/scripts/discord-setup.sh +784 -0
  375. package/skills/community/scripts/github-community.sh +706 -0
  376. package/skills/community/scripts/hn-community.sh +233 -0
  377. package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
  378. package/skills/community/scripts/linkedin-community.sh +750 -0
  379. package/skills/community/scripts/linkedin-setup.sh +758 -0
  380. package/skills/community/scripts/x-community.sh +890 -0
  381. package/skills/community/scripts/x-setup.sh +713 -0
  382. package/skills/community/test/community-argv.test.sh +2922 -0
  383. package/skills/community/test/github-community.test.sh +981 -0
  384. package/skills/competitive-analysis/SKILL.md +69 -0
  385. package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
  386. package/skills/compound/SKILL.md +673 -0
  387. package/skills/compound/scripts/token-efficiency-report.sh +334 -0
  388. package/skills/compound/test/phase-16.test.sh +379 -0
  389. package/skills/compound-capture/SKILL.md +791 -0
  390. package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
  391. package/skills/compound-capture/assets/resolution-template.md +116 -0
  392. package/skills/compound-capture/references/yaml-schema.md +67 -0
  393. package/skills/compound-capture/schema.yaml +181 -0
  394. package/skills/constraint-scaffold/SKILL.md +175 -0
  395. package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
  396. package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
  397. package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
  398. package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
  399. package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
  400. package/skills/constraint-scaffold/references/shared-runner.template +135 -0
  401. package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
  402. package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
  403. package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
  404. package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
  405. package/skills/constraint-scaffold/test/generator.test.sh +93 -0
  406. package/skills/constraint-scaffold/test/parity.test.sh +285 -0
  407. package/skills/content-writer/SKILL.md +242 -0
  408. package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
  409. package/skills/cron-delete/SKILL.md +56 -0
  410. package/skills/cron-list/SKILL.md +54 -0
  411. package/skills/deepen-plan/SKILL.md +937 -0
  412. package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
  413. package/skills/deploy/SKILL.md +115 -0
  414. package/skills/deploy/references/hetzner-setup.md +67 -0
  415. package/skills/deploy/scripts/deploy.sh +37 -0
  416. package/skills/deploy-docs/SKILL.md +95 -0
  417. package/skills/dhh-rails-style/SKILL.md +203 -0
  418. package/skills/dhh-rails-style/references/architecture.md +717 -0
  419. package/skills/dhh-rails-style/references/controllers.md +323 -0
  420. package/skills/dhh-rails-style/references/frontend.md +556 -0
  421. package/skills/dhh-rails-style/references/gems.md +316 -0
  422. package/skills/dhh-rails-style/references/models.md +388 -0
  423. package/skills/dhh-rails-style/references/testing.md +354 -0
  424. package/skills/discord-content/SKILL.md +126 -0
  425. package/skills/docs-site/SKILL.md +214 -0
  426. package/skills/drain-labeled-backlog/SKILL.md +203 -0
  427. package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
  428. package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
  429. package/skills/drain-prs/SKILL.md +143 -0
  430. package/skills/drain-prs/scripts/triage-prs.sh +192 -0
  431. package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
  432. package/skills/dspy-ruby/SKILL.md +634 -0
  433. package/skills/dspy-ruby/assets/config-template.rb +359 -0
  434. package/skills/dspy-ruby/assets/module-template.rb +326 -0
  435. package/skills/dspy-ruby/assets/signature-template.rb +143 -0
  436. package/skills/dspy-ruby/references/core-concepts.md +268 -0
  437. package/skills/dspy-ruby/references/optimization.md +623 -0
  438. package/skills/dspy-ruby/references/providers.md +335 -0
  439. package/skills/eval-harness/README.md +194 -0
  440. package/skills/eval-harness/SKILL.md +157 -0
  441. package/skills/eval-harness/enums/go-routes.json +13 -0
  442. package/skills/eval-harness/enums/incident-threshold.json +5 -0
  443. package/skills/eval-harness/enums/lane.json +5 -0
  444. package/skills/eval-harness/enums/tool-selection.json +20 -0
  445. package/skills/eval-harness/enums/triage-levels.json +5 -0
  446. package/skills/eval-harness/gated-skills.json +34 -0
  447. package/skills/eval-harness/models.generated.json +5 -0
  448. package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
  449. package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
  450. package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
  451. package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
  452. package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
  453. package/skills/eval-harness/prompts/go-baseline.txt +6 -0
  454. package/skills/eval-harness/prompts/go-skill.txt +21 -0
  455. package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
  456. package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
  457. package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
  458. package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
  459. package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
  460. package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
  461. package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
  462. package/skills/eval-harness/prompts/triage-skill.txt +10 -0
  463. package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
  464. package/skills/eval-harness/scripts/extract-block.cjs +87 -0
  465. package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
  466. package/skills/eval-harness/scripts/gen-models.sh +41 -0
  467. package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
  468. package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
  469. package/skills/eval-harness/scripts/parse-label.cjs +93 -0
  470. package/skills/eval-harness/scripts/verdict.cjs +132 -0
  471. package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
  472. package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
  473. package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
  474. package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
  475. package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
  476. package/skills/eval-harness/test/eval-gate.test.sh +132 -0
  477. package/skills/eval-harness/test/extract-block.test.sh +87 -0
  478. package/skills/eval-harness/test/gate-classification.test.sh +77 -0
  479. package/skills/eval-harness/test/gen-models.test.sh +75 -0
  480. package/skills/eval-harness/test/measure-classification.test.sh +74 -0
  481. package/skills/eval-harness/test/parse-label.test.sh +129 -0
  482. package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
  483. package/skills/eval-harness/test/verdict.test.sh +123 -0
  484. package/skills/every-style-editor/SKILL.md +142 -0
  485. package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
  486. package/skills/feature-tweet/SKILL.md +199 -0
  487. package/skills/feature-video/SKILL.md +412 -0
  488. package/skills/feature-video/scripts/check_deps.sh +281 -0
  489. package/skills/file-todos/SKILL.md +267 -0
  490. package/skills/file-todos/assets/todo-template.md +166 -0
  491. package/skills/fix-issue/SKILL.md +278 -0
  492. package/skills/fix-issue/references/agent-authored-exclusion.md +120 -0
  493. package/skills/fix-issue/references/exclude-label-jq-snippet.md +97 -0
  494. package/skills/flag-bootstrap/SETUP.md +142 -0
  495. package/skills/flag-create/SKILL.md +145 -0
  496. package/skills/flag-create/scripts/create.sh +273 -0
  497. package/skills/flag-delete/SKILL.md +167 -0
  498. package/skills/flag-delete/scripts/delete.sh +288 -0
  499. package/skills/flag-list/SKILL.md +99 -0
  500. package/skills/flag-list/scripts/list.sh +275 -0
  501. package/skills/flag-set-role/SKILL.md +229 -0
  502. package/skills/flag-set-role/scripts/flip.sh +774 -0
  503. package/skills/frontend-anti-slop/SKILL.md +106 -0
  504. package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
  505. package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
  506. package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
  507. package/skills/frontend-design/SKILL.md +77 -0
  508. package/skills/gdpr-gate/NOTICE +136 -0
  509. package/skills/gdpr-gate/SKILL.md +341 -0
  510. package/skills/gdpr-gate/references/fields.md +160 -0
  511. package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
  512. package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
  513. package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
  514. package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
  515. package/skills/gdpr-gate/references/layers/frontend.md +249 -0
  516. package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
  517. package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
  518. package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
  519. package/skills/gdpr-gate/references/legal-consent.md +140 -0
  520. package/skills/gdpr-gate/references/non-negotiables.md +60 -0
  521. package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
  522. package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
  523. package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
  524. package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
  525. package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
  526. package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
  527. package/skills/gemini-imagegen/SKILL.md +283 -0
  528. package/skills/gemini-imagegen/requirements.txt +5 -0
  529. package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
  530. package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
  531. package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
  532. package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
  533. package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
  534. package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
  535. package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
  536. package/skills/git-worktree/SKILL.md +534 -0
  537. package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
  538. package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
  539. package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
  540. package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
  541. package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
  542. package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
  543. package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
  544. package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
  545. package/skills/go/SKILL.md +54 -0
  546. package/skills/growth/SKILL.md +164 -0
  547. package/skills/harvest-debt/SKILL.md +78 -0
  548. package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
  549. package/skills/heal-skill/SKILL.md +173 -0
  550. package/skills/help/SKILL.md +46 -0
  551. package/skills/incident/NOTICE +39 -0
  552. package/skills/incident/SKILL.md +395 -0
  553. package/skills/incident/scripts/dry-run.sh +448 -0
  554. package/skills/incident/scripts/redact-engine.py +412 -0
  555. package/skills/incident/scripts/redact-sentinel.sh +44 -0
  556. package/skills/incident/templates/pir.md +139 -0
  557. package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
  558. package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
  559. package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
  560. package/skills/incident/test/fixtures/positive-corpus.md +87 -0
  561. package/skills/incident/test/redact-sentinel.test.sh +1420 -0
  562. package/skills/invoice/SKILL.md +206 -0
  563. package/skills/kb-glossary/SKILL.md +71 -0
  564. package/skills/kb-glossary/references/glossary-format.md +111 -0
  565. package/skills/kb-glossary/references/rejected-request-register.md +115 -0
  566. package/skills/kb-search/SKILL.md +276 -0
  567. package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
  568. package/skills/legal-audit/SKILL.md +161 -0
  569. package/skills/legal-generate/NOTICE +242 -0
  570. package/skills/legal-generate/SKILL.md +244 -0
  571. package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
  572. package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
  573. package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
  574. package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
  575. package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
  576. package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
  577. package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
  578. package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
  579. package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
  580. package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
  581. package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
  582. package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
  583. package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
  584. package/skills/linear-fetch/SKILL.md +247 -0
  585. package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
  586. package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
  587. package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
  588. package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
  589. package/skills/linear-fetch/test/parity.test.sh +112 -0
  590. package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
  591. package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
  592. package/skills/merge-pr/SKILL.md +670 -0
  593. package/skills/model-launch-review/SKILL.md +255 -0
  594. package/skills/model-launch-review/scripts/audit-models.sh +501 -0
  595. package/skills/one-shot/SKILL.md +316 -0
  596. package/skills/one-shot/references/token-discipline.md +74 -0
  597. package/skills/operator-bootstrap/SKILL.md +412 -0
  598. package/skills/operator-bootstrap/template.sh +439 -0
  599. package/skills/operator-digest/SKILL.md +287 -0
  600. package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
  601. package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
  602. package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
  603. package/skills/operator-rephrase/SKILL.md +98 -0
  604. package/skills/pencil-setup/SKILL.md +201 -0
  605. package/skills/pencil-setup/scripts/check_deps.sh +556 -0
  606. package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
  607. package/skills/pencil-setup/scripts/package-lock.json +1172 -0
  608. package/skills/pencil-setup/scripts/package.json +14 -0
  609. package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
  610. package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
  611. package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
  612. package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
  613. package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
  614. package/skills/plan/SKILL.md +1140 -0
  615. package/skills/plan/references/differential-migration-testing.md +185 -0
  616. package/skills/plan/references/plan-community-discovery.md +42 -0
  617. package/skills/plan/references/plan-founder-check.md +137 -0
  618. package/skills/plan/references/plan-functional-overlap.md +25 -0
  619. package/skills/plan/references/plan-host-fanout.md +51 -0
  620. package/skills/plan/references/plan-issue-templates.md +779 -0
  621. package/skills/plan/references/plan-network-outage-checklist.md +131 -0
  622. package/skills/plan/references/plan-scope-check.md +126 -0
  623. package/skills/plan/references/plan-sharp-edges.md +331 -0
  624. package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
  625. package/skills/plan/references/webapp-cwv-observability.md +143 -0
  626. package/skills/plan-review/SKILL.md +91 -0
  627. package/skills/plan-review/lib/named-panel.mjs +58 -0
  628. package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
  629. package/skills/postmerge/SKILL.md +642 -0
  630. package/skills/postmerge/references/deploy-status-debugging.md +141 -0
  631. package/skills/postmerge/references/graceful-degradation.md +20 -0
  632. package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
  633. package/skills/preflight/SKILL.md +1553 -0
  634. package/skills/preflight/references/check-13-founder-check.md +286 -0
  635. package/skills/preflight/scripts/founder-check.py +1640 -0
  636. package/skills/preflight/scripts/parse-form-a.awk +107 -0
  637. package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
  638. package/skills/product-roadmap/SKILL.md +357 -0
  639. package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
  640. package/skills/provision-cloudflare/SKILL.md +67 -0
  641. package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
  642. package/skills/provision-doppler/SKILL.md +58 -0
  643. package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
  644. package/skills/provision-github/SKILL.md +59 -0
  645. package/skills/provision-github/scripts/provision-github.sh +300 -0
  646. package/skills/provision-hetzner/SKILL.md +76 -0
  647. package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
  648. package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
  649. package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
  650. package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
  651. package/skills/qa/SKILL.md +263 -0
  652. package/skills/qa/references/browser-preflight.md +22 -0
  653. package/skills/questionnaire-generate/SKILL.md +328 -0
  654. package/skills/questionnaire-generate/references/questionnaire.template +60 -0
  655. package/skills/rclone/SKILL.md +162 -0
  656. package/skills/rclone/scripts/check_setup.sh +63 -0
  657. package/skills/release-announce/SKILL.md +65 -0
  658. package/skills/release-docs/SKILL.md +92 -0
  659. package/skills/reproduce-bug/SKILL.md +358 -0
  660. package/skills/resolve-debt/SKILL.md +85 -0
  661. package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
  662. package/skills/resolve-parallel/SKILL.md +45 -0
  663. package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
  664. package/skills/resolve-pr-parallel/SKILL.md +68 -0
  665. package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
  666. package/skills/resolve-todo-parallel/SKILL.md +57 -0
  667. package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
  668. package/skills/review/SKILL.md +1620 -0
  669. package/skills/review/references/anti-slop-scanner-hook.md +40 -0
  670. package/skills/review/references/defect-classes-migrations.md +9 -0
  671. package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
  672. package/skills/review/references/review-e2e-testing.md +116 -0
  673. package/skills/review/references/review-evidence-comment.md +36 -0
  674. package/skills/review/references/review-todo-structure.md +165 -0
  675. package/skills/review/references/risk-tier-and-fix-rounds.md +97 -0
  676. package/skills/review/references/semgrep-custom-rules.yaml +174 -0
  677. package/skills/review/references/ultrathink-phases.md +98 -0
  678. package/skills/review/references/wfs.md +6 -0
  679. package/skills/review/scripts/emit-review-trailer.sh +499 -0
  680. package/skills/review/scripts/ensure-semgrep.sh +99 -0
  681. package/skills/review/scripts/fix-round-seats.sh +197 -0
  682. package/skills/review/test/emit-review-trailer.test.sh +387 -0
  683. package/skills/review/test/ensure-semgrep.test.sh +111 -0
  684. package/skills/review/test/fix-round-seats.test.sh +237 -0
  685. package/skills/review/workflows/README.md +147 -0
  686. package/skills/review/workflows/review.workflow.js +667 -0
  687. package/skills/schedule/SKILL.md +796 -0
  688. package/skills/seo-aeo/SKILL.md +139 -0
  689. package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
  690. package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
  691. package/skills/ship/SKILL.md +3023 -0
  692. package/skills/ship/references/adr-ordinal-collision.md +47 -0
  693. package/skills/ship/references/affected-gate-contract.md +115 -0
  694. package/skills/ship/references/ci-run-diagnosis.md +29 -0
  695. package/skills/ship/references/ci-workflow-authoring.md +68 -0
  696. package/skills/ship/references/cleanup-merged-archival.md +39 -0
  697. package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
  698. package/skills/ship/references/cto-ruling-armed-push.md +5 -0
  699. package/skills/ship/references/decision-challenges-render.md +71 -0
  700. package/skills/ship/references/delegated-act-authoring-form.md +7 -0
  701. package/skills/ship/references/detached-battery-verdict.md +13 -0
  702. package/skills/ship/references/epic-close-sweep.md +62 -0
  703. package/skills/ship/references/feature-tweet-draft.md +42 -0
  704. package/skills/ship/references/followthrough-issue-body-template.md +47 -0
  705. package/skills/ship/references/followthrough-stub-template.sh +92 -0
  706. package/skills/ship/references/merge-queue-dequeue.md +39 -0
  707. package/skills/ship/references/operator-holds.md +179 -0
  708. package/skills/ship/references/pipeline-tally-render.md +43 -0
  709. package/skills/ship/references/pr-overlap.md +153 -0
  710. package/skills/ship/references/queue-mode.md +7 -0
  711. package/skills/ship/references/ready-run-wait.md +36 -0
  712. package/skills/ship/references/red-on-main-quarantine.md +110 -0
  713. package/skills/ship/references/register-update-pr-pattern.md +88 -0
  714. package/skills/ship/references/settle-then-admin-merge.md +130 -0
  715. package/skills/ship/references/supabase-column-probe.md +13 -0
  716. package/skills/ship/scripts/auto-close-scan.sh +76 -0
  717. package/skills/ship/scripts/battery-owed.sh +403 -0
  718. package/skills/ship/scripts/hold-gate-eval.sh +410 -0
  719. package/skills/ship/scripts/net-issue-flow.sh +665 -0
  720. package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
  721. package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
  722. package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
  723. package/skills/skill-creator/SKILL.md +299 -0
  724. package/skills/skill-creator/references/api-security.md +248 -0
  725. package/skills/skill-creator/references/authoring-levers.md +110 -0
  726. package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
  727. package/skills/skill-creator/references/best-practices.md +431 -0
  728. package/skills/skill-creator/references/common-patterns.md +639 -0
  729. package/skills/skill-creator/references/core-principles.md +193 -0
  730. package/skills/skill-creator/references/executable-code.md +188 -0
  731. package/skills/skill-creator/references/iteration-and-testing.md +509 -0
  732. package/skills/skill-creator/references/official-spec.md +193 -0
  733. package/skills/skill-creator/references/recommended-structure.md +180 -0
  734. package/skills/skill-creator/references/skill-structure.md +396 -0
  735. package/skills/skill-creator/references/using-scripts.md +122 -0
  736. package/skills/skill-creator/references/using-templates.md +120 -0
  737. package/skills/skill-creator/references/workflows-and-validation.md +541 -0
  738. package/skills/skill-creator/scripts/init_skill.py +322 -0
  739. package/skills/skill-creator/scripts/package_skill.py +110 -0
  740. package/skills/skill-creator/scripts/quick_validate.py +64 -0
  741. package/skills/skill-creator/templates/router-skill.md +73 -0
  742. package/skills/skill-creator/templates/simple-skill.md +34 -0
  743. package/skills/skill-creator/workflows/add-reference.md +96 -0
  744. package/skills/skill-creator/workflows/add-script.md +104 -0
  745. package/skills/skill-creator/workflows/add-template.md +83 -0
  746. package/skills/skill-creator/workflows/add-workflow.md +128 -0
  747. package/skills/skill-creator/workflows/audit-skill.md +159 -0
  748. package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
  749. package/skills/skill-creator/workflows/create-new-skill.md +207 -0
  750. package/skills/skill-creator/workflows/get-guidance.md +134 -0
  751. package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
  752. package/skills/skill-creator/workflows/verify-skill.md +226 -0
  753. package/skills/skill-security-scan/SKILL.md +145 -0
  754. package/skills/skill-security-scan/references/disclaimer.md +21 -0
  755. package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
  756. package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
  757. package/skills/skill-security-scan/references/override-mechanism.md +171 -0
  758. package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
  759. package/skills/skill-security-scan/references/regex-patterns.md +136 -0
  760. package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
  761. package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
  762. package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
  763. package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
  764. package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
  765. package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
  766. package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
  767. package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
  768. package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
  769. package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
  770. package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
  771. package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
  772. package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
  773. package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
  774. package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
  775. package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
  776. package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
  777. package/skills/skill-security-scan/scripts/lib.sh +197 -0
  778. package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
  779. package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
  780. package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
  781. package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
  782. package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
  783. package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
  784. package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
  785. package/skills/social-distribute/SKILL.md +485 -0
  786. package/skills/spec-templates/SKILL.md +274 -0
  787. package/skills/sync/SKILL.md +49 -0
  788. package/skills/test-browser/SKILL.md +464 -0
  789. package/skills/test-browser/references/feature-map.md +161 -0
  790. package/skills/test-fix-loop/SKILL.md +159 -0
  791. package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
  792. package/skills/triage/SKILL.md +402 -0
  793. package/skills/trigger-cron/SKILL.md +136 -0
  794. package/skills/trigger-cron/scripts/trigger.sh +198 -0
  795. package/skills/user-set-role/SKILL.md +147 -0
  796. package/skills/user-set-role/scripts/set-role.sh +218 -0
  797. package/skills/user-story-writer/SKILL.md +77 -0
  798. package/skills/ux-audit/SKILL.md +251 -0
  799. package/skills/ux-audit/references/finding.schema.json +62 -0
  800. package/skills/ux-audit/references/route-list.yaml +95 -0
  801. package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
  802. package/skills/ux-audit/scripts/bot-signin.ts +135 -0
  803. package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
  804. package/skills/verification-scaffold/SKILL.md +73 -0
  805. package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
  806. package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
  807. package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
  808. package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
  809. package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
  810. package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
  811. package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
  812. package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
  813. package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
  814. package/skills/verification-scaffold/references/guards.tsv +21 -0
  815. package/skills/verification-scaffold/references/verification-readme.template +55 -0
  816. package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
  817. package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
  818. package/skills/verification-scaffold/test/generator.test.sh +319 -0
  819. package/skills/verification-scaffold/test/manifest.test.sh +144 -0
  820. package/skills/work/SKILL.md +1480 -0
  821. package/skills/work/references/end-of-work-resume.md +7 -0
  822. package/skills/work/references/hook-test-main-cwd.md +24 -0
  823. package/skills/work/references/key-principles.md +83 -0
  824. package/skills/work/references/property-testing.md +122 -0
  825. package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
  826. package/skills/work/references/work-agent-teams.md +97 -0
  827. package/skills/work/references/work-lifecycle-parallel.md +147 -0
  828. package/skills/work/references/work-lockfile-bumps.md +97 -0
  829. package/skills/work/references/work-scratch-sandboxes.md +212 -0
  830. package/skills/work/references/work-subagent-fanout.md +61 -0
  831. package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
  832. package/skills/xcode-test/SKILL.md +360 -0
@@ -0,0 +1,1420 @@
1
+ #!/usr/bin/env bash
2
+ # Test harness for plugins/soleur/skills/incident/scripts/redact-sentinel.sh
3
+ #
4
+ # Pattern mirrors plugins/soleur/skills/git-worktree/test/lease-protects-active.test.sh:
5
+ # - set -uo pipefail (NOT -e — single test failure must not abort the suite)
6
+ # - PASS/FAIL counter
7
+ # - trap-based cleanup
8
+ #
9
+ # Tests 1-4 are the ORIGINAL contract (redact-sentinel #2725 FR3).
10
+ # Tests 5-12 are the redaction-hardening suite (#5987): NFKC + zero-width strip
11
+ # before matching, ReDoS-safe fail-closed input cap, fail-closed on no-python3,
12
+ # ERE->re golden parity, and the legal-generate gate.
13
+ #
14
+ # All confusable / oversize / invalid-byte inputs are generated AT RUNTIME via
15
+ # `python3 -c` with chr(0xXXXX) — NEVER committed as literal invisibles
16
+ # (`cq-regex-unicode-separators-escape-only`; AC6 enforces this). All tokens are
17
+ # synthesized from format specs (`cq-test-fixtures-synthesized-only`).
18
+ set -uo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
22
+ REPO_ROOT="$(cd "${SKILL_DIR}/../../../.." && pwd)"
23
+
24
+ SENTINEL="${SKILL_DIR}/scripts/redact-sentinel.sh"
25
+ POSITIVE_CORPUS="${SCRIPT_DIR}/fixtures/positive-corpus.md"
26
+ NEGATIVE_BASELINE="${REPO_ROOT}/knowledge-base/engineering/operations/post-mortems/dashboard-error-postmortem.md"
27
+ LEGAL_SKILL="${REPO_ROOT}/plugins/soleur/skills/legal-generate/SKILL.md"
28
+
29
+ PASS=0
30
+ FAIL=0
31
+ TMP_DIR="$(mktemp -d)"
32
+ trap 'rm -rf "${TMP_DIR}"' EXIT
33
+
34
+ assert_exit() {
35
+ local label="$1" expected="$2" actual="$3"
36
+ if [[ "${expected}" == "${actual}" ]]; then
37
+ echo "PASS: ${label} (exit=${actual})"
38
+ PASS=$((PASS + 1))
39
+ else
40
+ echo "FAIL: ${label} (expected exit=${expected}, got ${actual})"
41
+ FAIL=$((FAIL + 1))
42
+ fi
43
+ }
44
+
45
+ assert_grep() {
46
+ local label="$1" pattern="$2" file="$3"
47
+ if grep -qE "${pattern}" "${file}"; then
48
+ echo "PASS: ${label} (pattern matched in ${file##*/})"
49
+ PASS=$((PASS + 1))
50
+ else
51
+ echo "FAIL: ${label} (pattern '${pattern}' not found in ${file##*/})"
52
+ FAIL=$((FAIL + 1))
53
+ fi
54
+ }
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Test 1 — negative-baseline (clean hand-redacted PIR exits 0)
58
+ # ---------------------------------------------------------------------------
59
+ out="${TMP_DIR}/test1.out"
60
+ bash "${SENTINEL}" "${NEGATIVE_BASELINE}" >"${out}" 2>&1
61
+ assert_exit "Test 1: negative-baseline exits 0 on hand-redacted PIR" 0 $?
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Test 2 — positive-corpus: every regex class triggers >=1
65
+ # ---------------------------------------------------------------------------
66
+ out="${TMP_DIR}/test2.out"
67
+ bash "${SENTINEL}" "${POSITIVE_CORPUS}" >"${out}" 2>&1
68
+ rc=$?
69
+ assert_exit "Test 2: positive-corpus exits non-zero" 1 "${rc}"
70
+
71
+ for class in JWT email UUID stripe_key stripe_whsec stripe_acct stripe_cust_pi_seti_sub_in IPv4 env_var \
72
+ github_token anthropic_key openai_key supabase_pat pem_private_key doppler_token slack_token \
73
+ cloudflare_token; do
74
+ assert_grep "Test 2.${class}: pattern '${class}' present" "matched pattern ${class}" "${out}"
75
+ done
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Test 3 — invalid arg -> exit 2
79
+ # ---------------------------------------------------------------------------
80
+ out="${TMP_DIR}/test3.out"
81
+ bash "${SENTINEL}" /nonexistent/path/to/file.md >"${out}" 2>&1
82
+ assert_exit "Test 3: nonexistent file exits 2" 2 $?
83
+
84
+ bash "${SENTINEL}" >"${out}" 2>&1
85
+ assert_exit "Test 3: missing arg exits 2" 2 $?
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Test 4 — output format (TIGHTENED, #5987): capped reveal, never a full token.
89
+ # Was `.{8}\*\*\*.{8}` (leaked ~50% of a fixed-prefix key). Now <=4 + *** + <=4.
90
+ # ---------------------------------------------------------------------------
91
+ out="${TMP_DIR}/test4.out"
92
+ bash "${SENTINEL}" "${POSITIVE_CORPUS}" >"${out}" 2>&1 || true
93
+ assert_grep "Test 4: capped output format 'at offset N: <=4-prefix***<=4-suffix matched pattern X'" \
94
+ 'at offset [0-9]+: .{0,4}\*\*\*(.{0,4})? matched pattern [A-Za-z_]+' "${out}"
95
+
96
+ # Test 4b — NO full-token reveal: no finding line may reveal >4 prefix chars.
97
+ if grep -qE 'at offset [0-9]+: [^*]{5,}\*\*\*' "${out}"; then
98
+ echo "FAIL: Test 4b: a finding revealed >4 prefix chars (entropy leak regression)"
99
+ FAIL=$((FAIL + 1))
100
+ else
101
+ echo "PASS: Test 4b: no finding reveals >4 prefix chars (meta-redaction tightened)"
102
+ PASS=$((PASS + 1))
103
+ fi
104
+
105
+ # ===========================================================================
106
+ # #5987 — redaction hardening suite (Tests 5-12)
107
+ # ===========================================================================
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Test 5 — compatibility-confusable + invisible-splitter evasion (AC1).
111
+ # A JWT split by ZWSP (U+200B), soft-hyphen (U+00AD), and line-sep (U+2028),
112
+ # plus a fullwidth Stripe key. (a) OLD raw-byte engine MISSES; (b) new engine CATCHES.
113
+ # Invisibles built via chr(0xXXXX) so no literal invisibles land in this file.
114
+ # ---------------------------------------------------------------------------
115
+ t5_file="${TMP_DIR}/t5.txt"
116
+ python3 -c "
117
+ import sys
118
+ def fw(s): # ASCII printable -> fullwidth (NFKC folds back)
119
+ return ''.join(chr(ord(c)+0xFEE0) if 0x21<=ord(c)<=0x7e else c for c in s)
120
+ jwt = 'eyJ'+'A'*12+chr(0x200b)+'.'+'B'*10+chr(0x00ad)+'BB'+'.'+'C'*10+chr(0x2028)+'CC'
121
+ stripe = fw('sk_live_0000000000000000')
122
+ sys.stdout.write(jwt+'\n'+stripe+'\n')
123
+ " > "${t5_file}"
124
+
125
+ # (a) OLD bash engine MISSES the evasive tokens. Baseline is a FROZEN copy of the pre-#5987
126
+ # grep scanner committed at fixtures/legacy-bash-scanner.sh — NOT `git show main:redact-sentinel.sh`,
127
+ # which becomes the new shim post-merge (references redact-engine.py absent from a temp dir -> exit 2,
128
+ # a merge-time time-bomb that would turn main red for the next contributor). The frozen copy is a
129
+ # self-contained pure-bash scanner and is stable across merges.
130
+ OLD_ENGINE="${SCRIPT_DIR}/fixtures/legacy-bash-scanner.sh"
131
+ bash "${OLD_ENGINE}" "${t5_file}" >/dev/null 2>&1
132
+ assert_exit "Test 5a: OLD raw-byte engine MISSES confusable/invisible-split tokens" 0 $?
133
+
134
+ # (b) new engine catches after strip + NFKC
135
+ t5_out="${TMP_DIR}/t5.out"
136
+ bash "${SENTINEL}" "${t5_file}" >"${t5_out}" 2>&1
137
+ assert_exit "Test 5b: engine catches confusable/invisible-split tokens" 1 $?
138
+ assert_grep "Test 5b.JWT: JWT class tripped after strip" "matched pattern JWT" "${t5_out}"
139
+ assert_grep "Test 5b.stripe: fullwidth Stripe key tripped after NFKC" "matched pattern stripe_key" "${t5_out}"
140
+
141
+ # Test 5c — category-based STRIP: invisible/control/format families NFKC leaves intact must all be
142
+ # stripped, not just a hand-picked list. One secret per invisible class; each must be CAUGHT (exit 1).
143
+ # NUL(Cc), DEL(Cc), variation-selector U+FE0F(Mn), Tags-block U+E0020(Cf), combining grapheme joiner
144
+ # U+034F(Mn). Splice is placed mid-token so a raw-byte matcher would miss it.
145
+ for probe in "NUL:0x00" "DEL:0x7f" "VS16:0xfe0f" "TAGSPACE:0xe0020" "CGJ:0x34f"; do
146
+ name="${probe%%:*}"; cp="${probe##*:}"
147
+ f="${TMP_DIR}/t5c-${name}.txt"
148
+ python3 -c "import sys; sys.stdout.write('sk_live_0000'+chr(${cp})+'000000000000\n')" > "${f}"
149
+ bash "${SENTINEL}" "${f}" >/dev/null 2>&1
150
+ assert_exit "Test 5c.${name}: invisible-splice (${cp}) stripped by category, secret caught" 1 $?
151
+ done
152
+
153
+ # Test 5d — second-strip necessity (U+FFA0 halfwidth Hangul filler): NFKC folds U+FFA0 -> U+1160,
154
+ # a strippable char NOT present in the raw input. Only the post-NFKC second strip catches it. This
155
+ # pins the double-strip so a future "simplification" removing it fails loudly.
156
+ t5d_file="${TMP_DIR}/t5d.txt"
157
+ python3 -c "import sys; sys.stdout.write('sk_live_0000'+chr(0xFFA0)+'000000000000\n')" > "${t5d_file}"
158
+ bash "${SENTINEL}" "${t5d_file}" >/dev/null 2>&1
159
+ assert_exit "Test 5d: U+FFA0->U+1160 NFKC fold caught by the second strip" 1 $?
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # Test 6 — oversize -> synthetic HIGH (AC2). Cap lowered via env for speed.
163
+ # ---------------------------------------------------------------------------
164
+ t6_file="${TMP_DIR}/t6.txt"
165
+ head -c 2048 /dev/zero | tr '\0' 'a' > "${t6_file}" # 2048 bytes of 'a', no secret
166
+ t6_out="${TMP_DIR}/t6.out"
167
+ REDACT_MAX_INPUT_BYTES=1024 bash "${SENTINEL}" "${t6_file}" >"${t6_out}" 2>&1
168
+ assert_exit "Test 6: raw-oversize input exits 1 (synthetic HIGH)" 1 $?
169
+ assert_grep "Test 6: synthetic-HIGH marker present" "SYNTHETIC HIGH" "${t6_out}"
170
+ if grep -qE 'matched pattern' "${t6_out}"; then
171
+ echo "FAIL: Test 6: per-class matching ran on oversize input (should short-circuit)"
172
+ FAIL=$((FAIL + 1))
173
+ else
174
+ echo "PASS: Test 6: no per-class matching on oversize input (fail-fast)"
175
+ PASS=$((PASS + 1))
176
+ fi
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Test 6b — expansion bomb (AC2): raw < cap but NFKC-expanded > cap.
180
+ # U+FDFA NFKC-expands to 18 codepoints. Built via chr(0xFDFA).
181
+ # ---------------------------------------------------------------------------
182
+ t6b_file="${TMP_DIR}/t6b.txt"
183
+ python3 -c "import sys; sys.stdout.write(chr(0xFDFA)*40)" > "${t6b_file}" # raw ~120 bytes
184
+ t6b_raw=$(wc -c < "${t6b_file}")
185
+ t6b_out="${TMP_DIR}/t6b.out"
186
+ REDACT_MAX_INPUT_BYTES=200 bash "${SENTINEL}" "${t6b_file}" >"${t6b_out}" 2>&1
187
+ t6b_rc=$?
188
+ echo " (Test 6b: raw=${t6b_raw} bytes < cap=200; NFKC-expanded > cap)"
189
+ assert_exit "Test 6b: NFKC-expansion-oversize exits 1 (post-NFKC re-check)" 1 "${t6b_rc}"
190
+ assert_grep "Test 6b: synthetic-HIGH marker present" "SYNTHETIC HIGH" "${t6b_out}"
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # Test 7 — invalid-UTF-8 splice (AC1b): a secret with an invalid byte spliced in
194
+ # (-> U+FFFD) is caught after the strip.
195
+ # ---------------------------------------------------------------------------
196
+ t7_file="${TMP_DIR}/t7.txt"
197
+ python3 -c "
198
+ import sys
199
+ sys.stdout.buffer.write(b'sk_live_0000' + b'\x80' + b'0000000000000000\n')
200
+ " > "${t7_file}"
201
+ t7_out="${TMP_DIR}/t7.out"
202
+ bash "${SENTINEL}" "${t7_file}" >"${t7_out}" 2>&1
203
+ assert_exit "Test 7: invalid-UTF-8-spliced secret caught after strip" 1 $?
204
+ assert_grep "Test 7: stripe_key tripped post-strip" "matched pattern stripe_key" "${t7_out}"
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # Test 8 — no false positives: clean baselines still exit 0 after normalization.
208
+ # ---------------------------------------------------------------------------
209
+ bash "${SENTINEL}" "${NEGATIVE_BASELINE}" >/dev/null 2>&1
210
+ assert_exit "Test 8a: normalization does not manufacture matches on clean PIR" 0 $?
211
+ t8_file="${TMP_DIR}/t8.txt"
212
+ python3 -c "import sys; sys.stdout.write('Privet clean prose ' + chr(0x043f)+chr(0x0440)+' no secrets.\n')" > "${t8_file}"
213
+ bash "${SENTINEL}" "${t8_file}" >/dev/null 2>&1
214
+ assert_exit "Test 8b: Cyrillic prose without secrets exits 0 (fold != fabricate)" 0 $?
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # Test 9 — golden ERE<->re parity (AC3): every class the OLD bash engine catches
218
+ # on the corpus, the NEW engine also catches (no class narrowed by the port).
219
+ # New additive classes (doppler/slack) are a superset and do not break parity.
220
+ # ---------------------------------------------------------------------------
221
+ old_hits="${TMP_DIR}/old_hits.txt"
222
+ new_hits="${TMP_DIR}/new_hits.txt"
223
+ # LC_ALL=C on BOTH sort and comm: comm requires byte-collation order; a locale sort (en_US) collates
224
+ # case-insensitively so comm sees "unsorted input" and its diff is undefined — the parity guard would
225
+ # run blind. Byte-sort both streams and byte-compare.
226
+ bash "${OLD_ENGINE}" "${POSITIVE_CORPUS}" 2>/dev/null | grep -oE 'matched pattern [A-Za-z_]+' | LC_ALL=C sort -u > "${old_hits}"
227
+ bash "${SENTINEL}" "${POSITIVE_CORPUS}" 2>/dev/null | grep -oE 'matched pattern [A-Za-z_]+' | LC_ALL=C sort -u > "${new_hits}"
228
+ missing=$(LC_ALL=C comm -23 "${old_hits}" "${new_hits}")
229
+ if [[ -z "${missing}" ]]; then
230
+ echo "PASS: Test 9: golden parity — new engine catches every class the old engine did"
231
+ PASS=$((PASS + 1))
232
+ else
233
+ echo "FAIL: Test 9: new engine NARROWED these classes vs old: ${missing//$'\n'/, }"
234
+ FAIL=$((FAIL + 1))
235
+ fi
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Test 10 — fail-closed on no-python3 (AC4): shim exits 2 (not 0, not 1) with
239
+ # python3 shadowed off PATH. `dirname` symlinked so the shim can still resolve DIR.
240
+ # ---------------------------------------------------------------------------
241
+ nopy="${TMP_DIR}/nopy"
242
+ mkdir -p "${nopy}"
243
+ ln -s "$(command -v dirname)" "${nopy}/dirname"
244
+ BASH_BIN="$(command -v bash)" # absolute path — parent must NOT PATH-search under the stripped PATH
245
+ t10_file="${TMP_DIR}/t10.txt"
246
+ printf 'sk_test_0000000000000000\n' > "${t10_file}"
247
+ PATH="${nopy}" "${BASH_BIN}" "${SENTINEL}" "${t10_file}" >/dev/null 2>&1
248
+ assert_exit "Test 10: no-python3 fails closed with exit 2" 2 $?
249
+
250
+ # ---------------------------------------------------------------------------
251
+ # Test 11 — legal-generate gate (AC5): a synthesized secret in a legal draft
252
+ # mktemp makes the sentinel exit non-zero; and the SKILL.md wires the gate
253
+ # BEFORE the inline presentation step.
254
+ # ---------------------------------------------------------------------------
255
+ t11_file="${TMP_DIR}/legal-draft.md"
256
+ printf '# Privacy Policy (DRAFT)\n\nContact: STRIPE_SECRET_KEY=sk_live_0000000000000000\n' > "${t11_file}"
257
+ bash "${SENTINEL}" "${t11_file}" >/dev/null 2>&1
258
+ assert_exit "Test 11a: legal draft with secret trips the sentinel (non-zero)" 1 $?
259
+ assert_grep "Test 11b: legal-generate SKILL.md wires redact-sentinel.sh" \
260
+ 'redact-sentinel\.sh' "${LEGAL_SKILL}"
261
+ # The gate block must appear BEFORE the presentation step. Anchor on the stable structural heading
262
+ # `## Phase 3` (where the Accept/Edit/Reject presentation lives) rather than brittle label prose —
263
+ # a reworded decision-gate label must not silently empty the check.
264
+ gate_line=$(grep -nE 'redact-sentinel\.sh' "${LEGAL_SKILL}" 2>/dev/null | head -1 | cut -d: -f1)
265
+ present_line=$(grep -nE '^## Phase 3' "${LEGAL_SKILL}" 2>/dev/null | head -1 | cut -d: -f1)
266
+ if [[ -n "${gate_line}" && -n "${present_line}" && "${gate_line}" -lt "${present_line}" ]]; then
267
+ echo "PASS: Test 11c: redaction gate precedes inline presentation (Phase 3) in legal-generate SKILL.md"
268
+ PASS=$((PASS + 1))
269
+ else
270
+ echo "FAIL: Test 11c: redaction gate must precede Phase 3 presentation (gate=${gate_line:-none}, phase3=${present_line:-none})"
271
+ FAIL=$((FAIL + 1))
272
+ fi
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # Test 12 — cross-script homoglyph honesty (AC1): a CONFUSABLE_MAP-covered prefix
276
+ # (Cyrillic s U+0455) is caught; an UNMAPPED lookalike (Cyrillic t U+0442) is a
277
+ # version-controlled KNOWN GAP (current behavior = exit 0, not claimed covered).
278
+ # ---------------------------------------------------------------------------
279
+ t12_cov="${TMP_DIR}/t12-covered.txt"
280
+ python3 -c "import sys; sys.stdout.write(chr(0x0455)+'k-ant-' + 'A'*32 + '\n')" > "${t12_cov}" # Cyrillic s -> s
281
+ bash "${SENTINEL}" "${t12_cov}" >/dev/null 2>&1
282
+ assert_exit "Test 12a: CONFUSABLE_MAP-covered homoglyph prefix is caught (exit 1)" 1 $?
283
+
284
+ t12_gap="${TMP_DIR}/t12-gap.txt"
285
+ python3 -c "import sys; sys.stdout.write('sk-an'+chr(0x0442)+'-' + 'A'*32 + '\n')" > "${t12_gap}" # Cyrillic t -> UNMAPPED
286
+ bash "${SENTINEL}" "${t12_gap}" >/dev/null 2>&1
287
+ assert_exit "Test 12b: UNMAPPED homoglyph is a version-controlled known gap (exit 0)" 0 $?
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # AC6 — no literal invisibles committed anywhere in the two touched skills.
291
+ # Exclusion: skills/legal-generate/references/templates/ is upstream-vendored
292
+ # corpus whose bytes are pinned to upstream blobs in the skill NOTICE — the
293
+ # blob pin is a strictly stronger integrity control than this grep (any local
294
+ # edit, invisibles included, fails vendor-pin-integrity), and verbatim byte
295
+ # fidelity is what keeps drift detection meaningful.
296
+ # ---------------------------------------------------------------------------
297
+ if grep -rlP '[\x{200b}\x{200c}\x{200d}\x{2060}\x{feff}\x{202a}-\x{202e}\x{2028}\x{2029}\x{00ad}\x{fffd}]' \
298
+ "${REPO_ROOT}/plugins/soleur/skills/incident" \
299
+ "${REPO_ROOT}/plugins/soleur/skills/legal-generate" 2>/dev/null \
300
+ | grep -v '/skills/legal-generate/references/templates/' | grep -c >/dev/null .; then
301
+ echo "FAIL: AC6: literal invisibles committed (must be chr()/escapes only)"
302
+ grep -rlP '[\x{200b}\x{200c}\x{200d}\x{2060}\x{feff}\x{202a}-\x{202e}\x{2028}\x{2029}\x{00ad}\x{fffd}]' \
303
+ "${REPO_ROOT}/plugins/soleur/skills/incident" \
304
+ "${REPO_ROOT}/plugins/soleur/skills/legal-generate" 2>/dev/null \
305
+ | grep -v '/skills/legal-generate/references/templates/'
306
+ FAIL=$((FAIL + 1))
307
+ else
308
+ echo "PASS: AC6: no literal invisibles committed in incident/legal-generate skills"
309
+ PASS=$((PASS + 1))
310
+ fi
311
+
312
+ # ===========================================================================
313
+ # #6045 PR-B — item 1 (whitespace/newline reflow re-scan) + item 6 (Cloudflare token).
314
+ # All fixtures synthesized at runtime; invisibles/newlines via chr() (AC6).
315
+ # ===========================================================================
316
+
317
+ # Test 13 — reflow two-engine (item 1, AC1 shape): a stripe key split by a NEWLINE.
318
+ # (a) OLD raw-byte engine MISSES the split token (exit 0); (b) the new engine's base pass also
319
+ # cannot match across the kept newline — only _scan_reflow catches it after whitespace-rejoin.
320
+ # stripe_key is a pre-#5987 class the OLD engine already carries, so 13a isolates the SPLIT-evasion.
321
+ t13_file="${TMP_DIR}/t13.txt"
322
+ python3 -c "import sys; sys.stdout.write('key sk_live_0000ABCD'+chr(0x0a)+'1234000000000000 end'+chr(0x0a))" > "${t13_file}"
323
+ bash "${OLD_ENGINE}" "${t13_file}" >/dev/null 2>&1
324
+ assert_exit "Test 13a: OLD raw-byte engine MISSES newline-split stripe key" 0 $?
325
+ t13_out="${TMP_DIR}/t13.out"
326
+ bash "${SENTINEL}" "${t13_file}" >"${t13_out}" 2>&1
327
+ assert_exit "Test 13b: reflow catches newline-split stripe key" 1 $?
328
+ assert_grep "Test 13b: stripe_key via whitespace-rejoin" "matched pattern stripe_key" "${t13_out}"
329
+
330
+ # Test 13c — reflow with a SPACE split (distinct whitespace kind from 13b's newline). A doppler
331
+ # token split by an ASCII space (0x20 survives normalization). Must be caught.
332
+ t13c_file="${TMP_DIR}/t13c.txt"
333
+ python3 -c "import sys; sys.stdout.write('tok dp.st.ABCD1234 EFGH5678IJKLMNOP done'+chr(0x0a))" > "${t13c_file}"
334
+ bash "${SENTINEL}" "${t13c_file}" >/dev/null 2>&1
335
+ assert_exit "Test 13c: reflow catches space-split doppler token" 1 $?
336
+
337
+ # Test 13d — reflow NO-FALSE-POSITIVE on an INCLUDED prefix (spec-flow highest-value guard):
338
+ # an included distinctive prefix (dp.st.) followed by DESPACEABLE PROSE must NOT manufacture a
339
+ # match. Naive whitespace-strip would glue the prose into a 16+ run; the bounded-split guard
340
+ # (rejoin across few runs only; prose has a run every few chars) rejects it. Must exit 0.
341
+ t13d_file="${TMP_DIR}/t13d.txt"
342
+ python3 -c "import sys; sys.stdout.write('note dp.st. the quick brown fox ran far away today ok'+chr(0x0a))" > "${t13d_file}"
343
+ bash "${SENTINEL}" "${t13d_file}" >/dev/null 2>&1
344
+ assert_exit "Test 13d: reflow does NOT manufacture a match from prose after an included prefix" 0 $?
345
+
346
+ # Test 13e — negative baseline still clean after the reflow pass (no manufactured matches).
347
+ bash "${SENTINEL}" "${NEGATIVE_BASELINE}" >/dev/null 2>&1
348
+ assert_exit "Test 13e: reflow pass does not manufacture matches on the clean PIR" 0 $?
349
+
350
+ # Test 13f — Test-4b invariant (meta-redaction) holds on a REFLOW-caught finding (new emit path).
351
+ t13f_out="${TMP_DIR}/t13f.out"
352
+ bash "${SENTINEL}" "${t13_file}" >"${t13f_out}" 2>&1 || true
353
+ if grep -qE 'matched pattern stripe_key \(whitespace-rejoin\)' "${t13f_out}" && \
354
+ ! grep -qE 'at offset [0-9]+: [^*]{5,}\*\*\*' "${t13f_out}"; then
355
+ echo "PASS: Test 13f: reflow finding is tagged and meta-redacted (<=4-char reveal)"
356
+ PASS=$((PASS + 1))
357
+ else
358
+ echo "FAIL: Test 13f: reflow finding missing tag or leaked >4 prefix chars"
359
+ cat "${t13f_out}"
360
+ FAIL=$((FAIL + 1))
361
+ fi
362
+
363
+ # Test 14 — Cloudflare token (item 6): a 40-char [A-Za-z0-9_-] token containing BOTH an uppercase
364
+ # letter AND a digit trips cloudflare_token. Synthesized (no real token).
365
+ t14_file="${TMP_DIR}/t14.txt"
366
+ python3 -c "import sys; sys.stdout.write('CF_TOKEN v1.0 Ab3'+'x'*37+' end'+chr(0x0a))" > "${t14_file}"
367
+ t14_out="${TMP_DIR}/t14.out"
368
+ bash "${SENTINEL}" "${t14_file}" >"${t14_out}" 2>&1
369
+ assert_exit "Test 14: 40-char upper+digit token trips cloudflare_token" 1 $?
370
+ assert_grep "Test 14: cloudflare_token matched" "matched pattern cloudflare_token" "${t14_out}"
371
+
372
+ # Test 14b — anti-SHA NO-FALSE-POSITIVE: a 40-char lowercase-hex git SHA (digit, NO uppercase)
373
+ # must NOT trip. Incident PIRs cite commit SHAs constantly.
374
+ t14b_file="${TMP_DIR}/t14b.txt"
375
+ python3 -c "import sys; sys.stdout.write('commit '+'a1b2c3d4'*5+' shipped'+chr(0x0a))" > "${t14b_file}"
376
+ t14b_out="${TMP_DIR}/t14b.out"
377
+ bash "${SENTINEL}" "${t14b_file}" >"${t14b_out}" 2>&1
378
+ if grep -qE 'matched pattern cloudflare_token' "${t14b_out}"; then
379
+ echo "FAIL: Test 14b: a 40-char git SHA falsely tripped cloudflare_token"
380
+ FAIL=$((FAIL + 1))
381
+ else
382
+ echo "PASS: Test 14b: 40-char lowercase-hex git SHA does NOT trip cloudflare_token (anti-SHA predicate)"
383
+ PASS=$((PASS + 1))
384
+ fi
385
+
386
+ # Test 14c — anti-prose NO-FALSE-POSITIVE: a 40-char kebab-case identifier span (NO uppercase, NO
387
+ # digit) must NOT trip.
388
+ t14c_file="${TMP_DIR}/t14c.txt"
389
+ python3 -c "import sys; sys.stdout.write('rule cq-'+'-'.join(['abc']*9)+' xx'+chr(0x0a))" > "${t14c_file}"
390
+ t14c_out="${TMP_DIR}/t14c.out"
391
+ bash "${SENTINEL}" "${t14c_file}" >"${t14c_out}" 2>&1
392
+ if grep -qE 'matched pattern cloudflare_token' "${t14c_out}"; then
393
+ echo "FAIL: Test 14c: a 40-char kebab span falsely tripped cloudflare_token"
394
+ FAIL=$((FAIL + 1))
395
+ else
396
+ echo "PASS: Test 14c: 40-char kebab prose does NOT trip cloudflare_token"
397
+ PASS=$((PASS + 1))
398
+ fi
399
+
400
+ # Test 14d — negative baseline still clean with cloudflare_token added (cites SHAs).
401
+ bash "${SENTINEL}" "${NEGATIVE_BASELINE}" >/dev/null 2>&1
402
+ assert_exit "Test 14d: cloudflare_token class does not manufacture matches on the clean PIR" 0 $?
403
+
404
+ # ===========================================================================
405
+ # #6045 PR-C — item 2 (base64/hex/percent decode-and-rescan) + item 3 (headerless-PEM
406
+ # private-key DER discriminator). DER fixtures are SYNTHESIZED from the ASN.1 spec (no
407
+ # key material) so they exercise the discriminator branches directly.
408
+ # ===========================================================================
409
+
410
+ # DER shape helpers (Python): seq() wraps content in a SEQUENCE with correct short/long-form length.
411
+ _DER_PY='
412
+ import base64, sys
413
+ def seq(content):
414
+ n = len(content)
415
+ if n < 0x80: length = bytes([n])
416
+ elif n < 0x100: length = bytes([0x81, n])
417
+ else: length = bytes([0x82, n >> 8, n & 0xFF])
418
+ return bytes([0x30]) + length + content
419
+ def b64(b): return base64.b64encode(b).decode()
420
+ # private-key-shaped, SHORT-form length (EC/Ed25519 size): SEQUENCE{ INTEGER 1, OCTET STRING(44) }
421
+ priv_short = seq(bytes([0x02,0x01,0x01]) + bytes([0x04,0x2C]) + b"\x00"*44)
422
+ # private-key-shaped, LONG-form length (RSA size): SEQUENCE{ INTEGER 0, OCTET STRING(300) }
423
+ priv_long = seq(bytes([0x02,0x01,0x00]) + bytes([0x04,0x82,0x01,0x2C]) + b"\x00"*300)
424
+ # cert/SPKI/encrypted-PKCS#8 shape: SEQUENCE{ SEQUENCE(44) } — first inner is SEQUENCE, must REJECT
425
+ cert_like = seq(seq(b"\x00"*44))
426
+ # PNG magic (0x89) — not a SEQUENCE, must REJECT
427
+ png = bytes([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]) + b"\x00"*56
428
+ '
429
+
430
+ # Test 15 — item 3 private-key DER discriminator POSITIVES (short + long form).
431
+ t15a="${TMP_DIR}/t15a.txt"
432
+ python3 -c "${_DER_PY}
433
+ sys.stdout.write('k8s data: ' + b64(priv_short) + '\n')" > "${t15a}"
434
+ bash "${SENTINEL}" "${t15a}" >${TMP_DIR}/t15a.out 2>&1
435
+ assert_exit "Test 15a: short-form (EC/Ed25519-size) headerless private-key DER caught" 1 $?
436
+ assert_grep "Test 15a: pem_key_body" "matched pattern pem_key_body" ${TMP_DIR}/t15a.out
437
+
438
+ t15b="${TMP_DIR}/t15b.txt"
439
+ python3 -c "${_DER_PY}
440
+ sys.stdout.write('blob ' + b64(priv_long) + '\n')" > "${t15b}"
441
+ bash "${SENTINEL}" "${t15b}" >/dev/null 2>&1
442
+ assert_exit "Test 15b: long-form (RSA-size) headerless private-key DER caught" 1 $?
443
+
444
+ # Test 15c — item 3 with a 64-char-WRAPPED body (real PEM bodies are line-wrapped): block assembly
445
+ # must join consecutive base64 lines before decoding.
446
+ t15c="${TMP_DIR}/t15c.txt"
447
+ python3 -c "${_DER_PY}
448
+ b = b64(priv_long)
449
+ wrapped = '\n'.join(b[i:i+64] for i in range(0, len(b), 64))
450
+ sys.stdout.write('-- body --\n' + wrapped + '\n-- end --\n')" > "${t15c}"
451
+ bash "${SENTINEL}" "${t15c}" >/dev/null 2>&1
452
+ assert_exit "Test 15c: 64-char-wrapped headerless private-key body caught via block assembly" 1 $?
453
+
454
+ # Test 15d — item 3 NO-FALSE-POSITIVE: cert/SPKI/encrypted-PKCS#8 shape (inner SEQUENCE) must NOT
455
+ # trip pem_key_body — a public cert pasted into a legal draft is legitimate; flagging it fail-closes.
456
+ t15d="${TMP_DIR}/t15d.txt"
457
+ python3 -c "${_DER_PY}
458
+ sys.stdout.write('cert ' + b64(cert_like) + '\n')" > "${t15d}"
459
+ bash "${SENTINEL}" "${t15d}" >${TMP_DIR}/t15d.out 2>&1
460
+ if grep -qE 'matched pattern pem_key_body' ${TMP_DIR}/t15d.out; then
461
+ echo "FAIL: Test 15d: a public-cert-shaped DER (inner SEQUENCE) falsely tripped pem_key_body"; FAIL=$((FAIL + 1))
462
+ else
463
+ echo "PASS: Test 15d: cert/SPKI/encrypted-PKCS#8 shape does NOT trip pem_key_body (private-key discriminator)"; PASS=$((PASS + 1))
464
+ fi
465
+
466
+ # Test 15e — item 3 NO-FALSE-POSITIVE: a PNG image body (0x89 magic) must NOT trip pem_key_body.
467
+ t15e="${TMP_DIR}/t15e.txt"
468
+ python3 -c "${_DER_PY}
469
+ sys.stdout.write('img ' + b64(png) + '\n')" > "${t15e}"
470
+ bash "${SENTINEL}" "${t15e}" >${TMP_DIR}/t15e.out 2>&1
471
+ if grep -qE 'matched pattern pem_key_body' ${TMP_DIR}/t15e.out; then
472
+ echo "FAIL: Test 15e: a PNG body falsely tripped pem_key_body"; FAIL=$((FAIL + 1))
473
+ else
474
+ echo "PASS: Test 15e: PNG image body does NOT trip pem_key_body"; PASS=$((PASS + 1))
475
+ fi
476
+
477
+ # Test 16 — item 2 decode-and-rescan POSITIVES (base64, base64url, hex, percent) of a known secret.
478
+ _SECRET_PY='
479
+ import base64, urllib.parse, binascii, sys
480
+ # Split across concatenation so no contiguous sk_live_<24+> literal exists in source (GitHub push
481
+ # protection scans the raw file; cq-test-fixtures-synthesized-only). 38B body -> base64 is 52 chars
482
+ # (not 40 -> no cloudflare_token collision in the decode test).
483
+ s = b"sk_" + b"live_0000ABCD1234567890abcdefXYZ789"
484
+ b64s = base64.b64encode(s).decode()
485
+ b64url = base64.urlsafe_b64encode(s).decode()
486
+ hexs = s.hex()
487
+ pcts = urllib.parse.quote(s.decode())
488
+ '
489
+ for enc in b64s b64url hexs pcts; do
490
+ f="${TMP_DIR}/t16-${enc}.txt"
491
+ python3 -c "${_SECRET_PY}
492
+ sys.stdout.write('payload: ' + ${enc} + ' trailer\n')" > "${f}"
493
+ bash "${SENTINEL}" "${f}" >${TMP_DIR}/t16.out 2>&1
494
+ assert_exit "Test 16.${enc}: encoded stripe key decoded and caught" 1 $?
495
+ assert_grep "Test 16.${enc}: stripe_key via decode" "matched pattern stripe_key" ${TMP_DIR}/t16.out
496
+ done
497
+
498
+ # Test 16b — decode NO-FALSE-POSITIVE corpus: innocent encoded content must NOT manufacture a match.
499
+ # (i) base64 of a PNG image; (ii) an SRI sha512- hash; (iii) a git SHA / sha256 hex; (iv) a base64
500
+ # JWT-payload-shaped JSON containing an email (email is decode-skipped → must stay clean); (v) a
501
+ # percent-encoded URL.
502
+ t16b="${TMP_DIR}/t16b.txt"
503
+ python3 -c "
504
+ import base64, sys
505
+ png = bytes([0x89,0x50,0x4E,0x47]) + bytes(range(60))
506
+ sri = 'sha512-' + base64.b64encode(bytes(range(64))).decode()
507
+ jwtjson = base64.urlsafe_b64encode(b'{\"sub\":\"1\",\"email\":\"a@b.com\"}').decode()
508
+ sys.stdout.write('image ' + base64.b64encode(png).decode() + '\n')
509
+ sys.stdout.write('integrity ' + sri + '\n')
510
+ sys.stdout.write('commit ' + 'ab'*20 + ' sha256 ' + 'cd'*32 + '\n')
511
+ sys.stdout.write('token ' + jwtjson + '\n')
512
+ sys.stdout.write('url https://x.example/a%2Fb%2Fc?q=1 done\n')
513
+ " > "${t16b}"
514
+ bash "${SENTINEL}" "${t16b}" >${TMP_DIR}/t16b.out 2>&1
515
+ if [[ $? -eq 0 ]]; then
516
+ echo "PASS: Test 16b: innocent encoded content (image/SRI/SHA/JWT-JSON-email/percent-URL) does NOT manufacture a match"; PASS=$((PASS + 1))
517
+ else
518
+ echo "FAIL: Test 16b: decode pass manufactured a false positive on innocent encoded content"; cat ${TMP_DIR}/t16b.out; FAIL=$((FAIL + 1))
519
+ fi
520
+
521
+ # Test 16c — malformed base64 candidate must NOT bubble to the exit-2 catch-all (per-candidate guard).
522
+ # A real secret alongside a malformed blob must still be caught (exit 1), not fail-closed to exit 2.
523
+ t16c="${TMP_DIR}/t16c.txt"
524
+ python3 -c "
525
+ import base64, sys
526
+ good = base64.b64encode(b'sk_' + b'live_0000ABCD1234567890abXY').decode() # split literal: gitleaks stripe rule (cq-test-fixtures-synthesized-only)
527
+ sys.stdout.write('bad !!!====notbase64==== ' + good + ' end\n')
528
+ " > "${t16c}"
529
+ bash "${SENTINEL}" "${t16c}" >/dev/null 2>&1
530
+ assert_exit "Test 16c: malformed base64 candidate skipped per-candidate (real secret still caught, exit 1 not 2)" 1 $?
531
+
532
+ # Test 16d — Test-4b invariant on a DECODE-caught finding (new emit path).
533
+ bash "${SENTINEL}" "${TMP_DIR}/t16-b64s.txt" >${TMP_DIR}/t16d.out 2>&1 || true
534
+ if grep -qE 'matched pattern stripe_key \(decoded' ${TMP_DIR}/t16d.out && \
535
+ ! grep -qE 'at offset [0-9]+: [^*]{5,}\*\*\*' ${TMP_DIR}/t16d.out; then
536
+ echo "PASS: Test 16d: decode finding is tagged and meta-redacted (<=4-char reveal)"; PASS=$((PASS + 1))
537
+ else
538
+ echo "FAIL: Test 16d: decode finding missing tag or leaked >4 prefix chars"; cat ${TMP_DIR}/t16d.out; FAIL=$((FAIL + 1))
539
+ fi
540
+
541
+ # Test 16e — behavioral fan-out bound: a base64-run-flooded input completes without hanging and
542
+ # exits cleanly (no manufactured matches); pins _MAX_ENCODED_CANDIDATES against a future cap removal.
543
+ t16e="${TMP_DIR}/t16e.txt"
544
+ python3 -c "import sys; sys.stdout.write((('QUJDREVGR0hJSktMTU5PUFFSU1Q ')*5000))" > "${t16e}"
545
+ timeout 30 bash "${SENTINEL}" "${t16e}" >/dev/null 2>&1
546
+ rc=$?
547
+ if [[ $rc -eq 0 || $rc -eq 1 ]]; then
548
+ echo "PASS: Test 16e: base64-flooded input completes within bound (rc=${rc}, not a timeout)"; PASS=$((PASS + 1))
549
+ else
550
+ echo "FAIL: Test 16e: base64-flooded input did not complete (rc=${rc} — possible unbounded fan-out)"; FAIL=$((FAIL + 1))
551
+ fi
552
+
553
+ # Test 15f — item 3 with an INDENTED, 32-col-wrapped body (the real k8s Secret / Helm `data:` shape):
554
+ # block assembly must .strip() each line so indented base64 assembles. (#6045 review F2)
555
+ t15f="${TMP_DIR}/t15f.txt"
556
+ python3 -c "${_DER_PY}
557
+ b = b64(priv_long)
558
+ wrapped = '\n'.join(' ' + b[i:i+32] for i in range(0, len(b), 32)) # 4-space indent, 32-col
559
+ sys.stdout.write('data:\n tls.key: |\n' + wrapped + '\n')" > "${t15f}"
560
+ bash "${SENTINEL}" "${t15f}" >/dev/null 2>&1
561
+ assert_exit "Test 15f: indented 32-col-wrapped k8s Secret private-key body caught via block assembly" 1 $?
562
+
563
+ # Test 16f — decode NO-FALSE-POSITIVE on a BINARY asset: a base64'd mostly-non-printable blob that
564
+ # carries an INCIDENTAL sk- run must NOT fail-close (the printable-text gate skips binary). (#6045 review F4)
565
+ t16f="${TMP_DIR}/t16f.txt"
566
+ python3 -c "
567
+ import base64, sys
568
+ # 200 high bytes (a font/wasm/image shape) with an incidental 'sk-...' run spliced in.
569
+ blob = bytes(range(0x80, 0x100)) * 2 + b'sk-A1b2C3d4E5f6G7h8I9j0' + bytes(range(0x80, 0x100))
570
+ sys.stdout.write('embedded asset: ' + base64.b64encode(blob).decode() + ' end\n')
571
+ " > "${t16f}"
572
+ bash "${SENTINEL}" "${t16f}" >${TMP_DIR}/t16f.out 2>&1
573
+ if [[ $? -eq 0 ]]; then
574
+ echo "PASS: Test 16f: a base64 binary asset with an incidental sk- run does NOT fail-close (printable-text gate)"; PASS=$((PASS + 1))
575
+ else
576
+ echo "FAIL: Test 16f: decode pass fail-closed a legitimate binary asset (over-redaction FP)"; cat ${TMP_DIR}/t16f.out; FAIL=$((FAIL + 1))
577
+ fi
578
+
579
+ # Test 17 — email-class ReDoS bound (#6045 security review P1): a large run of email-class chars with
580
+ # NO '@' must complete quickly (bounded quantifier → linear), not hang the fail-closed gate. 256 KiB of
581
+ # 'a' at the pre-fix O(n^2) rate timed out (>2 min); bounded it returns in well under the 20s ceiling.
582
+ t17="${TMP_DIR}/t17.txt"
583
+ python3 -c "import sys; sys.stdout.write('a'*262144)" > "${t17}"
584
+ timeout 20 bash "${SENTINEL}" "${t17}" >/dev/null 2>&1
585
+ rc=$?
586
+ if [[ $rc -eq 0 || $rc -eq 1 ]]; then
587
+ echo "PASS: Test 17: 256 KiB email-class flood completes within 20s (email quantifier bounded — no O(n^2) ReDoS)"; PASS=$((PASS + 1))
588
+ else
589
+ echo "FAIL: Test 17: email-class flood did not complete (rc=${rc} — O(n^2) ReDoS regression)"; FAIL=$((FAIL + 1))
590
+ fi
591
+
592
+ # Test 18 — deployed-anchor coupling, RETARGETED and STRENGTHENED (#7450; was #6156 review P2).
593
+ #
594
+ # The original pinned the two redact-sentinel call sites byte-identical to each other, which is
595
+ # necessary and was never sufficient: two files can agree perfectly on an anchor that resolves
596
+ # into the reviewed party's tree. A naive `ANCHOR` swap would have preserved only that weak
597
+ # property and left the corpus unguarded, so this now asserts THREE things
598
+ # (`cq-assert-anchor-not-bare-token`).
599
+ INCIDENT_SKILL="${SKILL_DIR}/SKILL.md"
600
+ LINEAR_SKILL="${REPO_ROOT}/plugins/soleur/skills/linear-fetch/SKILL.md"
601
+ # The 4th gate ADR-179's negative branch names. It carries the preflight but nothing pinned
602
+ # it, and its halts carried no marker — so C10 covered 3 of the 4 skills the ADR credits.
603
+ TRIGGER_SKILL="${REPO_ROOT}/plugins/soleur/skills/trigger-cron/SKILL.md"
604
+
605
+ # 18a — coupling (preserved): the bare literal appears exactly 1x in each site.
606
+ ANCHOR='${CLAUDE_PLUGIN_ROOT}/skills/incident/scripts/redact-sentinel.sh'
607
+ lg_anchor=$(grep -Fc "${ANCHOR}" "${LEGAL_SKILL}" 2>/dev/null || true)
608
+ inc_anchor=$(grep -Fc "${ANCHOR}" "${INCIDENT_SKILL}" 2>/dev/null || true)
609
+ if [[ "${lg_anchor}" == "1" && "${inc_anchor}" == "1" ]]; then
610
+ echo "PASS: Test 18a: bare deployed-anchor redact-sentinel literal is byte-identical (1×) in both legal-generate & incident SKILL.md"
611
+ PASS=$((PASS + 1))
612
+ else
613
+ echo "FAIL: Test 18a: deployed-anchor drift — expected the ADR-179 bare anchored redact-sentinel literal exactly once in each (legal-generate=${lg_anchor}, incident=${inc_anchor})"
614
+ FAIL=$((FAIL + 1))
615
+ fi
616
+
617
+ # 18b — the ADR-179 decision-2 identity preflight is present at every secret-gate site.
618
+ # Anchored on the manifest name check, which a prose sentence cannot produce; a bare token like
619
+ # "preflight" or "CLAUDE_PLUGIN_ROOT" would be satisfied by a comment ABOUT the guard.
620
+ # `[[ -r ]]` does NOT satisfy this: ADR-179 §(a) measured a shape check passing while an
621
+ # attacker-chosen payload executed.
622
+ PREFLIGHT_NEEDLE='grep -q '"'"'"name"[[:space:]]*:[[:space:]]*"soleur"'"'"''
623
+ missing_preflight=""
624
+ unbound_halt=""
625
+
626
+ # STRENGTHENED, not cut (#7450 review). Guard 1 asserts the same property in
627
+ # TypeScript, but `scripts/test-all.sh` shards the two suites separately — Guard 1
628
+ # with the web-platform group, this file with the scripts group — so they do not
629
+ # always run together and neither is redundant with the other.
630
+ #
631
+ # Presence of the manifest check was never the property worth asserting: every gate
632
+ # carries a SECOND fail-closed check (`[[ -r "$SENTINEL" ]] || { …; exit 2; }`)
633
+ # immediately below, so "the file contains a preflight AND contains an exit 2" is
634
+ # satisfied with the preflight's own arm converted to a no-op. This now requires the
635
+ # halt to sit inside the preflight's OWN brace group, tracked by brace depth.
636
+ preflight_halts_in_own_arm() {
637
+ awk '
638
+ index($0, "\"${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json\"") && !started { started = 1 }
639
+ started {
640
+ stmt = stmt $0 "\n"
641
+ n = gsub(/\{/, "{"); depth += n
642
+ n = gsub(/\}/, "}"); depth -= n
643
+ # Statement ends when braces balance and no continuation is pending.
644
+ if (depth <= 0 && $0 !~ /\\[[:space:]]*$/) {
645
+ print (stmt ~ /exit 2/) ? "BOUND" : "UNBOUND"
646
+ exit
647
+ }
648
+ }
649
+ END { if (!started) print "ABSENT" }
650
+ ' "$1"
651
+ }
652
+
653
+ for gate_file in "${INCIDENT_SKILL}" "${LEGAL_SKILL}" "${LINEAR_SKILL}"; do
654
+ n=$(grep -Fc "${PREFLIGHT_NEEDLE}" "${gate_file}" 2>/dev/null || true)
655
+ if [[ "${n}" -lt 1 ]]; then
656
+ missing_preflight="${missing_preflight} ${gate_file##*/skills/}"
657
+ continue
658
+ fi
659
+ verdict="$(preflight_halts_in_own_arm "${gate_file}")"
660
+ [[ "${verdict}" == "BOUND" ]] || unbound_halt="${unbound_halt} ${gate_file##*/skills/}(${verdict})"
661
+ done
662
+
663
+ if [[ -z "${missing_preflight}" && -z "${unbound_halt}" ]]; then
664
+ echo "PASS: Test 18b: identity preflight present at all 3 secret-gate sites, and each HALTS IN ITS OWN ARM"
665
+ PASS=$((PASS + 1))
666
+ else
667
+ [[ -n "${missing_preflight}" ]] && echo "FAIL: Test 18b: identity preflight missing at:${missing_preflight} — the bare anchor is NOT safe by construction; the preflight is what carries it (ADR-179 decision 2)"
668
+ [[ -n "${unbound_halt}" ]] && echo "FAIL: Test 18b: identity preflight present but its own arm does not exit 2 at:${unbound_halt} — a sibling halt elsewhere in the fence does not make THIS check fail-closed"
669
+ FAIL=$((FAIL + 1))
670
+ fi
671
+
672
+ # 18c — corpus-wide negative. This single assertion mechanically closes ADR-179 §R5 and stops the
673
+ # rejected form reappearing ANYWHERE under the payload, which per-file counts structurally cannot
674
+ # do. The needle is built by concatenation so this file does not match its own assertion — the
675
+ # same reason the suite splits token-shaped fixtures.
676
+ #
677
+ # The split point sits INSIDE the rev-parse argument, not at the variable-name boundary. Splitting
678
+ # at the boundary leaves the shorter default-arm substring contiguous, and that shorter form is the
679
+ # needle this PR's own AC1 sweep greps for — so the file would report ITSELF as an unmigrated site
680
+ # while its own assertion passed. Do not restore the literal to this comment for readability: a
681
+ # body-grep cannot tell an explanation from an occurrence.
682
+ FORBIDDEN='${CLAUDE_PLUGIN_ROOT:-$(git rev-parse ''--show-toplevel)'
683
+ SEARCH_ROOT="${REPO_ROOT}/plugins/soleur/"
684
+
685
+ # ANTI-VACUITY FLOOR (#7450 review-finding A12). `grep -r` on a nonexistent root exits
686
+ # 2, `|| true` swallows it, `residual` is empty and the assertion reports a clean
687
+ # corpus — a PASS that means "the scan could not run", which is the one outcome a
688
+ # corpus-wide negative must never render as good. Two positive controls: the root must
689
+ # exist, and a literal KNOWN to be present must actually be found. If the apparatus is
690
+ # broken, the control fails loudly instead of the negative passing quietly.
691
+ control_hits=$(grep -rlF '${CLAUDE_PLUGIN_ROOT}' "${SEARCH_ROOT}" 2>/dev/null | grep -c . || true)
692
+ if [[ ! -d "${SEARCH_ROOT}" ]]; then
693
+ echo "FAIL: Test 18c: search root ${SEARCH_ROOT} does not exist — the corpus-wide negative cannot be evaluated (a vacuous PASS is not a clean corpus)"
694
+ FAIL=$((FAIL + 1))
695
+ elif [[ "${control_hits}" -lt 10 ]]; then
696
+ echo "FAIL: Test 18c: positive control found only ${control_hits} files containing the bare anchor under ${SEARCH_ROOT} — expected >=10; the scan apparatus is broken, so its empty result proves nothing"
697
+ FAIL=$((FAIL + 1))
698
+ else
699
+ residual=$(grep -rlF "${FORBIDDEN}" "${SEARCH_ROOT}" 2>/dev/null || true)
700
+ if [[ -z "${residual}" ]]; then
701
+ echo "PASS: Test 18c: zero git-root-defaulted plugin-root anchors remain anywhere under plugins/soleur/ (control: ${control_hits} files scanned and matched)"
702
+ PASS=$((PASS + 1))
703
+ else
704
+ echo "FAIL: Test 18c: the rejected git-root-default form survives at:"
705
+ echo "${residual}" | sed 's/^/ /'
706
+ FAIL=$((FAIL + 1))
707
+ fi
708
+ fi
709
+
710
+ # ---------------------------------------------------------------------------
711
+ # Test 19 — DECOY POSITIVE CONTROL for the untrusted-anchor vector (#7450, ADR-179).
712
+ #
713
+ # Test 18 above pins the two call sites to each OTHER. Nothing pinned either of them to a
714
+ # path the reviewed party cannot control — which is the actual defect: `review/SKILL.md`
715
+ # instructs `gh pr checkout`, so on the review path `$(git rev-parse --show-toplevel)` IS the
716
+ # contributor's tree, and the gate whose exit code decides whether secrets are emitted resolves
717
+ # its own scanner from there.
718
+ #
719
+ # The oracle is the anchor expression AS COMMITTED in incident/SKILL.md, extracted at runtime.
720
+ # It is deliberately NOT duplicated into this file: a guard whose expected value is a literal
721
+ # copied from the artifact it guards is a set-difference over one producer, and goes vacuous the
722
+ # moment someone edits both together
723
+ # (learnings/2026-07-19-a-self-graded-mutation-battery-went-vacuous-twice-in-one-pr...).
724
+ #
725
+ # 19a proves the decoy is a LIVE hazard rather than an inert file — the real sentinel flags the
726
+ # same fixture that the decoy waves through. A positive control that could not have passed
727
+ # proves nothing.
728
+ # ---------------------------------------------------------------------------
729
+ t19_tree="${TMP_DIR}/t19-contributor-tree"
730
+ mkdir -p "${t19_tree}/plugins/soleur/skills/incident/scripts" || {
731
+ echo "FAIL: Test 19: could not build the contributor-tree sandbox — a harness that cannot SET UP must abort, not report a verdict about the SUT" >&2
732
+ exit 2
733
+ }
734
+ git -C "${t19_tree}" init -q >/dev/null 2>&1
735
+ # Compare physical paths: mktemp may hand back a symlinked prefix while
736
+ # `git rev-parse --show-toplevel` always reports the resolved one, which would make the
737
+ # containment test below silently false-PASS.
738
+ t19_tree="$(cd "${t19_tree}" && pwd -P)"
739
+ t19_decoy="${t19_tree}/plugins/soleur/skills/incident/scripts/redact-sentinel.sh"
740
+ printf '#!/usr/bin/env bash\nexit 0\n' > "${t19_decoy}"
741
+ chmod 0755 "${t19_decoy}"
742
+
743
+ # 19a — the decoy is a live hazard: it passes a file the real sentinel rejects.
744
+ t19_secret="${TMP_DIR}/t19-secret.md"
745
+ python3 -c "import sys; sys.stdout.write('anthropic_key: ' + 'sk-' + 'ant-api03-' + 'A'*32 + '\n')" > "${t19_secret}"
746
+ bash "${SENTINEL}" "${t19_secret}" >/dev/null 2>&1
747
+ t19_real_rc=$?
748
+ bash "${t19_decoy}" "${t19_secret}" >/dev/null 2>&1
749
+ t19_decoy_rc=$?
750
+ if [[ "${t19_real_rc}" -eq 1 && "${t19_decoy_rc}" -eq 0 ]]; then
751
+ echo "PASS: Test 19a: decoy is a live hazard (real sentinel exit=1 on the fixture, decoy exit=0)"
752
+ PASS=$((PASS + 1))
753
+ else
754
+ echo "FAIL: Test 19a: decoy is not a valid positive control (real=${t19_real_rc} expected 1, decoy=${t19_decoy_rc} expected 0)"
755
+ FAIL=$((FAIL + 1))
756
+ fi
757
+
758
+ # 19b — extract the anchor from the committed producer, and refuse to run vacuously.
759
+ #
760
+ # THREE hardenings over the original (#7450 review):
761
+ #
762
+ # (1) BASH FENCES ONLY, and exactly one assignment per file. The old extractor was a
763
+ # whole-file `sed … | head -1`, so an EARLIER ```text doc example carrying a
764
+ # well-formed assignment became the oracle and silently retargeted 19b/19c away
765
+ # from the live gate — oracle shadowing, measured. `head -1` is what made it
766
+ # silent: a second match was not a conflict, it was simply discarded.
767
+ # (2) (SENTINEL|SCRUBBER), not SENTINEL alone. The decoy control never reached
768
+ # linear-fetch, which the PR body calls the highest residual risk, because its
769
+ # variable is named SCRUBBER.
770
+ # (3) Both gate files are driven, not just incident.
771
+ extract_gate_anchor() {
772
+ awk '
773
+ /^[[:space:]]*```bash[[:space:]]*$/ { infence = 1; next }
774
+ /^[[:space:]]*```/ { infence = 0; next }
775
+ infence && /^[[:space:]]*(SENTINEL|SCRUBBER)="[^"]*"[[:space:]]*$/ {
776
+ line = $0
777
+ sub(/^[[:space:]]*(SENTINEL|SCRUBBER)="/, "", line)
778
+ sub(/"[[:space:]]*$/, "", line)
779
+ print line
780
+ }
781
+ ' "$1"
782
+ }
783
+
784
+ for t19_gate in "${INCIDENT_SKILL}" "${LINEAR_SKILL}"; do
785
+ t19_name="${t19_gate##*/skills/}"
786
+ t19_anchors="$(extract_gate_anchor "${t19_gate}")"
787
+ t19_n=$(printf '%s\n' "${t19_anchors}" | grep -c . || true)
788
+ # DISTINCT values, not occurrences. The hazard this pins is a doc example or a second,
789
+ # DIFFERING assignment shadowing the real anchor — so the property is "the file names
790
+ # exactly one anchor value", not "it names it exactly once".
791
+ #
792
+ # Requiring a single occurrence was a proxy, and it was wrong in the fail-CLOSED direction:
793
+ # `linear-fetch` must legitimately re-derive `SCRUBBER` in its Phase D fence, because each
794
+ # fenced block is a separate Bash call and shell state does not persist across them. The
795
+ # single-occurrence form rejected the correct code (and, before it was fixed, accepted the
796
+ # bricked version that defined the variable in one fence and used it in another).
797
+ # Two byte-identical assignments cannot shadow each other; two differing ones can, and
798
+ # still fail here.
799
+ t19_distinct=$(printf '%s\n' "${t19_anchors}" | grep . | sort -u | grep -c . || true)
800
+
801
+ if [[ "${t19_n}" -lt 1 || "${t19_distinct}" -ne 1 ]]; then
802
+ echo "FAIL: Test 19b[${t19_name}]: expected exactly ONE DISTINCT gate-anchor value in bash fences, found ${t19_distinct} distinct across ${t19_n} assignment(s) — a differing second assignment can shadow the real anchor, and an extractor that takes the first of several would not notice"
803
+ FAIL=$((FAIL + 1))
804
+ continue
805
+ fi
806
+ # The UNIQUE value — asserted to be the only distinct one above. Taking the raw multi-line
807
+ # capture would leave `grep -F` treating the embedded newline as a pattern separator, so a
808
+ # match on either line would satisfy it; that happens to work while the values are identical
809
+ # and stops working the moment they are not, which is exactly the case being guarded.
810
+ t19_expr="$(printf '%s\n' "${t19_anchors}" | grep . | sort -u)"
811
+
812
+ if grep -Fq "${t19_expr}" "${t19_gate}"; then
813
+ echo "PASS: Test 19b[${t19_name}]: anchor expression extracted from the committed SKILL.md"
814
+ PASS=$((PASS + 1))
815
+ else
816
+ echo "FAIL: Test 19b[${t19_name}]: anchor extraction is vacuous — extracted text absent from the file"
817
+ FAIL=$((FAIL + 1))
818
+ continue
819
+ fi
820
+
821
+ # 19c — resolve the committed expression the way a reviewer's session would: plugin
822
+ # root UNRESOLVED, CWD inside the checked-out contributor tree.
823
+ #
824
+ # (a) NEVER bash-expand this text. On the review path the SKILL.md is the CONTRIBUTOR's
825
+ # file, so the previous form — writing the expression into a script and running it
826
+ # — handed arbitrary command substitution from a hostile PR straight to bash inside
827
+ # the reviewer's own test run. The guard for the untrusted-anchor vector was itself
828
+ # an untrusted-input execution sink. Metacharacters are now a REFUSAL, not input.
829
+ #
830
+ # (b) RESOLVE BEFORE CONTAINMENT. The old `case` compared an UNRESOLVED string against
831
+ # an absolute `${t19_tree}/*` prefix, so a bare RELATIVE path never matched the
832
+ # prefix and therefore always "passed" — while resolving, by definition, against
833
+ # the caller's CWD, which is the contributor tree. The relative case is the
834
+ # failure, so it must be tested as one.
835
+ case "${t19_expr}" in
836
+ *'$('*|*'`'*|*';'*|*'|'*|*'&'*|*'>'*|*'<'*)
837
+ echo "FAIL: Test 19c[${t19_name}]: anchor expression carries shell metacharacters (${t19_expr}) — refusing to expand contributor-controlled text"
838
+ FAIL=$((FAIL + 1))
839
+ continue ;;
840
+ *'${CLAUDE_PLUGIN_ROOT:'*)
841
+ echo "FAIL: Test 19c[${t19_name}]: anchor uses a :- / :? default (${t19_expr}) — ADR-179 rejects both; the default arm is the vector"
842
+ FAIL=$((FAIL + 1))
843
+ continue ;;
844
+ esac
845
+
846
+ # Pure textual substitution of the unresolved root — no shell evaluation.
847
+ t19_resolved="${t19_expr//\$\{CLAUDE_PLUGIN_ROOT\}/}"
848
+ case "${t19_resolved}" in
849
+ /*) case "${t19_resolved}/" in
850
+ "${t19_tree}"/*) t19_inside=1 ;;
851
+ *) t19_inside=0 ;;
852
+ esac ;;
853
+ # Relative: resolves against the caller's CWD, i.e. INTO the contributor tree.
854
+ *) t19_inside=1 ;;
855
+ esac
856
+
857
+ if [[ -n "${t19_resolved}" && "${t19_inside}" -eq 0 ]]; then
858
+ echo "PASS: Test 19c[${t19_name}]: committed anchor resolves outside the contributor tree (${t19_resolved})"
859
+ PASS=$((PASS + 1))
860
+ else
861
+ echo "FAIL: Test 19c[${t19_name}]: committed anchor resolves INTO the reviewed party's tree — a hostile PR's gate script would run as the gate (resolved=${t19_resolved:-<empty>}, tree=${t19_tree})"
862
+ FAIL=$((FAIL + 1))
863
+ fi
864
+ done
865
+
866
+ # ---------------------------------------------------------------------------
867
+ # Test 20 — CROSS-FILE anti-vacuity floor for the Guard 1 skills-axis block.
868
+ #
869
+ # Guard 1's own floor is closure-scoped to its `describe`, so DELETING THE WHOLE
870
+ # #7450 BLOCK was measured green at 9 tests: the floor died with the thing it
871
+ # counted. An in-file floor structurally cannot survive the deletion of its own
872
+ # file or block, so the floor that can must live somewhere else.
873
+ #
874
+ # Here specifically, and not in a sibling vitest file: `scripts/test-all.sh` shards
875
+ # these two suites differently (Guard 1 under the web-platform group, this one under
876
+ # the scripts group), so they do not always run together. A floor in the same shard
877
+ # would be reaped by the same skip that reaped the thing it guards.
878
+ # ---------------------------------------------------------------------------
879
+ T20_GUARD="${REPO_ROOT}/apps/web-platform/test/plugin-root-anchoring.test.ts"
880
+ T20_FLOOR=18
881
+
882
+ if [[ ! -f "${T20_GUARD}" ]]; then
883
+ echo "FAIL: Test 20: Guard 1 is GONE (${T20_GUARD}) — the skills-axis anchoring assertions no longer exist"
884
+ FAIL=$((FAIL + 1))
885
+ elif ! grep -Fq 'skills secret-gate subset (#7450)' "${T20_GUARD}"; then
886
+ echo "FAIL: Test 20: Guard 1 no longer contains the #7450 skills secret-gate describe block — deleting it is exactly what its own in-file floor cannot detect"
887
+ FAIL=$((FAIL + 1))
888
+ else
889
+ # SCOPED to the #7450 describe, not `| tail -1` over the whole file.
890
+ #
891
+ # The old form took the LAST `expect(assertions).toBe(N)` anywhere in the guard as a proxy
892
+ # for "Guard 1's floor". That is correct only while #7450 is the final block in the file.
893
+ # PR #8570 appended a third describe (the #7453 skills-ratchet axis, floor 8) AFTER it, so
894
+ # `tail -1` silently started reading the new block's floor and this test failed claiming the
895
+ # #7450 floor had been "lowered" to 8 — naming a cause that had not happened, on a file whose
896
+ # #7450 block was untouched. A positional proxy for a named thing breaks the moment the
897
+ # corpus grows, which is the exact class this suite exists to catch elsewhere.
898
+ #
899
+ # Range ends at the next line beginning `describe(`; sed searches the end pattern from the
900
+ # line AFTER the start, so the opening describe cannot close its own range. `head -1` takes
901
+ # the block's own floor.
902
+ t20_declared=$(sed -n '/^describe(.*#7450/,/^describe(/{ s/^[[:space:]]*expect(assertions)\.toBe(\([0-9]\+\));[[:space:]]*$/\1/p }' "${T20_GUARD}" | head -1)
903
+ if [[ "${t20_declared}" != "${T20_FLOOR}" ]]; then
904
+ echo "FAIL: Test 20: Guard 1's #7450 assertion floor is '${t20_declared:-<none>}', expected ${T20_FLOOR} — a floor lowered in the same commit that removes assertions is the failure mode this pins"
905
+ FAIL=$((FAIL + 1))
906
+ else
907
+ echo "PASS: Test 20: Guard 1's #7450 block is present and still declares ${T20_FLOOR} decided assertions (cross-file, cross-shard floor)"
908
+ PASS=$((PASS + 1))
909
+ fi
910
+ fi
911
+
912
+ # ---------------------------------------------------------------------------
913
+ # SHARED EXECUTABLE-LINE EXTRACTOR for Tests 21, 22 and 23.
914
+ #
915
+ # THE RECURRING DEFECT THIS CLOSES. Tests 22, 23 and 24 were written in the same two commits
916
+ # as Test 21 and asserted against the file's BYTES, not its executable content — so each was
917
+ # defeated by a COMMENT. Test 21 alone built an extractor, and its own comment argues at
918
+ # length why that scoping is load-bearing. Measured at round-2 review: deleting the real
919
+ # `[ -n "$PERSIST_SAFE" ]` arm and leaving a three-line comment quoting it kept BOTH Test 22
920
+ # and Test 23 green.
921
+ #
922
+ # So the extractor is shared rather than duplicated: a future assertion inherits the immunity
923
+ # instead of having to remember it.
924
+ #
925
+ # FENCE HANDLING, every arm added because a mutation walked through it:
926
+ # V1 ```sh / ```shell / ```zsh — an agent executes those exactly as it executes ```bash.
927
+ # This was the cheapest evasion of all: one word.
928
+ # V2 ~~~ fences, which CommonMark treats identically to ```.
929
+ # P2 a fence that is opened and never closed, and a stray ``` INSIDE a fence (a heredoc
930
+ # emitting markdown) closing it early — so the close marker must match the opening
931
+ # run length, and an unclosed fence is reported rather than silently swallowing the
932
+ # rest of the file.
933
+ # Info strings (```bash title="x") and CRLF were already handled and are kept.
934
+ # ---------------------------------------------------------------------------
935
+ exec_lines() {
936
+ awk '
937
+ # opening fence: ``` or ~~~ (>=3), a shell-ish language, optional info string
938
+ !infence && match($0, /^[[:space:]]*(`{3,}|~{3,})[[:space:]]*(bash|sh|shell|zsh)([[:space:]]|$)/) {
939
+ line = $0
940
+ sub(/^[[:space:]]*/, "", line)
941
+ run = 0; ch = substr(line, 1, 1)
942
+ while (substr(line, run + 1, 1) == ch) run++
943
+ infence = 1; fch = ch; frun = run; next
944
+ }
945
+ # a non-shell fence still OPENS a fence — otherwise its contents leak into the scan
946
+ !infence && match($0, /^[[:space:]]*(`{3,}|~{3,})/) {
947
+ line = $0; sub(/^[[:space:]]*/, "", line)
948
+ run = 0; ch = substr(line, 1, 1)
949
+ while (substr(line, run + 1, 1) == ch) run++
950
+ infence = 2; fch = ch; frun = run; next
951
+ }
952
+ # closing fence: same char, at least the opening run length, nothing but the marker
953
+ infence && match($0, /^[[:space:]]*(`{3,}|~{3,})[[:space:]]*\r?$/) {
954
+ line = $0; sub(/^[[:space:]]*/, "", line)
955
+ run = 0; ch = substr(line, 1, 1)
956
+ while (substr(line, run + 1, 1) == ch) run++
957
+ if (ch == fch && run >= frun) { infence = 0 }
958
+ next
959
+ }
960
+ # SHELL COMMENTS INSIDE THE FENCE ARE NOT EXECUTABLE EITHER.
961
+ # Fence-scoping alone is not enough and this was measured: with the fence scoping in
962
+ # place, deleting the real `[ -n "$PERSIST_SAFE" ]` arm and leaving a comment quoting it
963
+ # still passed Test 23, because the comment sits inside the ```bash fence. "Executable
964
+ # content" has to mean executable, not merely fenced.
965
+ infence == 1 && /^[[:space:]]*#/ { next }
966
+ infence == 1 { print }
967
+ END { if (infence) exit 3 }
968
+ ' "$1"
969
+ }
970
+
971
+ # ---------------------------------------------------------------------------
972
+ # Test 21 — B1: PIN THE ABSENCE of any git-root resolution at the three secret gates.
973
+ #
974
+ # Review finding §B1 prescribed adding, at each gate,
975
+ # case "$(cd "${CLAUDE_PLUGIN_ROOT}" && pwd -P)/" in "$(git rev-parse --show-toplevel)/"*) halt;;
976
+ # That was measured and REJECTED (specs/.../b1-disposition.md): the loader substitutes the bare
977
+ # token at delivery, so the operand is a literal the adversary cannot reach — while the
978
+ # assertion breaks dogfooding on any plain clone (there the plugin root IS inside the working
979
+ # tree) and puts `git rev-parse --show-toplevel` back into the exact three files this PR
980
+ # removes it from.
981
+ #
982
+ # A disposition that is merely SATISFIED is not guarded, so this pins it. Deliberately broader
983
+ # than 18c, which is keyed to the `${CLAUDE_PLUGIN_ROOT:-...}` SYNTAX and so cannot see a bare
984
+ # `$(git rev-parse --show-toplevel)` — precisely the shape §B1 proposes.
985
+ # ---------------------------------------------------------------------------
986
+ t21_offenders=""
987
+ t21_unclosed=""
988
+ for gate_file in "${INCIDENT_SKILL}" "${LEGAL_SKILL}" "${LINEAR_SKILL}"; do
989
+ if ! exec_lines "${gate_file}" >/dev/null 2>&1; then
990
+ t21_unclosed="${t21_unclosed} ${gate_file##*/skills/}"
991
+ continue
992
+ fi
993
+ n=$(exec_lines "${gate_file}" | grep -c 'git rev-parse' || true)
994
+ [[ "${n}" -ne 0 ]] && t21_offenders="${t21_offenders} ${gate_file##*/skills/}(${n})"
995
+ done
996
+
997
+ # ANTI-VACUITY: PER-FILE required membership, never a total count.
998
+ #
999
+ # A `>= N` total fails OPEN on shrinkage — measured, the previous floor was `>= 3` against a
1000
+ # real population of 15 (5 anchor lines per gate), so a one-character regression that blinded
1001
+ # every INDENTED fence dropped `legal-generate` to 0 of its 5 while the other two still
1002
+ # supplied 10, and B1's `case` planted in the blinded file stayed GREEN. That is finding A10
1003
+ # reintroduced — and required-membership was the fix already applied to Test 24's floor in the
1004
+ # same commit, and not carried across to its sibling.
1005
+ t21_blind=""
1006
+ for gate_file in "${INCIDENT_SKILL}" "${LEGAL_SKILL}" "${LINEAR_SKILL}"; do
1007
+ c=$(exec_lines "${gate_file}" 2>/dev/null | grep -c 'CLAUDE_PLUGIN_ROOT' || true)
1008
+ [[ "${c}" -ge 1 ]] || t21_blind="${t21_blind} ${gate_file##*/skills/}"
1009
+ done
1010
+
1011
+ if [[ -n "${t21_unclosed}" ]]; then
1012
+ echo "FAIL: Test 21: unterminated bash fence in:${t21_unclosed} — the extractor cannot bound the executable region, so every verdict over this file would be unreliable"
1013
+ FAIL=$((FAIL + 1))
1014
+ elif [[ -n "${t21_blind}" ]]; then
1015
+ echo "FAIL: Test 21: the fence extractor sees ZERO anchor lines in:${t21_blind} — it is blind to that gate's executable content, so a zero-offender verdict there is vacuous (per-file floor, not a total: a total lets one gate go dark while its siblings carry the count)"
1016
+ FAIL=$((FAIL + 1))
1017
+ elif [[ -z "${t21_offenders}" ]]; then
1018
+ echo "PASS: Test 21: all 3 secret gates carry ZERO git-root resolution in executable positions, and the extractor sees every gate (the B1 invariant is pinned, not merely satisfied)"
1019
+ PASS=$((PASS + 1))
1020
+ else
1021
+ echo "FAIL: Test 21: git-root resolution reappeared in an executable fence at:${t21_offenders} — see specs/feat-one-shot-7450-git-root-anchor-untrusted/b1-disposition.md. If this is a deliberate reversal, that document must be superseded FIRST; a gate that resolves the git root is the #7450 vector itself"
1022
+ FAIL=$((FAIL + 1))
1023
+ fi
1024
+
1025
+ # ---------------------------------------------------------------------------
1026
+ # Test 22 — C10: every fail-closed halt on the anchor path emits a SOLEUR_* marker,
1027
+ # ON STDOUT, FROM AN EXECUTABLE LINE.
1028
+ #
1029
+ # `go.md` and `sync.md` already emit one and `go.md` documents why: a refusal nobody can see
1030
+ # in telemetry is indistinguishable from a run that never happened.
1031
+ #
1032
+ # THREE evasions this now closes, all measured green against the previous version:
1033
+ # W1 the marker present only in PROSE while the real echo was silenced — the whole point
1034
+ # of `exec_lines`.
1035
+ # W2 `exit 2; } >&2` — the redirect on the BRACE GROUP rather than the marker line, so a
1036
+ # substring test on the marker line alone sees nothing.
1037
+ # W5 `>&"2"` and `1>&2` spellings.
1038
+ # And W3, the growth gap: the reason table is hardcoded, so a NEW fail-closed halt added with
1039
+ # no marker passed silently. The cardinality check below closes that — the number of halts and
1040
+ # the number of markers must agree per file.
1041
+ # ---------------------------------------------------------------------------
1042
+ t22_missing=""
1043
+ t22_on_stderr=""
1044
+ t22_uncovered=""
1045
+ t22_specs="${INCIDENT_SKILL}:SOLEUR_INCIDENT_HALT:plugin-root-unverified,sentinel-unreadable,draft-alloc-failed,draft-empty,invalid-calendar-date,mttr-transposed,mttd-transposed
1046
+ ${LEGAL_SKILL}:SOLEUR_LEGAL_GENERATE_HALT:plugin-root-unverified,sentinel-unreadable,draft-alloc-failed,draft-empty
1047
+ ${LINEAR_SKILL}:SOLEUR_LINEAR_FETCH_HALT:plugin-root-unverified,scrubber-unreadable,scrubber-nonzero-exit,redaction-empty-output,redaction-ineffective
1048
+ ${TRIGGER_SKILL}:SOLEUR_TRIGGER_CRON_HALT:plugin-root-unverified,producer-missing"
1049
+
1050
+ while IFS= read -r t22_spec; do
1051
+ [[ -n "${t22_spec}" ]] || continue
1052
+ t22_file="${t22_spec%%:*}"
1053
+ t22_rest="${t22_spec#*:}"
1054
+ t22_prefix="${t22_rest%%:*}"
1055
+ t22_reasons="${t22_rest#*:}"
1056
+ t22_exec="$(exec_lines "${t22_file}" 2>/dev/null)"
1057
+
1058
+ while IFS= read -r t22_reason; do
1059
+ [[ -n "${t22_reason}" ]] || continue
1060
+ # EXECUTABLE lines only, and the marker must be EMITTED (an `echo`/`printf` argument),
1061
+ # not merely mentioned.
1062
+ t22_line="$(printf '%s\n' "${t22_exec}" \
1063
+ | grep -E "(echo|printf)[^|]*${t22_prefix} reason=${t22_reason}" || true)"
1064
+ if [[ -z "${t22_line}" ]]; then
1065
+ t22_missing="${t22_missing} ${t22_file##*/skills/}:${t22_reason}"
1066
+ else
1067
+ # Inspect the MARKER'S OWN command segment, not the whole line. A single-line brace
1068
+ # group puts the marker echo and the human-guidance echo (`… >&2`) on one line, so a
1069
+ # line-wide test reports every correctly-written marker as stderr-bound — measured, it
1070
+ # flagged all three date halts the moment they were marked. Cut from the marker's echo
1071
+ # to the next `;`, which is where that command ends.
1072
+ t22_seg="$(printf '%s' "${t22_line}" \
1073
+ | sed -E "s/.*((echo|printf)[^;]*${t22_prefix} reason=${t22_reason}[^;]*).*/\\1/")"
1074
+ # ...AND the enclosing brace group. `{ echo "<marker>"; …; exit 2; } >&2` redirects the
1075
+ # WHOLE arm, including the marker, while leaving the marker's own segment clean — so a
1076
+ # per-command test alone reports it as compliant. Measured: that mutation landed and the
1077
+ # suite stayed green until this arm existed. Walk from the marker to the point where
1078
+ # brace depth returns to zero and inspect the closing line.
1079
+ t22_arm_redir="$(printf '%s\n' "${t22_exec}" | awk -v mark="${t22_prefix} reason=${t22_reason}" '
1080
+ index($0, mark) { armed = 1 }
1081
+ armed {
1082
+ n = gsub(/\{/, "{"); m = gsub(/\}/, "}")
1083
+ depth += n - m
1084
+ if (depth <= 0 && /\}/) {
1085
+ if ($0 ~ /\}[[:space:]]*[0-9]*>&[[:space:]]*"?2"?/) print "REDIR"
1086
+ armed = 0
1087
+ }
1088
+ }')"
1089
+ if printf '%s' "${t22_seg}" | grep -cE >/dev/null '>&[[:space:]]*"?2"?|[0-9]+>&' \
1090
+ || [[ -n "${t22_arm_redir}" ]]; then
1091
+ t22_on_stderr="${t22_on_stderr} ${t22_file##*/skills/}:${t22_reason}"
1092
+ fi
1093
+ fi
1094
+ done < <(printf '%s\n' "${t22_reasons}" | tr ',' '\n')
1095
+
1096
+ # GROWTH FLOOR (W3): a new fail-closed halt must arrive with a marker. Count `exit 2`
1097
+ # dispatches and distinct markers in the executable region and require agreement.
1098
+ t22_exits=$(printf '%s\n' "${t22_exec}" | grep -c 'exit 2' || true)
1099
+ t22_marks=$(printf '%s\n' "${t22_exec}" | grep -oE "${t22_prefix} reason=[a-z-]+" | sort -u | grep -c . || true)
1100
+ if [[ "${t22_exits}" -gt "${t22_marks}" ]]; then
1101
+ t22_uncovered="${t22_uncovered} ${t22_file##*/skills/}(${t22_exits} halts vs ${t22_marks} markers)"
1102
+ fi
1103
+ done < <(printf '%s\n' "${t22_specs}")
1104
+
1105
+ if [[ -z "${t22_missing}" && -z "${t22_on_stderr}" && -z "${t22_uncovered}" ]]; then
1106
+ echo "PASS: Test 22: all 18 fail-closed halts across the 4 gates (incident 7, legal-generate 4, linear-fetch 5, trigger-cron 2) emit a distinct SOLEUR_*_HALT reason= marker, on STDOUT, from an executable line — and each gate's halt count is covered by its marker count"
1107
+ PASS=$((PASS + 1))
1108
+ else
1109
+ [[ -n "${t22_missing}" ]] && echo "FAIL: Test 22: fail-closed halt with no SOLEUR_* marker EMITTED from an executable line at:${t22_missing} — a marker that appears only in prose or a comment is documentation, and a halt nobody can see in telemetry is indistinguishable from a run that never happened"
1110
+ [[ -n "${t22_on_stderr}" ]] && echo "FAIL: Test 22: SOLEUR_* marker redirected to stderr at:${t22_on_stderr} — the human guidance belongs on stderr, but the MARKER is the machine-readable half and must go to stdout (check the brace group too: \`exit 2; } >&2\` redirects the whole arm)"
1111
+ [[ -n "${t22_uncovered}" ]] && echo "FAIL: Test 22: a gate has more fail-closed halts than distinct markers at:${t22_uncovered} — a new halt was added without one, and the reason table above cannot notice that on its own"
1112
+ FAIL=$((FAIL + 1))
1113
+ fi
1114
+
1115
+ # ---------------------------------------------------------------------------
1116
+ # Test 23 — C12/AC5d: the linear-fetch non-empty check is ASSERTED, not merely present.
1117
+ #
1118
+ # AC5d claimed "asserted by a test, not by inspection" and no test asserted it: deleting
1119
+ # `[ -n "$PERSIST_SAFE" ]` left the whole suite green.
1120
+ #
1121
+ # The first fix used `grep -A6` for the `exit 2`, and review defeated it two ways: a COMMENT
1122
+ # quoting the old arm satisfied the presence half (X1b), and `[ -n … ] || true` next to an
1123
+ # UNRELATED guard's `exit 2` satisfied the dispatch half (X2) — finding A2 reintroduced inside
1124
+ # the assertion whose own comment cites A2. A window cannot tell whose `exit 2` it found.
1125
+ #
1126
+ # So this reuses `preflight_halts_in_own_arm`'s technique, which already exists in this file
1127
+ # and solved exactly this: require the halt inside the check's OWN brace group, by brace depth.
1128
+ # ---------------------------------------------------------------------------
1129
+ t23_needle='[ -n "$PERSIST_SAFE" ]'
1130
+ t23_exec="$(exec_lines "${LINEAR_SKILL}" 2>/dev/null)"
1131
+ t23_n=$(printf '%s\n' "${t23_exec}" | grep -Fc "${t23_needle}" || true)
1132
+ t23_bound=$(printf '%s\n' "${t23_exec}" | awk -v needle="${t23_needle}" '
1133
+ index($0, needle) { armed = 1 }
1134
+ armed {
1135
+ n = gsub(/\{/, "{"); m = gsub(/\}/, "}")
1136
+ depth += n - m
1137
+ if ($0 ~ /exit[[:space:]]+2/) { found = 1 }
1138
+ if (armed && depth <= 0 && /\}/) { armed = 0 }
1139
+ }
1140
+ END { print (found ? 1 : 0) }
1141
+ ')
1142
+
1143
+ if [[ "${t23_n}" -ge 1 && "${t23_bound}" -eq 1 ]]; then
1144
+ echo "PASS: Test 23: linear-fetch asserts non-empty redaction output on an executable line AND halts inside that check's OWN arm (AC5d is test-enforced)"
1145
+ PASS=$((PASS + 1))
1146
+ else
1147
+ echo "FAIL: Test 23: the linear-fetch non-empty guard is missing from executable lines or does not halt in its own arm (present=${t23_n}, halts-in-own-arm=${t23_bound}) — empty scrubber output would be persisted as persist_safe_summary and the callers' absent-artifact contract, which is what stops them substituting the bearer-URL-bearing agent_context, would never fire"
1148
+ FAIL=$((FAIL + 1))
1149
+ fi
1150
+
1151
+ # ---------------------------------------------------------------------------
1152
+ # Test 24 — STAKES-KEYED discovery, INVARIANT-keyed violation, NO allowlist.
1153
+ # (CTO ruling item 2, rewritten at #7450 round-2 review.)
1154
+ #
1155
+ # WHY THIS IS NOT KEYED ON SYNTAX. Test 18c is keyed to one literal,
1156
+ # `${CLAUDE_PLUGIN_ROOT:-$(git rev-parse …)}`. That is what let finding B2 hide: `trigger-cron`
1157
+ # invoked a script running `doppler secrets get … -c prd --plain` through the `./`-relative
1158
+ # `:-` form, so 18c truthfully printed "zero … remain anywhere under plugins/soleur/" while a
1159
+ # cheaper-to-exploit secret site sat one syntax away. A needle shaped like yesterday's bug
1160
+ # cannot see tomorrow's.
1161
+ #
1162
+ # The first version of THIS test then made the same mistake one level up: it keyed the
1163
+ # violation on `CLAUDE_PLUGIN_ROOT:-`, so it could not see the 16 credential scripts invoked
1164
+ # through a BARE repo-relative path — strictly worse, because a bare path is CWD-relative
1165
+ # unconditionally. So the assertion below is written as the INVARIANT:
1166
+ #
1167
+ # no credential-ACQUIRING script is reachable through a path that resolves relative to CWD
1168
+ #
1169
+ # `:-`, bare `plugins/soleur/…` and `./plugins/soleur/…` are three instances of one class. A
1170
+ # fourth spelling needs no new clause.
1171
+ #
1172
+ # WHY ACQUISITION, NOT ADJACENCY. The ruling says "reads a secret". Keying on credential-shaped
1173
+ # NAMES instead enrols every redaction engine, because token names are their needles — measured:
1174
+ # a name-keyed predicate matched `$SESSION_TOKENS` (LLM tokens), `$TRAILER_KEY` (a map key) and
1175
+ # `${LINEAR_CDN_PATTERNS}` (via `_PAT`), inflating the population by 9 with scripts that acquire
1176
+ # nothing. It also drags in `worktree-manager.sh`, which copies `.env*` files but acquires no
1177
+ # named credential — and which #7453 owns.
1178
+ #
1179
+ # The boundary, as a DEFINITION rather than an allowlist (so a future member is excluded by
1180
+ # reasoning, not by having been omitted from a list): a script that copies files which may
1181
+ # contain secrets, on a machine that has already granted it execution, adds no marginal exposure
1182
+ # beyond the generic arbitrary-code-execution that #7453 addresses for all ~105 deferred sites.
1183
+ # A script whose purpose is to FETCH a named production credential and act with it is a direct
1184
+ # exfiltration primitive when substituted.
1185
+ # ---------------------------------------------------------------------------
1186
+
1187
+ # A credential token inside a COMMENT is a mention, not an acquisition. Without this a
1188
+ # `# Never echo the API_KEY` line enrols a script that handles no credential, and the resulting
1189
+ # CI failure asserts something untrue about it.
1190
+ t24_strip_comments() {
1191
+ case "$1" in
1192
+ *.ts|*.mjs|*.cjs) sed -E 's,//.*,,' "$1" | grep -vE '^[[:space:]]*\*' ;;
1193
+ *) grep -vE '^[[:space:]]*#' "$1" ;;
1194
+ esac
1195
+ }
1196
+
1197
+ # Keyed on the ACT of obtaining or presenting a credential. `read -[a-z]*s` (not `read -s`)
1198
+ # because the real sites write `read -rs`; that exact miss cost a false-clean during the ruling.
1199
+ t24_acq_re='doppler secrets (get|download)|op read |vault kv get|gh auth token|read -[a-z]*s |Authorization: *[A-Za-z]|-H .Authorization|process\.env\.[A-Z_]*(TOKEN|_KEY|SECRET|PASSWORD)|env\("[A-Z_]*(TOKEN|_KEY|SECRET|PASSWORD)'
1200
+
1201
+ t24_acquirers=()
1202
+ while IFS= read -r t24_f; do
1203
+ t24_strip_comments "${t24_f}" | grep -cE >/dev/null "${t24_acq_re}" \
1204
+ && t24_acquirers+=("${t24_f#"${REPO_ROOT}"/plugins/soleur/}")
1205
+ done < <(find "${REPO_ROOT}/plugins/soleur" -path '*/scripts/*' -type f \
1206
+ \( -name '*.sh' -o -name '*.py' -o -name '*.ts' -o -name '*.mjs' -o -name '*.cjs' \) \
1207
+ ! -name '*.test.sh' 2>/dev/null | sort)
1208
+
1209
+ # ANTI-VACUITY: required MEMBERSHIP, never a count. A bare `>= N` floor fails OPEN on
1210
+ # shrinkage — measured, an early draft used `>= 5` against a population of 21 and a mutation
1211
+ # that gutted the predicate down to 9 members SURVIVED it. That is finding A10's shape.
1212
+ # These three are pinned because each is a DISTINCT acquisition mechanism; a predicate that
1213
+ # stops seeing any one of them has stopped doing its job whatever its total.
1214
+ # trigger.sh — secrets-manager read (`doppler secrets get … -c prd --plain`)
1215
+ # discord-setup.sh — presents a bot token (`Authorization: Bot` on curl's stdin config)
1216
+ # provision-hetzner.sh — silent interactive prompt (`read -rs`)
1217
+ # `community-router.sh` is deliberately NOT pinned: under an acquisition predicate it correctly
1218
+ # drops out, because its only credential mention is inside a routing-table string.
1219
+ t24_required=(
1220
+ "skills/trigger-cron/scripts/trigger.sh"
1221
+ "skills/community/scripts/discord-setup.sh"
1222
+ "skills/provision-hetzner/scripts/provision-hetzner.sh"
1223
+ )
1224
+ t24_missing_required=""
1225
+ for t24_req in "${t24_required[@]}"; do
1226
+ t24_found=0
1227
+ for t24_have in ${t24_acquirers[@]+"${t24_acquirers[@]}"}; do
1228
+ [[ "${t24_have}" == "${t24_req}" ]] && { t24_found=1; break; }
1229
+ done
1230
+ [[ "${t24_found}" -eq 1 ]] || t24_missing_required="${t24_missing_required} ${t24_req}"
1231
+ done
1232
+
1233
+ if [[ -n "${t24_missing_required}" ]]; then
1234
+ echo "FAIL: Test 24: credential-acquisition discovery no longer sees:${t24_missing_required} — each is a DISTINCT acquisition mechanism (secrets-manager read / presented bot token / silent prompt), so the predicate is broken and the zero-violation verdict below would be vacuous. Population is ${#t24_acquirers[@]}."
1235
+ FAIL=$((FAIL + 1))
1236
+ else
1237
+ t24_violations=""
1238
+ for t24_rel in ${t24_acquirers[@]+"${t24_acquirers[@]}"}; do
1239
+ # Keyed on the PAYLOAD-RELATIVE PATH, never the basename: `flip.sh` is a suffix of
1240
+ # `audit-flag-flip.sh`, and a bare basename also collides with markdown link targets and
1241
+ # `# Usage:` lines. The full path is the script's unambiguous identity.
1242
+ t24_rel_re="$(printf '%s' "${t24_rel}" | sed 's/[.[\*^$]/\\&/g')"
1243
+ while IFS= read -r t24_hit; do
1244
+ [[ -n "${t24_hit}" ]] || continue
1245
+ t24_file="${t24_hit%%:*}"; t24_rest="${t24_hit#*:}"
1246
+ t24_lno="${t24_rest%%:*}"; t24_line="${t24_rest#*:}"
1247
+
1248
+ # A script naming its OWN path (a usage header, a self-referential constant) is not a
1249
+ # call to itself.
1250
+ [[ "${t24_file}" == *"${t24_rel}" ]] && continue
1251
+
1252
+ # A path inside a COMMENT is documentation, not an invocation — `audit-flag-flip.sh`
1253
+ # lists its four consumers in a comment block, which is a manifest, not a call.
1254
+ case "${t24_file}" in
1255
+ *.sh|*.py) printf '%s' "${t24_line}" | grep -cE >/dev/null '^[[:space:]]*#' && continue ;;
1256
+ *.ts|*.mjs|*.cjs) printf '%s' "${t24_line}" | grep -cE >/dev/null '^[[:space:]]*(//|\*)' && continue ;;
1257
+ esac
1258
+
1259
+ # EXECUTION context only. The invariant is about reachability FOR EXECUTION, so a path
1260
+ # merely read or written as CONTENT is a data root and correct as-is (the ruling's
1261
+ # classification table). Measured: `flag-delete/scripts/delete.sh` names `flip.sh` twice
1262
+ # to edit a map entry inside it — a source edit, not a call — and flagging that would
1263
+ # teach the next reader that data roots need anchoring, which inverts the rule.
1264
+ t24_is_exec=0
1265
+ # (1) an explicit verb, or a prose "Run <path>" instruction
1266
+ printf '%s' "${t24_line}" \
1267
+ | grep -cE >/dev/null "(^|[[:space:];&|(])(bash|sh|bun|node|python3|python|exec|source|\.)[[:space:]]+[^[:space:]]*${t24_rel_re}|[Rr]un[[:space:]]+[^[:space:]]*${t24_rel_re}" \
1268
+ && t24_is_exec=1
1269
+ # (2) a BARE quoted-path invocation — `"${CLAUDE_PLUGIN_ROOT}/…/trigger.sh" --list`
1270
+ # relies on the file's exec bit and carries no verb at all.
1271
+ printf '%s' "${t24_line}" | grep -cE >/dev/null "^[[:space:]]*\"?[^[:space:]]*${t24_rel_re}\"?[[:space:]]" \
1272
+ && t24_is_exec=1
1273
+ # (3) ASSIGN-THEN-INVOKE. `TRIGGER="…/trigger.sh"` then `bash "$TRIGGER"` on another
1274
+ # line is THE dominant shape in this corpus — it is why Guard 1 needs G2 at all ("the
1275
+ # anchor lives in an assignment, so an operand rule certifies the invocation while the
1276
+ # assignment points anywhere"). Requiring a verb on the same line missed every one of
1277
+ # them: measured, reverting trigger-cron's anchor to the `:-` form SURVIVED, because
1278
+ # all three of its sites are an assignment and two bare invocations.
1279
+ # The variable must actually be INVOKED somewhere in the file — otherwise this would
1280
+ # re-flag `flag-delete`'s `FLIP_SH`, which is only ever read as content (a data root).
1281
+ # `t24_rel_re` is a PAYLOAD-RELATIVE PATH, so it always contains `/` — which is this
1282
+ # `s///`'s own delimiter. Interpolating it raw ended the expression early and sed died
1283
+ # with `unknown option to 's'` for EVERY acquirer, leaving `t24_var` empty and this whole
1284
+ # arm dead while the suite still read 96/0. That is the vacuity class this file exists to
1285
+ # catch, in the arm whose comment says the mutation it guards already SURVIVED once.
1286
+ # Escape the delimiter for sed only: `t24_rel_re` is reused verbatim in the `grep -E`
1287
+ # arms above, where `\/` is not portable.
1288
+ t24_rel_sed="${t24_rel_re//\//\\/}"
1289
+ t24_var="$(printf '%s' "${t24_line}" | sed -nE "s/^[[:space:]]*(readonly[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=.*${t24_rel_sed}.*/\\2/p")"
1290
+ if [[ -n "${t24_var}" ]] \
1291
+ && grep -qE "(^|[[:space:];&|(])(bash|sh|bun|node|python3|python|exec|source|\.)[[:space:]]+\"?\\\$\{?${t24_var}\}?\"?|^[[:space:]]*\"\\\$\{?${t24_var}\}?\"[[:space:]]" "${t24_file}"; then
1292
+ t24_is_exec=1
1293
+ fi
1294
+ [[ "${t24_is_exec}" -eq 1 ]] || continue
1295
+
1296
+ # POSITIVE requirement — the invariant, not a list of bad spellings. TWO prefixes satisfy
1297
+ # it, and both must be accepted or the guard flags correct code (and gets weakened by
1298
+ # whoever hits that next):
1299
+ # ${CLAUDE_PLUGIN_ROOT}/… loader-substituted at delivery (ADR-179 decision 1)
1300
+ # $SCRIPT_DIR / $BASH_SOURCE layout-invariant per ADR-178, likewise not CWD-derived
1301
+ printf '%s' "${t24_line}" | grep -cE >/dev/null '\$\{CLAUDE_PLUGIN_ROOT\}/' && continue
1302
+ # The operator-terminal handoff shape of ADR-249 (#8486): a skill tells the agent to PRINT
1303
+ # `cd <WORKTREE> && bash <WORKTREE>/…` with <WORKTREE> replaced by the absolute worktree path,
1304
+ # for the operator to run in their own terminal. Like ${CLAUDE_PLUGIN_ROOT} (substituted
1305
+ # absolute by the loader), the placeholder is substituted absolute by the agent, and the `cd`
1306
+ # into the same absolute root means no working directory chooses the script. Anchored on the
1307
+ # WHOLE shape at line start, not on the token, so a bare `bash <WORKTREE>/…` elsewhere still
1308
+ # counts. ${CLAUDE_PLUGIN_ROOT} cannot serve here: it is unset in the operator's terminal.
1309
+ printf '%s' "${t24_line}" | grep -cE >/dev/null '^[[:space:]]*cd <WORKTREE> && bash <WORKTREE>/' && continue
1310
+ printf '%s' "${t24_line}" | grep -cE >/dev/null '\$\{?(SCRIPT_DIR|BASH_SOURCE)' && continue
1311
+
1312
+ t24_violations="${t24_violations}
1313
+ ${t24_file#"${REPO_ROOT}"/}:${t24_lno}"
1314
+ done < <(grep -rn -F "${t24_rel}" "${REPO_ROOT}/plugins/soleur" \
1315
+ --include='*.md' --include='*.sh' --include='*.json' --include='*.ts' 2>/dev/null \
1316
+ | grep -v '/test/' | grep -v '\.test\.' || true)
1317
+ done
1318
+
1319
+ if [[ -z "${t24_violations}" ]]; then
1320
+ echo "PASS: Test 24: none of the ${#t24_acquirers[@]} discovered credential-acquiring scripts is reachable through a CWD-relative path (stakes-keyed discovery, invariant-keyed violation, no allowlist)"
1321
+ PASS=$((PASS + 1))
1322
+ else
1323
+ echo "FAIL: Test 24: a credential-ACQUIRING script is invoked through a path that resolves relative to CWD, so the session's own working directory decides which script runs:${t24_violations}"
1324
+ echo " Fix: anchor the invocation on the bare quoted \${CLAUDE_PLUGIN_ROOT}, or resolve it \$BASH_SOURCE-relative if the target is a sibling in the payload."
1325
+ echo " This assertion has NO allowlist by ruling — deferring to #7453 is for non-gate sites, not for sites that hand over a credential."
1326
+ FAIL=$((FAIL + 1))
1327
+ fi
1328
+ fi
1329
+
1330
+
1331
+ echo
1332
+ # ---------------------------------------------------------------------------
1333
+ # Test 25 — an engine crash is reported by class name and the engine's own frame only (PR2 of #10331): the exception text
1334
+ # can carry scanned content, so the fail-closed arm must not print it.
1335
+ # ---------------------------------------------------------------------------
1336
+ t25_out="${TMP_DIR}/test25.out"
1337
+ python3 -I - "${SKILL_DIR}/scripts/redact-engine.py" >"${t25_out}" 2>&1 <<'PY'
1338
+ import importlib.util, sys
1339
+ spec = importlib.util.spec_from_file_location("redact_engine_under_test", sys.argv[1])
1340
+ mod = importlib.util.module_from_spec(spec)
1341
+ spec.loader.exec_module(mod)
1342
+ def boom(path):
1343
+ raise ValueError("CANARY-7f3a")
1344
+ mod.scan = boom
1345
+ sys.argv = ["redact-engine.py", "x"]
1346
+ print("RC=%s" % mod.main())
1347
+ PY
1348
+ assert_grep "Test 25: a crashing engine exits 2" "^RC=2$" "${t25_out}"
1349
+ assert_grep "Test 25: the crash line names the class" "redact-engine: internal error: ValueError at main:[0-9]+$" "${t25_out}"
1350
+ if grep -q "CANARY" "${t25_out}"; then
1351
+ echo "FAIL: Test 25: the exception text reached the output"
1352
+ FAIL=$((FAIL + 1))
1353
+ else
1354
+ echo "PASS: Test 25: the exception text is absent from the output"
1355
+ PASS=$((PASS + 1))
1356
+ fi
1357
+
1358
+ # Test 25b — with stderr closed (sys.stderr is None) the fail-closed arm still returns 2, never an uncaught crash.
1359
+ t25b_out="${TMP_DIR}/test25b.out"
1360
+ python3 -I - "${SKILL_DIR}/scripts/redact-engine.py" >"${t25b_out}" 2>&1 <<'PY'
1361
+ import importlib.util, sys
1362
+ spec = importlib.util.spec_from_file_location("redact_engine_under_test", sys.argv[1])
1363
+ mod = importlib.util.module_from_spec(spec)
1364
+ spec.loader.exec_module(mod)
1365
+ def boom(path):
1366
+ raise ValueError("CANARY-7f3a")
1367
+ mod.scan = boom
1368
+ sys.argv = ["redact-engine.py", "x"]
1369
+ real, sys.stderr = sys.stderr, None
1370
+ try:
1371
+ rc = mod.main()
1372
+ finally:
1373
+ sys.stderr = real
1374
+ print("RC=%s" % rc)
1375
+ PY
1376
+ assert_grep "Test 25b: a crashing engine with stderr closed still exits 2" "^RC=2$" "${t25b_out}"
1377
+
1378
+ # Test 26 — _b64_decode contract (slice 1 of #10331): a non-base64 candidate returns None (never raises), a valid one decodes,
1379
+ # the url-safe alphabet is tried second. Guards the sentinel form that replaced the per-candidate `continue`.
1380
+ t26_out="${TMP_DIR}/test26.out"
1381
+ python3 -I - "${SKILL_DIR}/scripts/redact-engine.py" >"${t26_out}" 2>&1 <<'PY'
1382
+ import importlib.util, sys
1383
+ spec = importlib.util.spec_from_file_location("redact_engine_under_test", sys.argv[1])
1384
+ mod = importlib.util.module_from_spec(spec)
1385
+ spec.loader.exec_module(mod)
1386
+ print("NONB64=%r" % (mod._b64_decode("not base64 !!"),))
1387
+ print("STD=%r" % (mod._b64_decode("aGVsbG8="),))
1388
+ print("URL=%r" % (mod._b64_decode("--8_") == bytes([0xFB, 0xEF, 0x3F]),))
1389
+ PY
1390
+ assert_grep "Test 26: a non-base64 candidate returns None" "^NONB64=None$" "${t26_out}"
1391
+ assert_grep "Test 26: a standard-alphabet candidate decodes" "^STD=b'hello'$" "${t26_out}"
1392
+ assert_grep "Test 26: a url-safe candidate falls through to the second alphabet" "^URL=True$" "${t26_out}"
1393
+
1394
+ echo "Total: ${PASS} pass, ${FAIL} fail"
1395
+
1396
+ # ---------------------------------------------------------------------------
1397
+ # ANTI-VACUITY FLOOR ON THIS HARNESS'S OWN DISPATCH.
1398
+ #
1399
+ # `[[ "${FAIL}" -eq 0 ]]` alone is satisfied by a run that asserted NOTHING: delete every
1400
+ # assertion between the setup block and here and the file exits 0 at `Total: 0 pass, 0 fail`,
1401
+ # which `scripts/test-all.sh` reads as a passing suite. Measured — that is exactly what
1402
+ # happened, in the file whose whole purpose is proving OTHER files are not deletable at green.
1403
+ # Test 20 gives Guard 1 a cross-file floor and nothing reciprocated for Guard 2.
1404
+ #
1405
+ # This is not an invented convention: `scripts/lint-guard-contract.test.sh` already ships the
1406
+ # identical `EXPECTED_MIN` floor, and it was not reused here.
1407
+ #
1408
+ # A FLOOR, deliberately, not `-eq`: the count is developer-incremented, so equality turns every
1409
+ # added assertion into a spurious failure and the natural fix is to stop adding them. Derive the
1410
+ # value from a green run and ratchet it UP when assertions are added — never down to make a
1411
+ # failure go away, which is how a floor becomes the thing it was built to prevent.
1412
+ # ---------------------------------------------------------------------------
1413
+ EXPECTED_MIN=103
1414
+ TOTAL_DISPATCHED=$(( PASS + FAIL ))
1415
+ if [[ "${TOTAL_DISPATCHED}" -lt "${EXPECTED_MIN}" ]]; then
1416
+ echo "FAIL: harness dispatched only ${TOTAL_DISPATCHED} assertions (expected >= ${EXPECTED_MIN}) — a vacuous run. Assertions were removed or the dispatch was neutered; a suite that asserts nothing exits 0 and reads as PASS." >&2
1417
+ exit 1
1418
+ fi
1419
+
1420
+ [[ "${FAIL}" -eq 0 ]]