@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,1620 @@
1
+ #!/usr/bin/env bash
2
+ # Guard-contract suite for the Playwright MCP browser lifetime (feat-one-shot-playwright-mcp-stability).
3
+ #
4
+ # The defect: a plugin Stop hook SIGTERMed every Playwright Chrome on the host at the end of
5
+ # EVERY assistant turn (Stop fires per turn, not per session), and the project .mcp.json
6
+ # launch string pkill -9'd the proxy, the server and Chrome of every sibling session. The
7
+ # fix removes both killers and replaces the launch-time reaper with a kernel flock slot
8
+ # lease (scripts/playwright-mcp-profile-slot.sh). This suite pins the properties:
9
+ #
10
+ # Guard 1 -- no hook registered in plugins/soleur/hooks/hooks.json or .claude/settings.json and no
11
+ # MCP launch string names a browser with a PID-selecting tool (population derived by
12
+ # parsing the registries at test time, plus one hop of sourced scripts), and the
13
+ # plugin hook scripts, executed under a pgrep/pkill PATH shim, leave a live decoy alone.
14
+ # This is a text-and-shim property of THIS tree's registries: other plugins, older
15
+ # checkouts, the installed plugin copy and tools the shim does not cover are outside it.
16
+ # Project hooks (.claude/hooks) are scanned, never executed: running them is not safe;
17
+ # Guard 2 -- a launch claims a slot no live launch holds and kills nothing (the real
18
+ # launch string, executed under a scratch HOME with an npx shim);
19
+ # Guard 3 -- both registrations carry the ping-timeout setting (an INERT latent-hazard
20
+ # guard on stdio, not the fix) and the plugin registration carries
21
+ # --chromium-fallback.
22
+ #
23
+ # Verification hygiene (mirrors playwright-mcp-redact-proxy.test.sh): ok/bad helpers, an
24
+ # instrument self-test and a helper control that must REJECT (for every verdict-owning helper,
25
+ # including the inline verdicts of the mutant helpers), mutants asserted to have LANDED against a
26
+ # pristine copy, an anti-vacuity floor bound adjacent to the floor block and reported with printf +
27
+ # exit (never through the helpers it backstops).
28
+ #
29
+ # SAFETY. No row here terminates a process this suite did not start. Every decoy, holder
30
+ # and sleeper is recorded in $OWN when it is spawned and only those pids are ever signalled.
31
+ # The old hook text is executed ONLY under a scratch PATH whose pgrep/pidof print the decoy
32
+ # pid and nothing else, and whose pkill/killall signal the decoy and nothing else (ps, fuser and lsof
33
+ # print nothing). Every mutated pkill pattern embeds the unique scratch HOME, so it cannot select a
34
+ # real process. The slot script only ever runs with HOME pointed at a scratch directory.
35
+ # The suite's OWN fixtures must not match the retired hook's pattern (chrome.*--remote-debugging-pipe): a host that still
36
+ # has the old Stop hook installed kills any process of that shape within seconds, with no suite running, and a Guard 1
37
+ # decoy lost that way reads as a dead scanner. The shim selects by PID, so the Guard 1 decoys carry an argv the old pattern
38
+ # cannot match (start_g1_decoy refuses one that does); the Guard 2 decoys carry no --remote-debugging-pipe either.
39
+ # Known gap, by design: no decoy has the production Chrome shape (--remote-debugging-pipe), so a killer keyed on it is not
40
+ # modelled here; the static scan and the retired pattern itself are the cover for that shape.
41
+ set -Eeuo pipefail
42
+ trap 'printf "[ABORT] line %s: %s (rc=%s)\n" "$LINENO" "$BASH_COMMAND" "$?" >&2' ERR
43
+ export TMPDIR="${TMPDIR:-/var/tmp}"
44
+ export PLAYWRIGHT_MCP_PROXY_GRACE_S=1
45
+
46
+ # Refuses an empty, relative, root or synthetic-fs fixture dir (byte-identical copy; the
47
+ # fixture-dir-operand-assert suite pins every tracked copy).
48
+ assert_fixture_dir() {
49
+ case "${1-}" in
50
+ "") printf 'FATAL: fixture dir is EMPTY; git -C "" would operate on %s\n' "$PWD" >&2; exit 2 ;;
51
+ */../*|*/..) printf 'FATAL: fixture dir %s contains ..; refusing\n' "$1" >&2; exit 2 ;;
52
+ /proc/*|/sys/*|/dev/*) printf 'FATAL: fixture dir %s is a synthetic-fs path; refusing\n' "$1" >&2; exit 2 ;;
53
+ /|//|/.) printf 'FATAL: fixture dir resolves to the filesystem root; refusing\n' >&2; exit 2 ;;
54
+ /*) : ;;
55
+ *) printf 'FATAL: fixture dir %s is RELATIVE; refusing\n' "$1" >&2; exit 2 ;;
56
+ esac
57
+ }
58
+ REPO_ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)"
59
+ assert_fixture_dir "$REPO_ROOT"
60
+ SKILL_REL=plugins/soleur/skills/agent-browser
61
+ SLOT_REL="$SKILL_REL/scripts/playwright-mcp-profile-slot.sh"
62
+ PROXY_REL_PATH="$SKILL_REL/scripts/playwright-mcp-redact-proxy.py"
63
+ REDACTOR_REL="$SKILL_REL/scripts/redact-a11y-snapshot.py"
64
+ LOCKNAME=.pwslot.lock
65
+ WORK="$(mktemp -d -t lifetime-suite.XXXXXXXX)"
66
+ assert_fixture_dir "$WORK"
67
+ OWN="$WORK/own-pids"; : > "$OWN"
68
+ own() { printf '%s\n' "$1" >> "$OWN"; }
69
+ # Only pids this suite spawned are ever recorded in $OWN, and each is re-verified before the signal: a direct child
70
+ # of this shell, or (the npx shim's `sleep 300`) a process whose argv is exactly that and whose pid file lives in $WORK.
71
+ # reap_list <file of pids>: signal each recorded pid that is a direct child of this shell
72
+ reap_list() {
73
+ local p pp
74
+ while read -r p; do
75
+ [[ -n "$p" ]] || continue
76
+ pp="$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ' || true)"
77
+ if [[ "$pp" == "$$" ]]; then kill -KILL "$p" 2>/dev/null || true; fi
78
+ done < "$1"
79
+ }
80
+ reap_own() {
81
+ local p a
82
+ reap_list "$OWN"
83
+ while read -r p; do
84
+ [[ -n "$p" ]] || continue
85
+ a="$(ps -o args= -p "$p" 2>/dev/null || true)"
86
+ if [[ "$a" == "sleep 300" ]]; then kill -KILL "$p" 2>/dev/null || true; fi
87
+ done < <(cat "$WORK"/g2/*.jsonl.pid 2>/dev/null || true)
88
+ }
89
+ trap 'reap_own; rm -rf "$WORK"' EXIT
90
+
91
+ pass=0; fail=0; cases=0; mutants_declared=0; red_rows=0
92
+ ok() { printf 'ok - %s\n' "$1"; pass=$((pass + 1)); }
93
+ bad() { printf 'FAIL - %s\n' "$1"; fail=$((fail + 1)); }
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Instrument self-test (runs BEFORE any real row): both verdict helpers must
97
+ # move their counters or the suite refuses to continue.
98
+ # ---------------------------------------------------------------------------
99
+ _p0=$pass; _f0=$fail
100
+ ok "instrument self-test: ok() increments"
101
+ bad "instrument self-test: bad() increments (EXPECTED, not a real failure)"
102
+ if [[ $pass -ne $((_p0 + 1)) || $fail -ne $((_f0 + 1)) ]]; then
103
+ printf 'INSTRUMENT BROKEN: ok()/bad() did not both move\n' >&2; exit 1
104
+ fi
105
+ pass=0; fail=0; cases=0
106
+
107
+ # assert_true <label> <shell-test...> — one observable, one verdict
108
+ assert_true() {
109
+ local label="$1"; shift
110
+ cases=$((cases + 1))
111
+ if "$@"; then ok "$label"; else bad "$label"; fi
112
+ }
113
+ # red <label> <shell-test...> — a mutation row is ok when the DEFECT is observable
114
+ red() { local label="$1"; shift; cases=$((cases + 1)); red_rows=$((red_rows + 1)); if "$@"; then ok "$label"; else bad "$label — mutant survived"; fi; }
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # Process helpers (every pid they touch was spawned by this suite)
118
+ # ---------------------------------------------------------------------------
119
+ # alive <pid>: exists and is not a zombie
120
+ alive() { local s; s="$(ps -o stat= -p "${1:-0}" 2>/dev/null || true)"; [[ -n "$s" && "$s" != Z* ]]; }
121
+ wait_dead() { # <pid> <seconds>
122
+ local i=0
123
+ while alive "$1" && [[ $i -lt $(( $2 * 10 )) ]]; do sleep 0.1; i=$((i + 1)); done
124
+ if alive "$1"; then return 1; fi
125
+ return 0
126
+ }
127
+ # lock_held <lockfile>: a DIFFERENT open file description cannot take the lock (rc 75 = conflict)
128
+ lock_held() {
129
+ local rc=0
130
+ [[ -e "$1" ]] || return 1
131
+ flock -n -E 75 "$1" true || rc=$?
132
+ [[ $rc -eq 75 ]]
133
+ }
134
+ lock_free() { if [[ -e "$1" ]] && flock -n "$1" true; then return 0; fi; return 1; }
135
+ # landed <pristine> <mutated>: the mutation changed the tree
136
+ landed() { ! diff -rq "$1" "$2" >/dev/null 2>&1; }
137
+ now_ms() { printf '%s' "$(( ${EPOCHREALTIME/./} / 1000 ))"; }
138
+ # Spawned in the CURRENT shell (a $(...) subshell would orphan them, and reap_own only signals direct children); the pid
139
+ # lands in SPAWNED. A decoy's argv looks like the Chrome a pattern-reaper would select.
140
+ SPAWNED=""
141
+ start_decoy() { exec_a="$1"; ( exec -a "$exec_a" sleep 300 ) >/dev/null 2>&1 & SPAWNED=$!; own "$SPAWNED"; disown "$SPAWNED"; }
142
+ start_sleeper() { sleep 300 >/dev/null 2>&1 & SPAWNED=$!; own "$SPAWNED"; disown "$SPAWNED"; }
143
+ # the retired Stop hook's own selector (a PID-selecting pattern kill of every pipe-driven Chrome)
144
+ old_pattern_selects() { [[ "$1" =~ chrome.*--remote-debugging-pipe ]]; }
145
+ proc_argv() { tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null || true; }
146
+ # start_g1_decoy <tag>: a Guard 1 decoy whose argv the retired hook's pattern cannot match (the shim finds it by pid); sets SPAWNED
147
+ start_g1_decoy() {
148
+ local i a
149
+ start_decoy "sleep-decoy-$$-$1"
150
+ for i in $(seq 1 60); do a="$(proc_argv "$SPAWNED")"; if [[ "$a" == "sleep-decoy-$$-$1 "* ]]; then break; fi; sleep 0.05; done
151
+ if [[ "$a" != "sleep-decoy-$$-$1 "* ]] || old_pattern_selects "$a"; then printf 'HARNESS BROKEN: the Guard 1 decoy %s does not carry its own argv, or carries one the retired Stop hook would kill on its own (%s)\n' "$1" "$a" >&2; exit 1; fi
152
+ }
153
+ # a decoy that holds <file> open (fd 7) and carries <argv>: the shape a file-handle-selecting killer targets
154
+ start_holder() { local a="$1" f="$2"; ( exec 7> "$f"; exec -a "$a" sleep 300 ) >/dev/null 2>&1 & SPAWNED=$!; own "$SPAWNED"; disown "$SPAWNED"; }
155
+ # a process whose comm really is "chrome" (a symlink to sleep): the only way a pid probe reads a Chrome-like comm
156
+ CHROMEBIN="$WORK/chromebin"; mkdir -p "$CHROMEBIN"; ln -sf "$(command -v sleep)" "$CHROMEBIN/chrome"
157
+ start_comm_owner() { ln -sf "$(command -v sleep)" "$CHROMEBIN/$1"; "$CHROMEBIN/$1" 300 >/dev/null 2>&1 & SPAWNED=$!; own "$SPAWNED"; disown "$SPAWNED"; }
158
+ start_chrome_owner() { start_comm_owner chrome; }
159
+ # hold_lock <profile-dir> <seconds> [owner-pid]: a holder (own child, no-fork flock) on <dir>/.pwslot.lock; the owner file records [owner-pid]
160
+ hold_lock() {
161
+ local d="$1" secs="$2" i
162
+ assert_fixture_dir "$d"
163
+ mkdir -p "$d"; : > "$d/$LOCKNAME"
164
+ if [[ -n "${3:-}" ]]; then printf '%s\n' "$3" > "$d/.pwslot.owner"; fi
165
+ flock -F "$d/$LOCKNAME" sleep "$secs" >/dev/null 2>&1 & SPAWNED=$!; own "$SPAWNED"; disown "$SPAWNED"
166
+ for i in $(seq 1 50); do if lock_held "$d/$LOCKNAME"; then break; fi; sleep 0.1; done
167
+ }
168
+ HOST_NOW="$(uname -n)"
169
+ RN_PROFS=""; RN_RAN=0; RN_OVERLAP=0; RN_MS=0 # the run_n result variables (defined here so the helper control can drive the verdict helpers below)
170
+ # one launch ran and landed on <profile>
171
+ prof_is() { [[ "$RN_RAN" -eq 1 && "$(head -n1 <<< "$RN_PROFS")" == "$1" ]]; }
172
+ # <profile-dir>/SingletonLock is a symlink naming <target>
173
+ link_is() { [[ -L "$1/SingletonLock" && "$(readlink "$1/SingletonLock")" == "$2" ]]; }
174
+ no_link() { [[ ! -e "$1/SingletonLock" && ! -L "$1/SingletonLock" ]]; }
175
+ mode_is() { [[ "$(stat -c %a "$1" 2>/dev/null)" == "$2" ]]; }
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Guard 1 scanner (python, written once). Population is DERIVED by parsing the
179
+ # files at test time: every command of every event of plugins/soleur/hooks/hooks.json and of
180
+ # .claude/settings.json, the script file each one invokes, one hop of scripts those scripts
181
+ # source or run, the args of .mcp.json and plugins/soleur/.mcp.json, and every script those launch
182
+ # strings source.
183
+ # rc 0 clean | 1 a browser/MCP kill by name or pattern was found | 3 population below a floor
184
+ # Modes: scan (default) | list-exec (the scripts the dynamic rows execute under the shim)
185
+ # ---------------------------------------------------------------------------
186
+ HELP="$WORK/helpers"; mkdir -p "$HELP"
187
+ cat > "$HELP/g1_scan.py" <<'PY'
188
+ import json, os, re, sys
189
+
190
+ root = sys.argv[1]
191
+ floor_vals = [int(x) for x in sys.argv[2:6]]
192
+ mode = sys.argv[6] if len(sys.argv) > 6 else "scan"
193
+ FLOOR_NAMES = [("commands", "G1_FLOOR_CMDS"), ("scripts", "G1_FLOOR_SCRIPTS"), ("mcp", "G1_FLOOR_MCP"), ("sourced", "G1_FLOOR_SOURCED")]
194
+ BROWSER = re.compile(r"chrom|playwright|mcp|remote-debugging|user-data-dir|headless_shell", re.I)
195
+ SELECTORS = re.compile(r"\b(pkill|killall|pgrep|pidof)\b")
196
+ KILLISH = re.compile(r"(?<![\w-])kill(?![\w-])")
197
+ SCRIPT_TOKEN = re.compile(r"[^\s\"']+\.(?:sh|py|js|mjs|cjs|ts)\b")
198
+ HOP_TOKEN = re.compile(r"(?:^|[;&|(\s])(?:\.|source|bash|sh|exec)\s+[\"']?([^\s;&|\"'<>)]+\.(?:sh|py))")
199
+
200
+
201
+ def reasons_for(text):
202
+ """(tool, segment) pairs where a process is selected by a browser/MCP name or pattern and killed."""
203
+ found = []
204
+ text = re.sub(r"\\\n[ \t]*", " ", text) # a backslash-continued command is one command
205
+ has_kill = bool(KILLISH.search(text))
206
+ for seg in re.split(r"[;\n]|&&|\|\||\|", text):
207
+ norm = re.sub(r"\[(\w)\]", r"\1", seg) # the [c]hrome self-match trick hides the name
208
+ if not BROWSER.search(norm):
209
+ continue
210
+ m = SELECTORS.search(norm)
211
+ if m and (m.group(1) in ("pkill", "killall") or has_kill):
212
+ found.append((m.group(1), norm.strip()[:90]))
213
+ elif re.search(r"\bgrep\b", norm) and has_kill:
214
+ found.append(("grep", norm.strip()[:90]))
215
+ return found
216
+
217
+
218
+ def load_json(path):
219
+ try:
220
+ return json.load(open(path))
221
+ except Exception:
222
+ return {}
223
+
224
+
225
+ def code_of(path):
226
+ return "\n".join(l for l in open(path, errors="replace").read().splitlines() if not l.lstrip().startswith("#"))
227
+
228
+
229
+ plugin_root = os.path.join(root, "plugins/soleur")
230
+ VARS = [
231
+ ('"$CLAUDE_PROJECT_DIR"', root), ('"${CLAUDE_PROJECT_DIR}"', root), ("${CLAUDE_PROJECT_DIR}", root), ("$CLAUDE_PROJECT_DIR", root),
232
+ ('"${CLAUDE_PLUGIN_ROOT}"', plugin_root), ("${CLAUDE_PLUGIN_ROOT}", plugin_root), ("$CLAUDE_PLUGIN_ROOT", plugin_root),
233
+ ]
234
+
235
+
236
+ def expand(s):
237
+ for k, v in VARS:
238
+ s = s.replace(k, v)
239
+ return s
240
+
241
+
242
+ def commands_of(path):
243
+ out = []
244
+ for event, groups in (load_json(path).get("hooks") or {}).items():
245
+ for g in groups or []:
246
+ for h in g.get("hooks", []):
247
+ if h.get("type") == "command" and isinstance(h.get("command"), str):
248
+ out.append((event, h["command"]))
249
+ return out
250
+
251
+
252
+ def scripts_of(commands):
253
+ found = {}
254
+ for _event, cmd in commands:
255
+ for tok in SCRIPT_TOKEN.findall(expand(cmd)):
256
+ p = tok if os.path.isabs(tok) else os.path.join(root, tok)
257
+ if os.path.isfile(p):
258
+ found[os.path.realpath(p)] = p
259
+ return found
260
+
261
+
262
+ def resolve_hop(tok, script):
263
+ sdir = os.path.dirname(script)
264
+ names = {"SCRIPT_DIR": sdir, "HOOK_DIR": sdir, "HOOKS_DIR": sdir, "HERE": sdir, "DIR": sdir,
265
+ "PLUGIN_ROOT": plugin_root, "CLAUDE_PLUGIN_ROOT": plugin_root,
266
+ "CLAUDE_PROJECT_DIR": root, "REPO_ROOT": root, "PROJECT_ROOT": root, "GIT_ROOT": root}
267
+ t = re.sub(r"\$\(dirname[^)]*\)", sdir, tok)
268
+ t = re.sub(r"\$\{?(\w+)\}?", lambda m: names.get(m.group(1), m.group(0)), t)
269
+ if "$" in t:
270
+ tail = re.split(r"\$\{?\w+\}?", t)[-1].lstrip("/")
271
+ cands = [os.path.join(b, tail) for b in (sdir, os.path.join(sdir, ".."), plugin_root, root)]
272
+ else:
273
+ cands = [t if os.path.isabs(t) else os.path.join(sdir, t), t if os.path.isabs(t) else os.path.join(root, t)]
274
+ for c in cands:
275
+ c = os.path.normpath(c)
276
+ if os.path.isfile(c):
277
+ return c
278
+ return None
279
+
280
+
281
+ def hops_of(files):
282
+ found = {}
283
+ for p in files:
284
+ for tok in HOP_TOKEN.findall(code_of(p)):
285
+ r = resolve_hop(tok, p)
286
+ if r:
287
+ found[os.path.realpath(r)] = r
288
+ return found
289
+
290
+
291
+ commands = commands_of(os.path.join(plugin_root, "hooks/hooks.json"))
292
+ settings_commands = commands_of(os.path.join(root, ".claude/settings.json"))
293
+ scripts = scripts_of(commands)
294
+ settings_scripts = scripts_of(settings_commands)
295
+ units = [] # (label, text)
296
+ for event, cmd in commands:
297
+ units.append((f"hooks.json {event} command", cmd))
298
+ for event, cmd in settings_commands:
299
+ units.append((f"settings.json {event} command", cmd))
300
+ mcp_strings = 0
301
+ sourced = {}
302
+ for rel in (".mcp.json", "plugins/soleur/.mcp.json"):
303
+ for name, srv in (load_json(os.path.join(root, rel)).get("mcpServers") or {}).items():
304
+ text = " ".join([str(srv.get("command", ""))] + [str(a) for a in srv.get("args", [])])
305
+ mcp_strings += 1
306
+ units.append((f"{rel}:{name}", text))
307
+ for tok in re.findall(r"(?:^|[;&|(\s])(?:\.|source)\s+([^\s;&|\"'<>]+\.sh)\b", text):
308
+ p = tok.replace("${CLAUDE_PLUGIN_ROOT}", plugin_root)
309
+ p = p if os.path.isabs(p) else os.path.join(root, p)
310
+ if os.path.isfile(p):
311
+ sourced[os.path.realpath(p)] = p
312
+ plugin_side = {**scripts, **sourced}
313
+ hops_plugin = {k: v for k, v in hops_of(plugin_side.values()).items() if k not in plugin_side}
314
+ hops_settings = {k: v for k, v in hops_of(settings_scripts.values()).items() if k not in settings_scripts and k not in plugin_side}
315
+ everything = {**plugin_side, **settings_scripts, **hops_plugin, **hops_settings}
316
+ for real, p in everything.items():
317
+ units.append((os.path.relpath(p, root), code_of(p)))
318
+
319
+ flagged = []
320
+ for label, text in units:
321
+ for tool, seg in reasons_for(text):
322
+ flagged.append((label, tool, seg))
323
+
324
+ pop = {"commands": len(commands), "scripts": len(scripts), "mcp": mcp_strings, "sourced": len(sourced)}
325
+ print(f"POP commands={len(commands)} scripts={len(scripts)} mcp={mcp_strings} sourced={len(sourced)} "
326
+ f"settings={len(settings_commands)} settings_scripts={len(settings_scripts)} hops={len(hops_plugin) + len(hops_settings)}")
327
+ if mode == "list-exec":
328
+ # Plugin hook scripts and what they pull in, plus the scripts the launch strings source. Project hooks are NOT executed.
329
+ for p in scripts.values():
330
+ print(("RUN\t" if p.endswith((".sh", ".py")) else "STATIC\t") + p)
331
+ for p in list(sourced.values()) + list(hops_plugin.values()):
332
+ print(("SRC\t" if p.endswith(".sh") else "STATIC\t") + p)
333
+ sys.exit(0)
334
+ for label, tool, seg in flagged:
335
+ print(f"FLAG {label} [{tool}] {seg}")
336
+ low = [f"{key}={pop[key]} < {name}={floor}" for (key, name), floor in zip(FLOOR_NAMES, floor_vals) if pop[key] < floor]
337
+ if low:
338
+ print("VACUOUS population below the floor: " + ", ".join(low)
339
+ + "; lower the named constant in playwright-mcp-lifetime.test.sh only when hooks or registrations were legitimately removed")
340
+ sys.exit(3)
341
+ sys.exit(1 if flagged else 0)
342
+ PY
343
+ # g1 <root> [floor-cmds] [floor-scripts] [floor-mcp] [floor-sourced] -> sets G1_RC and G1_OUT
344
+ # The two hook floors are SLACK on purpose (proof the scan ran, not a pin of someone else's registry); the planted-fixture
345
+ # rows below are the real positive control. When one fires the message names the constant to lower.
346
+ G1_FLOOR_CMDS=6
347
+ G1_FLOOR_SCRIPTS=5
348
+ G1_FLOOR_MCP=2 # .mcp.json plus plugins/soleur/.mcp.json
349
+ G1_FLOOR_SOURCED=1 # the slot script, sourced by the project launch string
350
+ G1_FLOOR_RUN=5 # population scripts the dynamic rows must have executed under the shim
351
+ G1_RC=0; G1_OUT=""
352
+ g1() {
353
+ G1_RC=0
354
+ G1_OUT="$(python3 "$HELP/g1_scan.py" "$1" "${2:-$G1_FLOOR_CMDS}" "${3:-$G1_FLOOR_SCRIPTS}" "${4:-$G1_FLOOR_MCP}" "${5:-$G1_FLOOR_SOURCED}" 2>&1)" || G1_RC=$?
355
+ }
356
+ g1_rc_is() { [[ "$G1_RC" == "$1" ]]; }
357
+
358
+ # mkroot1 <dir>: the Guard 1 surface of a tree (both hook registries and their scripts, both registrations, the slot script) under the same relative paths
359
+ mkroot1() {
360
+ local d="$1"
361
+ assert_fixture_dir "$d"
362
+ mkdir -p "$d/plugins/soleur" "$d/$SKILL_REL/scripts" "$d/.claude" "$d/scripts"
363
+ cp -R "$REPO_ROOT/plugins/soleur/hooks" "$d/plugins/soleur/"
364
+ cp -R "$REPO_ROOT/plugins/soleur/scripts" "$d/plugins/soleur/"
365
+ cp -R "$REPO_ROOT/.claude/hooks" "$d/.claude/"
366
+ cp "$REPO_ROOT/.claude/settings.json" "$d/.claude/settings.json"
367
+ cp "$REPO_ROOT/scripts/ensure-kb-index.sh" "$d/scripts/ensure-kb-index.sh"
368
+ cp "$REPO_ROOT/.mcp.json" "$d/.mcp.json"
369
+ cp "$REPO_ROOT/plugins/soleur/.mcp.json" "$d/plugins/soleur/.mcp.json"
370
+ if [[ -f "$REPO_ROOT/$SLOT_REL" ]]; then cp "$REPO_ROOT/$SLOT_REL" "$d/$SLOT_REL"; fi
371
+ }
372
+
373
+ # A synthesized stand-in for the removed hook (cq-test-fixtures-synthesized-only): pattern-select every
374
+ # remote-debugging Chrome on the host and signal each, with no regard to who launched it.
375
+ OLD_HOOK="$HELP/old-stop-hook.sh"
376
+ cat > "$OLD_HOOK" <<'HOOK'
377
+ #!/usr/bin/env bash
378
+ set -euo pipefail
379
+ cat > /dev/null
380
+ PIDS=$(pgrep -f 'chrome.*--remote-debugging-pipe' 2>/dev/null || true)
381
+ if [[ -n "$PIDS" ]]; then
382
+ for pid in $PIDS; do kill "$pid" 2>/dev/null || true; done
383
+ fi
384
+ exit 0
385
+ HOOK
386
+ chmod +x "$OLD_HOOK"
387
+
388
+ # Python helper that edits a Guard 1 scratch root. <root> <mutation> <old-hook>
389
+ # Mutations that plant a killer body: vocab-<word> | killall-chrome | pidof-chrome | ps-grep-kill | continuation | indirection | split-word
390
+ # Other mutations: sourced-split-word (a split-word killer inside the SOURCED slot script, before its final return) | settings-hop-killer
391
+ cat > "$HELP/g1_mut.py" <<'PY'
392
+ import json, os, shutil, sys
393
+
394
+ root, mutation, old_hook = sys.argv[1:4]
395
+ if mutation == "noop":
396
+ sys.exit(0)
397
+ hj = os.path.join(root, "plugins/soleur/hooks/hooks.json")
398
+ d = json.load(open(hj))
399
+ KILLER = '#!/usr/bin/env bash\nset -euo pipefail\npkill -9 -f "chrome.*--remote-debugging-pipe" 2>/dev/null || true\nexit 0\n'
400
+ BODIES = {
401
+ "killall-chrome": 'killall -9 chrome 2>/dev/null || true',
402
+ "pidof-chrome": 'kill $(pidof chrome) 2>/dev/null || true',
403
+ "ps-grep-kill": "ps aux | grep chrome | awk '{print $2}' | xargs kill 2>/dev/null || true",
404
+ "continuation": 'pkill -9 -f \\\n "chrome.*--remote-debugging-pipe" 2>/dev/null || true',
405
+ "indirection": 'PAT="chrome.*--remote-debugging-pipe"\npkill -9 -f "$PAT" 2>/dev/null || true',
406
+ "split-word": 'pkill -9 -f "chr""ome" 2>/dev/null || true',
407
+ }
408
+
409
+
410
+ def add_group(event, command):
411
+ d["hooks"].setdefault(event, []).append({"hooks": [{"type": "command", "command": command}]})
412
+
413
+
414
+ def write(path, text, mode=0o755):
415
+ os.makedirs(os.path.dirname(path), exist_ok=True)
416
+ open(path, "w").write(text)
417
+ os.chmod(path, mode)
418
+
419
+
420
+ def plant(body):
421
+ write(os.path.join(root, "plugins/soleur/hooks/zz-killer.sh"), "#!/usr/bin/env bash\n" + body + "\nexit 0\n")
422
+ add_group("Stop", "${CLAUDE_PLUGIN_ROOT}/hooks/zz-killer.sh")
423
+
424
+
425
+ if mutation == "restore-old-hook":
426
+ shutil.copy(old_hook, os.path.join(root, "plugins/soleur/hooks/browser-cleanup-hook.sh"))
427
+ cmds = [h["command"] for g in d["hooks"].get("Stop", []) for h in g["hooks"]]
428
+ if not any("browser-cleanup-hook.sh" in c for c in cmds):
429
+ add_group("Stop", "${CLAUDE_PLUGIN_ROOT}/hooks/browser-cleanup-hook.sh")
430
+ elif mutation == "killer-under-sessionstart":
431
+ open(os.path.join(root, "plugins/soleur/hooks/zz-killer.sh"), "w").write(KILLER)
432
+ add_group("SessionStart", "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/zz-killer.sh\"")
433
+ elif mutation == "second-killer-same-event":
434
+ open(os.path.join(root, "plugins/soleur/hooks/zz-killer.sh"), "w").write(KILLER)
435
+ add_group("Stop", "${CLAUDE_PLUGIN_ROOT}/hooks/zz-killer.sh")
436
+ elif mutation == "second-in-group":
437
+ open(os.path.join(root, "plugins/soleur/hooks/zz-killer.sh"), "w").write(KILLER)
438
+ d["hooks"]["Stop"][0]["hooks"].append({"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/zz-killer.sh"})
439
+ elif mutation == "empty-commands":
440
+ d["hooks"] = {}
441
+ elif mutation == "mcp-pkill":
442
+ p = os.path.join(root, ".mcp.json")
443
+ m = json.load(open(p))
444
+ a = m["mcpServers"]["playwright"]["args"]
445
+ a[1] = 'pkill -9 -f "[c]hrome.*$prof" 2>/dev/null || true; ' + a[1]
446
+ json.dump(m, open(p, "w"))
447
+ elif mutation == "plugin-mcp-pkill":
448
+ p = os.path.join(root, "plugins/soleur/.mcp.json")
449
+ m = json.load(open(p))
450
+ m["mcpServers"]["reaper"] = {"command": "bash", "args": ["-c", "pkill -9 -f chrome; exec true"]}
451
+ json.dump(m, open(p, "w"))
452
+ elif mutation == "drop-plugin-mcp":
453
+ p = os.path.join(root, "plugins/soleur/.mcp.json")
454
+ json.dump({"mcpServers": {}}, open(p, "w"))
455
+ elif mutation == "drop-sourced":
456
+ p = os.path.join(root, ".mcp.json")
457
+ m = json.load(open(p))
458
+ a = m["mcpServers"]["playwright"]["args"]
459
+ a[1] = a[1].replace(". plugins/soleur/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh || exit 1;", "prof=$HOME/.cache/playwright-mcp-profile;")
460
+ json.dump(m, open(p, "w"))
461
+ elif mutation == "sourced-killer":
462
+ p = os.path.join(root, "plugins/soleur/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh")
463
+ open(p, "a").write('\npkill -9 -f "chrome.*$prof" 2>/dev/null || true\n')
464
+ elif mutation == "settings-killer":
465
+ write(os.path.join(root, ".claude/hooks/zz-killer.sh"), KILLER)
466
+ sp = os.path.join(root, ".claude/settings.json")
467
+ s = json.load(open(sp))
468
+ s["hooks"].setdefault("PreToolUse", []).append({"matcher": "Bash", "hooks": [{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/zz-killer.sh"}]})
469
+ json.dump(s, open(sp, "w"))
470
+ elif mutation == "sourced-split-word":
471
+ p = os.path.join(root, "plugins/soleur/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh")
472
+ src = open(p).read()
473
+ i = src.rindex("\nreturn 0")
474
+ open(p, "w").write(src[:i] + '\npkill -9 -f "chr""ome" 2>/dev/null || true' + src[i:])
475
+ elif mutation == "settings-hop-killer":
476
+ write(os.path.join(root, ".claude/hooks/lib/zz-helper.sh"), KILLER, 0o644)
477
+ write(os.path.join(root, ".claude/hooks/zz-hook.sh"),
478
+ '#!/usr/bin/env bash\nSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\nsource "$SCRIPT_DIR/lib/zz-helper.sh"\nexit 0\n')
479
+ sp = os.path.join(root, ".claude/settings.json")
480
+ s = json.load(open(sp))
481
+ s["hooks"].setdefault("PreToolUse", []).append({"matcher": "Bash", "hooks": [{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/zz-hook.sh"}]})
482
+ json.dump(s, open(sp, "w"))
483
+ elif mutation == "hop-killer":
484
+ write(os.path.join(root, "plugins/soleur/hooks/lib/zz-helper.sh"), KILLER, 0o644)
485
+ write(os.path.join(root, "plugins/soleur/hooks/zz-hook.sh"),
486
+ '#!/usr/bin/env bash\nSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\nsource "$SCRIPT_DIR/lib/zz-helper.sh"\nexit 0\n')
487
+ add_group("Stop", "${CLAUDE_PLUGIN_ROOT}/hooks/zz-hook.sh")
488
+ elif mutation.startswith("vocab-"):
489
+ plant("pkill -9 -f " + mutation[len("vocab-"):] + " 2>/dev/null || true")
490
+ elif mutation in BODIES:
491
+ plant(BODIES[mutation])
492
+ elif mutation == "kill-own-child-only":
493
+ open(os.path.join(root, "plugins/soleur/hooks/zz-own.sh"), "w").write('#!/usr/bin/env bash\nsleep 1 &\nkill "$!"\n')
494
+ add_group("Stop", "${CLAUDE_PLUGIN_ROOT}/hooks/zz-own.sh")
495
+ elif mutation == "pkill-non-browser":
496
+ open(os.path.join(root, "plugins/soleur/hooks/zz-other.sh"), "w").write('#!/usr/bin/env bash\npkill -f my-unrelated-daemon 2>/dev/null || true\n')
497
+ add_group("Stop", "${CLAUDE_PLUGIN_ROOT}/hooks/zz-other.sh")
498
+ else:
499
+ sys.exit(9)
500
+ json.dump(d, open(hj, "w"))
501
+ PY
502
+
503
+ # ---------------------------------------------------------------------------
504
+ # Guard 1 dynamic harness: a scratch PATH whose pgrep/pidof print ONLY the decoy pid and whose pkill/killall signal ONLY the
505
+ # decoy; ps, fuser and lsof print nothing (so a ps|grep|kill pipeline selects nothing real).
506
+ # ---------------------------------------------------------------------------
507
+ SHIM="$WORK/shim"; mkdir -p "$SHIM"
508
+ cat > "$SHIM/pgrep" <<'SHIMSRC'
509
+ #!/usr/bin/env bash
510
+ # prints ONLY the decoy pid (or nothing when decoy.pid is empty)
511
+ d="$(cat "$(dirname "$0")/decoy.pid")"
512
+ if [[ -n "$d" ]]; then printf '%s\n' "$d"; fi
513
+ exit 0
514
+ SHIMSRC
515
+ cp "$SHIM/pgrep" "$SHIM/pidof"
516
+ cat > "$SHIM/pkill" <<'SHIMSRC'
517
+ #!/usr/bin/env bash
518
+ # signals ONLY the decoy, whatever pattern it was handed
519
+ d="$(cat "$(dirname "$0")/decoy.pid")"
520
+ if [[ -n "$d" ]]; then kill "$d" 2>/dev/null || true; fi
521
+ exit 0
522
+ SHIMSRC
523
+ cp "$SHIM/pkill" "$SHIM/killall"
524
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$SHIM/ps"
525
+ cp "$SHIM/ps" "$SHIM/fuser"; cp "$SHIM/ps" "$SHIM/lsof"
526
+ chmod +x "$SHIM"/pgrep "$SHIM"/pidof "$SHIM"/pkill "$SHIM"/killall "$SHIM"/ps "$SHIM"/fuser "$SHIM"/lsof
527
+ DYN="$WORK/dyn"; mkdir -p "$DYN/home" "$DYN/proj" "$DYN/tmp"
528
+ # run_under_shim <script> <pid-the-shim-targets|""> <pid-to-watch> -> 0 when the watched pid is still alive afterwards
529
+ run_under_shim() {
530
+ local script="$1" target="$2" watch="$3"
531
+ printf '%s' "$target" > "$SHIM/decoy.pid"
532
+ ( cd "$WORK" && PATH="$SHIM:$PATH" CLAUDE_PLUGIN_ROOT="$REPO_ROOT/plugins/soleur" bash "$script" </dev/null >/dev/null 2>&1 ) || true
533
+ wait_dead "$watch" 1 || true
534
+ alive "$watch"
535
+ }
536
+ # dyn_run <RUN|SRC> <path> <plugin-root>: one population script, executed (RUN) or sourced (SRC) under the shim, with a scratch
537
+ # HOME, cwd and project dir, a scrubbed environment, no stdin and a time bound
538
+ dyn_run() {
539
+ local kind="$1" path="$2" pr="$3"
540
+ local -a runner
541
+ case "$kind:$path" in
542
+ RUN:*.py) runner=(python3 "$path") ;;
543
+ RUN:*) runner=(bash "$path") ;;
544
+ *) runner=(bash -c '. "$1"' _ "$path") ;;
545
+ esac
546
+ ( cd "$DYN" && exec env -i PATH="$SHIM:/usr/bin:/bin" HOME="$DYN/home" TMPDIR="$DYN/tmp" CLAUDE_PLUGIN_ROOT="$pr" CLAUDE_PROJECT_DIR="$DYN/proj" timeout 20 "${runner[@]}" </dev/null >/dev/null 2>&1 ) || true
547
+ }
548
+ # g1_dynamic <root> <decoy-pid>: every executable population script of <root> run under the shim; sets DYN_RAN and DYN_ALIVE
549
+ DYN_RAN=0; DYN_ALIVE=1
550
+ g1_dynamic() {
551
+ local root="$1" decoy="$2" kind path
552
+ DYN_RAN=0
553
+ printf '%s' "$decoy" > "$SHIM/decoy.pid"
554
+ while IFS=$'\t' read -r kind path; do
555
+ case "$kind" in RUN|SRC) : ;; *) continue ;; esac
556
+ dyn_run "$kind" "$path" "$root/plugins/soleur"
557
+ DYN_RAN=$((DYN_RAN + 1))
558
+ done < <(python3 "$HELP/g1_scan.py" "$root" 0 0 0 0 list-exec)
559
+ wait_dead "$decoy" 1 || true
560
+ if alive "$decoy"; then DYN_ALIVE=1; else DYN_ALIVE=0; fi
561
+ }
562
+
563
+ # ---------------------------------------------------------------------------
564
+ # Verdict helpers of the mutation matrices (defined before the helper control so the control can drive them)
565
+ # ---------------------------------------------------------------------------
566
+ G1="$WORK/g1"; mkdir -p "$G1"
567
+ G2="$WORK/g2"; mkdir -p "$G2/bin"
568
+ # g1_mutant <name> <mutation> <expected-rc> <label> [<flag-substring>]: static verdict; the FLAG line must name the planted unit
569
+ g1_mutant() {
570
+ local name="$1" mutation="$2" want="$3" label="$4" flag="${5:-}"
571
+ cases=$((cases + 1)); mutants_declared=$((mutants_declared + 1))
572
+ mkroot1 "$G1/m-$name"
573
+ python3 "$HELP/g1_mut.py" "$G1/m-$name" "$mutation" "$OLD_HOOK"
574
+ if ! landed "$G1/real" "$G1/m-$name"; then bad "Guard 1 mutant $name did NOT land"; return 0; fi
575
+ ok "Guard 1 mutant $name landed"
576
+ g1 "$G1/m-$name"
577
+ cases=$((cases + 1)); red_rows=$((red_rows + 1))
578
+ # rc 1 rows name the planted unit on a FLAG line; rc 3 rows name the floor constant that fired on the VACUOUS line
579
+ if [[ "$G1_RC" == "$want" && ( -z "$flag" || "$G1_OUT" == *"FLAG"*"$flag"* || ( "$want" == 3 && "$G1_OUT" == *"VACUOUS"*"$flag"* ) ) ]]; then ok "$label"; else bad "$label — mutant survived (scanner rc $G1_RC, expected $want${flag:+, naming $flag})"; fi
580
+ }
581
+ # g1_dyn_mutant <name> <mutation> <label>: dynamic verdict — the planted tree's scripts, executed under the shim, kill the decoy
582
+ g1_dyn_mutant() {
583
+ local name="$1" mutation="$2" label="$3" d
584
+ cases=$((cases + 1)); mutants_declared=$((mutants_declared + 1))
585
+ mkroot1 "$G1/m-$name"
586
+ python3 "$HELP/g1_mut.py" "$G1/m-$name" "$mutation" "$OLD_HOOK"
587
+ if ! landed "$G1/real" "$G1/m-$name"; then bad "Guard 1 mutant $name did NOT land"; return 0; fi
588
+ ok "Guard 1 mutant $name landed"
589
+ start_g1_decoy "$name"; d=$SPAWNED
590
+ g1_dynamic "$G1/m-$name" "$d"
591
+ cases=$((cases + 1)); red_rows=$((red_rows + 1))
592
+ if [[ "$DYN_ALIVE" -eq 0 && "$DYN_RAN" -ge "$G1_FLOOR_RUN" ]]; then ok "$label"; else bad "$label — mutant survived (decoy alive=$DYN_ALIVE after $DYN_RAN scripts)"; fi
593
+ }
594
+ mkroot() { # <dir>: a scratch tree holding the launch string's file dependencies
595
+ local d="$1"
596
+ assert_fixture_dir "$d"
597
+ mkdir -p "$d/$SKILL_REL/scripts" "$d/.claude"
598
+ cp "$REPO_ROOT/$PROXY_REL_PATH" "$d/$SKILL_REL/scripts/"
599
+ cp "$REPO_ROOT/$REDACTOR_REL" "$d/$SKILL_REL/scripts/"
600
+ cp "$REPO_ROOT/.claude/playwright-mcp.config.json" "$d/.claude/"
601
+ if [[ -f "$REPO_ROOT/$SLOT_REL" ]]; then cp "$REPO_ROOT/$SLOT_REL" "$d/$SLOT_REL"; fi
602
+ }
603
+ # slot_mutant <name> <old> <new> [<old2> <new2>] — an edited COPY of the slot script in a scratch tree; every edit must
604
+ # occur exactly once in the pristine script and the copy must differ (landed). Sets MROOT (empty when it did not land).
605
+ MROOT=""
606
+ slot_mutant() {
607
+ local name="$1"; shift
608
+ cases=$((cases + 1)); mutants_declared=$((mutants_declared + 1)); MROOT=""
609
+ local root="$G2/mut-$name"
610
+ mkroot "$root"
611
+ if python3 - "$REPO_ROOT/$SLOT_REL" "$root/$SLOT_REL" "$@" <<'PY'
612
+ import sys
613
+ src_path, out_path, *edits = sys.argv[1:]
614
+ try:
615
+ src = open(src_path).read()
616
+ except OSError:
617
+ sys.exit(2)
618
+ out = src
619
+ for old, new in zip(edits[0::2], edits[1::2]):
620
+ if out.count(old) != 1:
621
+ sys.stderr.write(f"edit target occurs {out.count(old)} times: {old!r}\n"); sys.exit(3)
622
+ out = out.replace(old, new)
623
+ if out == src:
624
+ sys.exit(4)
625
+ open(out_path, "w").write(out)
626
+ PY
627
+ then ok "Guard 2 mutant $name landed"; MROOT="$root"; else bad "Guard 2 mutant $name did NOT land"; fi
628
+ }
629
+ PLUGIN_MCP="$REPO_ROOT/plugins/soleur/.mcp.json"
630
+ PROJECT_MCP="$REPO_ROOT/.mcp.json"
631
+ # reg_ok <file> <project|plugin>: the ping-timeout env is "0" (an inert stdio guard; see the ADR), and the plugin carries --chromium-fallback
632
+ reg_ok() {
633
+ python3 - "$1" "$2" <<'PY'
634
+ import json, sys
635
+ path, kind = sys.argv[1:]
636
+ e = json.load(open(path))["mcpServers"]["playwright"]
637
+ ok = e.get("env") == {"PLAYWRIGHT_MCP_PING_TIMEOUT_MS": "0"}
638
+ if kind == "plugin":
639
+ ok = ok and e["args"].count("--chromium-fallback") == 1 and e["args"].index("--chromium-fallback") < e["args"].index("--")
640
+ sys.exit(0 if ok else 1)
641
+ PY
642
+ }
643
+ reg_mutant() { # <label> <file> <kind> <python-mutation-src>
644
+ cases=$((cases + 1)); mutants_declared=$((mutants_declared + 1))
645
+ local f="$WORK/reg-mut.json"
646
+ if ! MUTATION="$4" python3 - "$2" "$f" 2>/dev/null <<'PY'
647
+ import json, os, sys
648
+ d = json.load(open(sys.argv[1]))
649
+ e = d["mcpServers"]["playwright"]
650
+ exec(os.environ["MUTATION"])
651
+ json.dump(d, open(sys.argv[2], "w"))
652
+ PY
653
+ then bad "$1 did NOT land (the edit target is absent)"; return 0; fi
654
+ if cmp -s "$2" "$f" || diff -q <(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1])), sort_keys=True))' "$2") <(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1])), sort_keys=True))' "$f") >/dev/null; then
655
+ bad "$1 did NOT land"; return 0
656
+ fi
657
+ ok "$1 landed"
658
+ cases=$((cases + 1)); red_rows=$((red_rows + 1))
659
+ if reg_ok "$f" "$3"; then bad "$1 — mutant survived"; else ok "$1 → RED"; fi
660
+ }
661
+
662
+ # ---------------------------------------------------------------------------
663
+ # Helper control: every verdict-owning helper driven with an input it MUST reject.
664
+ # Counters are unwound afterwards; the suite refuses to continue unless all rejected. The controls
665
+ # report with printf + exit, never through the helper under test.
666
+ # ---------------------------------------------------------------------------
667
+ hc_broken() { printf 'HELPER CONTROL BROKEN: %s\n' "$1" >&2; exit 1; }
668
+ _helper_control() {
669
+ local _p=$pass _f=$fail _c=$cases _r=$red_rows _m=$mutants_declared
670
+ local planted="$WORK/hc-planted"; mkroot1 "$planted"
671
+ python3 "$HELP/g1_mut.py" "$planted" killer-under-sessionstart "$OLD_HOOK"
672
+ assert_true 'helper control: assert_true must REJECT false (EXPECTED)' false
673
+ red 'helper control: red must REJECT an unobservable defect (EXPECTED)' false
674
+ g1 "$planted" 1 1 0 0
675
+ assert_true 'helper control: a planted killer must make the scanner rc 1, so rc 0 is REJECTED (EXPECTED)' g1_rc_is 0
676
+ g1 "$planted" 999 999 0 0
677
+ assert_true 'helper control: a population below the floor must be rc 3, so rc 1 is REJECTED (EXPECTED)' g1_rc_is 1
678
+ assert_true 'helper control: lock_held must be FALSE on a missing lock file (EXPECTED)' lock_held "$WORK/no-such.lock"
679
+ assert_true 'helper control: alive must be FALSE on a pid that does not exist (EXPECTED)' alive 2147483646
680
+ if [[ $fail -ne $((_f + 6)) ]]; then
681
+ hc_broken "expected 6 rejections, fail $_f->$fail"
682
+ fi
683
+ # lock_held / lock_free: an existing UNLOCKED file and a LOCKED one, both ways
684
+ local unlocked="$WORK/hc-unlocked.lock" locked="$WORK/hc-locked.lock" hp i rc
685
+ : > "$unlocked"; : > "$locked"
686
+ flock -F "$locked" sleep 30 >/dev/null 2>&1 & hp=$!; own "$hp"; disown "$hp"
687
+ for i in $(seq 1 50); do rc=0; flock -n -E 75 "$locked" true || rc=$?; if [[ $rc -eq 75 ]]; then break; fi; sleep 0.1; done
688
+ if lock_held "$unlocked"; then hc_broken "lock_held accepted an existing but UNLOCKED file"; fi
689
+ if ! lock_held "$locked"; then hc_broken "lock_held rejected a LOCKED file"; fi
690
+ if lock_free "$locked"; then hc_broken "lock_free accepted a LOCKED file"; fi
691
+ if ! lock_free "$unlocked"; then hc_broken "lock_free rejected an UNLOCKED file"; fi
692
+ kill -KILL "$hp" 2>/dev/null || true
693
+ # landed: identical trees are not a landing, differing trees are
694
+ mkdir -p "$WORK/hc-l1" "$WORK/hc-l2"; : > "$WORK/hc-l1/a"; : > "$WORK/hc-l2/a"
695
+ if landed "$WORK/hc-l1" "$WORK/hc-l2"; then hc_broken "landed accepted identical trees"; fi
696
+ : > "$WORK/hc-l2/b"
697
+ if ! landed "$WORK/hc-l1" "$WORK/hc-l2"; then hc_broken "landed rejected differing trees"; fi
698
+ # g1_mutant: a no-op mutation must be reported as not landed; a landed mutant the scanner does not flag must be reported as surviving
699
+ local f0=$fail p0=$pass r0=$red_rows
700
+ g1_mutant ctl-noop noop 1 'helper control: no-op' >/dev/null
701
+ if [[ $fail -ne $((f0 + 1)) || $red_rows -ne $r0 ]]; then hc_broken "g1_mutant accepted a no-op mutation"; fi
702
+ f0=$fail; p0=$pass
703
+ g1_mutant ctl-survive pkill-non-browser 1 'helper control: unflagged mutant' >/dev/null
704
+ if [[ $fail -ne $((f0 + 1)) || $pass -ne $((p0 + 1)) ]]; then hc_broken "g1_mutant passed a landed mutant the scanner does not flag"; fi
705
+ f0=$fail; p0=$pass
706
+ g1_mutant ctl-flag second-killer-same-event 1 'helper control: wrong unit named' 'no-such-unit' >/dev/null
707
+ if [[ $fail -ne $((f0 + 1)) ]]; then hc_broken "g1_mutant passed a mutant flagged for the wrong unit"; fi
708
+ f0=$fail; p0=$pass
709
+ g1_mutant ctl-floor-key empty-commands 3 'helper control: the wrong floor named' 'G1_FLOOR_MCP' >/dev/null
710
+ if [[ $fail -ne $((f0 + 1)) ]]; then hc_broken "g1_mutant passed a floor row that names a floor that did not fire"; fi
711
+ p0=$pass
712
+ g1_mutant ctl-floor-ok empty-commands 3 'helper control: the right floor named' 'G1_FLOOR_CMDS' >/dev/null
713
+ if [[ $pass -ne $((p0 + 2)) ]]; then hc_broken "g1_mutant rejected a floor row that names the floor that fired"; fi
714
+ # g1_dyn_mutant: a no-op is not a landing, and a landed tree whose scripts kill nothing under the shim is a surviving mutant
715
+ f0=$fail
716
+ g1_dyn_mutant ctl-dyn-noop noop 'helper control: dynamic no-op' >/dev/null
717
+ if [[ $fail -ne $((f0 + 1)) ]]; then hc_broken "g1_dyn_mutant accepted a no-op mutation"; fi
718
+ f0=$fail; p0=$pass
719
+ g1_dyn_mutant ctl-dyn-survive kill-own-child-only 'helper control: dynamic survivor' >/dev/null
720
+ if [[ $fail -ne $((f0 + 1)) || $pass -ne $((p0 + 1)) ]]; then hc_broken "g1_dyn_mutant passed a landed tree that kills nothing"; fi
721
+ # the Guard 2 state verdicts: each must accept the state it names and REJECT every other
722
+ local sd="$WORK/hc-state" _rr=$RN_RAN _rp=$RN_PROFS
723
+ mkdir -p "$sd/slot"; chmod 755 "$sd/slot"
724
+ RN_RAN=1; RN_PROFS=$'/p/a\n'
725
+ if ! prof_is /p/a; then hc_broken "prof_is rejected the profile the launch ran on"; fi
726
+ if prof_is /p/b; then hc_broken "prof_is accepted a different profile"; fi
727
+ RN_RAN=2; if prof_is /p/a; then hc_broken "prof_is accepted two launches as one"; fi
728
+ RN_RAN=0; if prof_is /p/a; then hc_broken "prof_is accepted a launch that never ran"; fi
729
+ RN_RAN=$_rr; RN_PROFS=$_rp
730
+ if link_is "$sd/slot" tgt; then hc_broken "link_is accepted a directory with no SingletonLock"; fi
731
+ if ! no_link "$sd/slot"; then hc_broken "no_link rejected a directory with no SingletonLock"; fi
732
+ ln -s tgt "$sd/slot/SingletonLock"
733
+ if ! link_is "$sd/slot" tgt; then hc_broken "link_is rejected the link it names"; fi
734
+ if link_is "$sd/slot" other; then hc_broken "link_is accepted a link with another target"; fi
735
+ if no_link "$sd/slot"; then hc_broken "no_link accepted a (dangling) SingletonLock symlink"; fi
736
+ if ! mode_is "$sd/slot" 755; then hc_broken "mode_is rejected the mode the directory has"; fi
737
+ if mode_is "$sd/slot" 700; then hc_broken "mode_is accepted a 755 directory as 700"; fi
738
+ # old_pattern_selects: the retired hook's selector matches the old decoy shape and not the Guard 1 decoy shape
739
+ if ! old_pattern_selects 'chrome --remote-debugging-pipe --user-data-dir=/x'; then hc_broken "old_pattern_selects missed the pipe-shaped Chrome argv"; fi
740
+ if old_pattern_selects "sleep-decoy-$$-x 300"; then hc_broken "old_pattern_selects matched a Guard 1 decoy argv"; fi
741
+ # reg_mutant
742
+ f0=$fail; r0=$red_rows
743
+ reg_mutant 'helper control: reg no-op' "$PROJECT_MCP" project 'pass' >/dev/null
744
+ if [[ $fail -ne $((f0 + 1)) || $red_rows -ne $r0 ]]; then hc_broken "reg_mutant accepted a no-op mutation"; fi
745
+ f0=$fail; p0=$pass
746
+ reg_mutant 'helper control: reg benign' "$PROJECT_MCP" project 'e["note"] = "benign"' >/dev/null
747
+ if [[ $fail -ne $((f0 + 1)) || $pass -ne $((p0 + 1)) ]]; then hc_broken "reg_mutant passed a landed mutant reg_ok does not reject"; fi
748
+ # slot_mutant: an edit that changes nothing is not a landing
749
+ f0=$fail
750
+ slot_mutant ctl-noop '_pwslot_max=32' '_pwslot_max=32' >/dev/null
751
+ if [[ $fail -ne $((f0 + 1)) || -n "$MROOT" ]]; then hc_broken "slot_mutant accepted an edit that changes nothing"; fi
752
+ f0=$fail
753
+ slot_mutant ctl-absent 'no such text in the script' 'x' >/dev/null 2>&1
754
+ if [[ $fail -ne $((f0 + 1)) || -n "$MROOT" ]]; then hc_broken "slot_mutant accepted an edit whose target is absent"; fi
755
+ pass=$_p; fail=$_f; cases=$_c; red_rows=$_r; mutants_declared=$_m
756
+ }
757
+
758
+ # ===========================================================================
759
+ # Guard 1 — no registered hook or launch command names a browser with a PID-selecting tool
760
+ # ===========================================================================
761
+ # The two real-tree verdicts run BEFORE the helper control: the control builds its fixtures from the real tree's hooks and slot
762
+ # script, so a pattern kill in the real tree would otherwise abort the suite as "HELPER CONTROL BROKEN" before the row that
763
+ # names the culprit could report. A defect in the tree is attributed here; a defect in a helper still aborts in the control.
764
+ mkroot1 "$G1/real"
765
+ g1 "$G1/real"
766
+ assert_true 'Guard 1: the real tree has no hook or launch string that kills a browser or MCP process by name or pattern' g1_rc_is 0
767
+ # --- live decoy: every executable population script of the real tree (plugin hook scripts, the sourced slot script, one hop of
768
+ # sourced libs) is run under the shim; the decoy must survive each, and the loop must have run (not zero iterations)
769
+ start_g1_decoy pop; DECOY=$SPAWNED
770
+ g1_dynamic "$G1/real" "$DECOY"
771
+ assert_true 'Guard 1 live: every executable population script of the real tree ran under the shim (at least the floor), not zero iterations' test "$DYN_RAN" -ge "$G1_FLOOR_RUN"
772
+ assert_true 'Guard 1 live: the decoy survived every population script of the real tree (the positive control below proves the decoy is killable)' test "$DYN_ALIVE" -eq 1
773
+ _helper_control
774
+ g1 "$G1/real"
775
+ pop_reported() { [[ "$G1_OUT" == *"POP commands="* && "$G1_OUT" != *VACUOUS* ]]; }
776
+ assert_true 'Guard 1: the scan examined a population at or above every floor (not vacuous)' pop_reported
777
+ pop_axes_covered() {
778
+ if [[ "$G1_OUT" =~ settings=([0-9]+)\ settings_scripts=([0-9]+)\ hops=([0-9]+) ]] && [[ "${BASH_REMATCH[1]}" -ge 10 && "${BASH_REMATCH[2]}" -ge 10 && "${BASH_REMATCH[3]}" -ge 1 ]]; then return 0; fi
779
+ return 1
780
+ }
781
+ assert_true 'Guard 1: the population includes the .claude/settings.json hooks and scripts sourced one hop from hook scripts' pop_axes_covered
782
+ assert_true 'Guard 1: hooks.json still parses and keeps stop-hook.sh and unkept-promise-hook.sh (positive proof the Stop event was scanned)' python3 - "$REPO_ROOT/plugins/soleur/hooks/hooks.json" <<'PY'
783
+ import json, sys
784
+ cmds = [h["command"] for g in json.load(open(sys.argv[1]))["hooks"]["Stop"] for h in g["hooks"]]
785
+ sys.exit(0 if any(c.endswith("/stop-hook.sh") for c in cmds) and any(c.endswith("/unkept-promise-hook.sh") for c in cmds) and not any("browser-cleanup" in c for c in cmds) else 1)
786
+ PY
787
+ assert_true 'Guard 1: plugins/soleur/hooks/browser-cleanup-hook.sh is absent' test ! -e "$REPO_ROOT/plugins/soleur/hooks/browser-cleanup-hook.sh"
788
+
789
+ # Must-PASS non-canonical inputs (static scan only: the shim kills the decoy for ANY pkill, so these are never executed):
790
+ # a hook that kills only a pid it spawned, and pkill on a non-browser name.
791
+ mkroot1 "$G1/ok-own"; python3 "$HELP/g1_mut.py" "$G1/ok-own" kill-own-child-only "$OLD_HOOK"
792
+ g1 "$G1/ok-own"
793
+ assert_true 'Guard 1 must-PASS: a hook that backgrounds and kills its own child (kill "$!") passes the static scan (the scan does not distinguish own from foreign pids; the shim rows do not run it)' g1_rc_is 0
794
+ mkroot1 "$G1/ok-other"; python3 "$HELP/g1_mut.py" "$G1/ok-other" pkill-non-browser "$OLD_HOOK"
795
+ g1 "$G1/ok-other"
796
+ assert_true 'Guard 1 must-PASS: pkill on a non-browser name passes' g1_rc_is 0
797
+
798
+ # Positive proof the scanner can see a killer at all: the synthesized old hook, planted under Stop.
799
+ mkroot1 "$G1/planted"; python3 "$HELP/g1_mut.py" "$G1/planted" restore-old-hook "$OLD_HOOK"
800
+ g1 "$G1/planted"
801
+ assert_true 'Guard 1 positive control: the scanner flags the old hook text registered under Stop (and names pgrep)' bash -c '[[ "$1" == 1 && "$2" == *"FLAG"*"[pgrep]"* ]]' _ "$G1_RC" "$G1_OUT"
802
+
803
+ # --- live decoy: the old hook text, executed under the shim, DOES kill a pattern-matched Chrome ---
804
+ start_g1_decoy live; DECOY=$SPAWNED
805
+ printf '%s' "$DECOY" > "$SHIM/decoy.pid"
806
+ shim_resolves() { local out; out="$(PATH="$SHIM:$PATH" pgrep anything)"; [[ "$(PATH="$SHIM:$PATH" command -v pgrep)" == "$SHIM/pgrep" && "$out" == "$DECOY" ]]; }
807
+ assert_true 'Guard 1 harness: under the scratch PATH pgrep resolves to the shim and prints only the decoy pid' shim_resolves
808
+ assert_true 'Guard 1 harness: the decoy is a live process before any hook runs' alive "$DECOY"
809
+ decoy_not_old_shaped() { local a; a="$(proc_argv "$DECOY")"; if [[ "$a" == "sleep-decoy-"* ]] && ! old_pattern_selects "$a"; then return 0; fi; return 1; }
810
+ assert_true 'Guard 1 harness: the decoy argv (read from /proc) cannot be selected by the retired Stop hook'"'"'s own pattern, so a host that still has that hook installed cannot kill it' decoy_not_old_shaped
811
+ cases=$((cases + 1))
812
+ if run_under_shim "$OLD_HOOK" "$DECOY" "$DECOY"; then bad 'Guard 1 positive control: the old hook text killed the decoy'; else ok 'Guard 1 positive control: the old hook text killed the decoy (the decoy is a real target)'; fi
813
+ # (the live rows over EVERY executable population script of the real tree ran above, before the helper control)
814
+
815
+ # --- Guard 1 mutation matrix: each mutated copy must be flagged (or hit a vacuity floor, or kill the decoy under the shim) ---
816
+ g1_mutant 1-restore-old-hook restore-old-hook 1 'Guard 1 mutant 1: the old hook and its Stop registration restored → RED' 'browser-cleanup-hook.sh'
817
+ g1_mutant 2-other-event killer-under-sessionstart 1 'Guard 1 mutant 2: a pattern-killing script registered under SessionStart → RED' 'zz-killer.sh'
818
+ g1_mutant 3-second-killer second-killer-same-event 1 'Guard 1 mutant 3: a second killing hook after the compliant ones in the same Stop array → RED' 'zz-killer.sh'
819
+ g1_mutant 4-mcp-pkill mcp-pkill 1 'Guard 1 mutant 4: pkill -9 -f "[c]hrome.*$prof" back in the .mcp.json launch string → RED' ' .mcp.json:playwright'
820
+ g1_mutant 5-empty-population empty-commands 3 'Guard 1 mutant 5: hooks.json command population emptied → RED (vacuity floor)' 'G1_FLOOR_CMDS='
821
+ g1_mutant 6-second-in-group second-in-group 1 'Guard 1 mutant 6: a killer as the SECOND hook inside one hooks.json group → RED' 'zz-killer.sh'
822
+ g1_mutant 7-plugin-mcp plugin-mcp-pkill 1 'Guard 1 mutant 7: a pkill in plugins/soleur/.mcp.json → RED' 'plugins/soleur/.mcp.json:reaper'
823
+ g1_mutant 8-sourced-killer sourced-killer 1 'Guard 1 mutant 8: a pattern kill appended to the sourced slot script → RED' 'playwright-mcp-profile-slot.sh'
824
+ g1_mutant 9-settings-killer settings-killer 1 'Guard 1 mutant 9: a killer script registered in .claude/settings.json → RED' '.claude/hooks/zz-killer.sh'
825
+ g1_mutant 10-hop-killer hop-killer 1 'Guard 1 mutant 10: a killer in a lib sourced one hop from a registered hook → RED' 'hooks/lib/zz-helper.sh'
826
+ for _w in headless_shell playwright remote-debugging user-data-dir mcp; do
827
+ g1_mutant "11-vocab-$_w" "vocab-$_w" 1 "Guard 1 mutant 11: pkill -f $_w (browser vocabulary without the word chrome) → RED" 'zz-killer.sh'
828
+ done
829
+ g1_mutant 12-killall killall-chrome 1 'Guard 1 mutant 12: killall -9 chrome → RED' 'zz-killer.sh'
830
+ g1_mutant 13-pidof pidof-chrome 1 'Guard 1 mutant 13: kill $(pidof chrome) → RED' 'zz-killer.sh'
831
+ g1_mutant 14-ps-grep-kill ps-grep-kill 1 'Guard 1 mutant 14: ps | grep chrome | xargs kill (the grep branch) → RED' 'zz-killer.sh'
832
+ g1_mutant 15-continuation continuation 1 'Guard 1 mutant 15: a backslash-continued pkill -f \<newline> "chrome..." → RED' 'zz-killer.sh'
833
+ g1_mutant 16-drop-plugin-mcp drop-plugin-mcp 3 'Guard 1 mutant 16: plugins/soleur/.mcp.json emptied → RED (mcp floor)' 'G1_FLOOR_MCP='
834
+ g1_mutant 17-drop-sourced drop-sourced 3 'Guard 1 mutant 17: the launch string no longer sources the slot script → RED (sourced floor)' 'G1_FLOOR_SOURCED='
835
+ # the shim run catches what the static text scan cannot see: indirection, split words, and (as a known positive) the old hook
836
+ g1_dyn_mutant 18-dyn-old-hook restore-old-hook 'Guard 1 mutant 18 (dynamic): the old hook, registered under Stop, kills the decoy when the population is executed under the shim → RED'
837
+ g1_dyn_mutant 19-dyn-indirection indirection 'Guard 1 mutant 19 (dynamic): pkill -f "$PAT" with the pattern in a variable (invisible to the static scan) kills the decoy → RED'
838
+ g1_dyn_mutant 20-dyn-split-word split-word 'Guard 1 mutant 20 (dynamic): pkill -f "chr""ome" (split word, invisible to the static scan) kills the decoy → RED'
839
+ g1_dyn_mutant 21-dyn-sourced-split-word sourced-split-word 'Guard 1 mutant 21 (dynamic): a split-word pkill planted in the SOURCED slot script (executed by sourcing under the shim) kills the decoy → RED'
840
+ g1_mutant 22-settings-hop-killer settings-hop-killer 1 'Guard 1 mutant 22: a killer in a lib sourced one hop from a .claude/settings.json hook script → RED' '.claude/hooks/lib/zz-helper.sh'
841
+ # Mutation (harness): the shim prints nothing, so the old hook kills nothing — the positive control must then FAIL
842
+ start_g1_decoy harness; DECOY=$SPAWNED
843
+ red 'Guard 1 mutant (harness): a pgrep shim that prints nothing → the old hook kills nothing, so the positive control would fail (the decoy is what makes it bite)' run_under_shim "$OLD_HOOK" "" "$DECOY"
844
+
845
+ # ===========================================================================
846
+ # Guard 2 — a launch claims a slot no live launch holds, and kills nothing
847
+ # ===========================================================================
848
+ MCP_ARGS="$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['mcpServers']['playwright']['args'][1])" "$REPO_ROOT/.mcp.json")"
849
+ MCP_ARG0="$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['mcpServers']['playwright']['args'][0])" "$REPO_ROOT/.mcp.json")"
850
+ # The npx shim records its argv (one JSON line), records its own pid, and STAYS ALIVE until the proxy ends it.
851
+ cat > "$G2/bin/npx" <<'SHIMSRC'
852
+ #!/usr/bin/env bash
853
+ python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' "$@" >> "$SHIM_OUT"
854
+ echo $$ >> "$SHIM_OUT.pid"
855
+ exec sleep 300
856
+ SHIMSRC
857
+ chmod +x "$G2/bin/npx"
858
+
859
+ assert_true 'Guard 2: .mcp.json playwright command is bash -c <string>' test "$MCP_ARG0" = "-c"
860
+ assert_true 'Guard 2: the launch string carries no pkill, killall or pgrep (no pattern kill anywhere)' bash -c '! grep -Eq "pkill|killall|pgrep" <<< "$1"' _ "$MCP_ARGS"
861
+ assert_true 'Guard 2: the launch string sources the slot script, then execs env and the proxy with --user-data-dir="$prof" (quoted, once), --config and exactly one pin, and carries no unreachable [ -n "$prof" ] clause' python3 - "$MCP_ARGS" "$SLOT_REL" "$PROXY_REL_PATH" <<'PY'
862
+ import re, sys
863
+ s, slot, proxy = sys.argv[1:]
864
+ ok = (
865
+ s.startswith(". " + slot + " || exit 1; exec env ")
866
+ and proxy in s
867
+ and s.count('--user-data-dir="$prof"') == 1
868
+ and "--user-data-dir=$prof" not in s
869
+ and s.count("--config=.claude/playwright-mcp.config.json") == 1
870
+ and len(re.findall(r"@playwright/mcp@[0-9.]+", s)) == 1
871
+ and '[ -n "$prof"' not in s
872
+ )
873
+ sys.exit(0 if ok else 1)
874
+ PY
875
+ assert_true 'Guard 2: the slot script exists, uses return (never exit), sets no errexit/nounset, never removes recursively, assigns prof, and holds fd 9' python3 - "$REPO_ROOT/$SLOT_REL" <<'PY'
876
+ import re, sys
877
+ try:
878
+ src = open(sys.argv[1]).read()
879
+ except OSError:
880
+ sys.exit(1)
881
+ code = "\n".join(l for l in src.splitlines() if not l.lstrip().startswith("#"))
882
+ sys.exit(0 if (
883
+ re.search(r"^\s*exit\b", code, re.M) is None
884
+ and re.search(r"\bset\s+-[a-zA-Z]*[eu]", code) is None
885
+ and re.search(r"\brm\s+(-[a-zA-Z]*[rR]|--recursive)", code) is None
886
+ and re.search(r"\breturn\b", code)
887
+ and re.search(r"\bprof=", code)
888
+ and re.search(r"9>", code)
889
+ ) else 1)
890
+ PY
891
+ # default_wait_is <slot-script> <n>: the ${PW_PROFILE_SLOT0_WAIT_S:-N} default, read from CODE (comment lines and trailing comments stripped)
892
+ default_wait_is() {
893
+ python3 - "$1" "$2" <<'PY'
894
+ import re, sys
895
+ code = "\n".join(re.sub(r"\s#.*$", "", l) for l in open(sys.argv[1]).read().splitlines() if not l.lstrip().startswith("#"))
896
+ sys.exit(0 if re.findall(r"PW_PROFILE_SLOT0_WAIT_S:-([0-9.]+)\}", code) == [sys.argv[2]] else 1)
897
+ PY
898
+ }
899
+ # wait_pairs_grace <slot-script> <proxy>: the slot-0 wait covers the proxy teardown (GRACE_S + STDIN_CLOSE_WAIT_S) with at least a second to spare
900
+ wait_pairs_grace() {
901
+ python3 - "$1" "$2" <<'PY'
902
+ import re, sys
903
+ slot = "\n".join(re.sub(r"\s#.*$", "", l) for l in open(sys.argv[1]).read().splitlines() if not l.lstrip().startswith("#"))
904
+ proxy = open(sys.argv[2]).read()
905
+ w = float(re.findall(r"PW_PROFILE_SLOT0_WAIT_S:-([0-9.]+)\}", slot)[0])
906
+ g = float(re.search(r"^GRACE_S = ([0-9.]+)", proxy, re.M).group(1))
907
+ s = float(re.search(r"^STDIN_CLOSE_WAIT_S = ([0-9.]+)", proxy, re.M).group(1))
908
+ sys.exit(0 if w >= g + s + 1 else 1)
909
+ PY
910
+ }
911
+ assert_true 'Guard 2: the slot-0 reconnect wait defaults to 7 seconds (read from code, comments stripped)' default_wait_is "$REPO_ROOT/$SLOT_REL" 7
912
+ assert_true 'Guard 2: the slot-0 default wait covers the proxy teardown: >= GRACE_S + STDIN_CLOSE_WAIT_S + 1, both read from the proxy' wait_pairs_grace "$REPO_ROOT/$SLOT_REL" "$REPO_ROOT/$PROXY_REL_PATH"
913
+
914
+ # launch <tag> <root> <home> <cmd> [VAR=val...] — the launch string under bash -c, stdin held open by a sleeper
915
+ # (LAUNCH_PATH, when set, replaces the PATH the launch runs under)
916
+ launch() {
917
+ local tag="$1" root="$2" home="$3" cmd="$4"; shift 4
918
+ local fifo="$G2/$tag.fifo"
919
+ : > "$G2/$tag.jsonl"; rm -f "$G2/$tag.jsonl.pid" "$fifo"
920
+ mkdir -p "$home"; mkfifo "$fifo"
921
+ sleep 600 > "$fifo" &
922
+ printf '%s' "$!" > "$G2/$tag.sleeper"; own "$!"; disown "$!"
923
+ ( cd "$root" && exec env "$@" SHIM_OUT="$G2/$tag.jsonl" HOME="$home" PATH="${LAUNCH_PATH:-$G2/bin:$PATH}" bash -c "$cmd" ) < "$fifo" > /dev/null 2> "$G2/$tag.err" &
924
+ printf '%s' "$!" > "$G2/$tag.pid"; own "$!"; disown "$!"
925
+ }
926
+ # wait_ready <tag> — 0 when the shim ran (its argv is recorded), 1 when the launch died first (a FAIL for the calling row). A launch
927
+ # that is STILL RUNNING after 30 s without reaching the shim is UNRESOLVED (the host is too loaded to judge the row, or the slot script
928
+ # hangs; not a verdict on any guard): the suite stops with that message, rc 2, instead of letting the row read as "mutant survived".
929
+ WAIT_READY_TICKS=300 # 0.1 s ticks; a helper control shortens it in a subshell
930
+ wait_ready() {
931
+ local tag="$1" i=0
932
+ while [[ ! -s "$G2/$tag.jsonl" && $i -lt $WAIT_READY_TICKS ]]; do
933
+ if ! alive "$(cat "$G2/$tag.pid")"; then break; fi
934
+ sleep 0.1; i=$((i + 1))
935
+ done
936
+ if [[ -s "$G2/$tag.jsonl" ]]; then return 0; fi
937
+ if alive "$(cat "$G2/$tag.pid")"; then
938
+ printf 'UNRESOLVED: launch %s was still running after %d s without reaching the server shim: the host is too loaded to judge this row, or the slot script hangs; rerun when the host is idle, and read the slot script if it still hangs\n' "$tag" "$((WAIT_READY_TICKS / 10))" >&2
939
+ exit 2
940
+ fi
941
+ return 1
942
+ }
943
+ end_launch() { # <tag> — close the launch's stdin (kill its sleeper), wait for it to exit
944
+ local tag="$1" s
945
+ s="$(cat "$G2/$tag.sleeper" 2>/dev/null || true)"
946
+ if [[ -n "$s" ]]; then kill "$s" 2>/dev/null || true; fi
947
+ wait_dead "$(cat "$G2/$tag.pid")" 8 || true
948
+ }
949
+ prof_of() { # <tag> — the --user-data-dir the server was launched with
950
+ python3 -c 'import json,sys
951
+ for l in open(sys.argv[1]):
952
+ for a in json.loads(l):
953
+ if a.startswith("--user-data-dir="): print(a.split("=", 1)[1]); sys.exit(0)' "$G2/$1.jsonl"
954
+ }
955
+ # run_n <pfx> <root> <home> <cmd> <n> <overlap|sequential> [VAR=val...]
956
+ # sets RN_PROFS (one per line), RN_RAN (shims that ran), RN_OVERLAP (every launch alive AND every distinct profile's lock held at one instant),
957
+ # RN_MS (milliseconds from starting the first launch until its server shim ran)
958
+ run_n() {
959
+ local pfx="$1" root="$2" home="$3" cmd="$4" n="$5" mode="$6" i tag p t0; shift 6
960
+ RN_PROFS=""; RN_RAN=0; RN_OVERLAP=1; RN_MS=0
961
+ for ((i = 1; i <= n; i++)); do
962
+ tag="$pfx-$i"
963
+ t0="$(now_ms)"
964
+ launch "$tag" "$root" "$home" "$cmd" "$@"
965
+ if wait_ready "$tag"; then RN_RAN=$((RN_RAN + 1)); RN_PROFS+="$(prof_of "$tag")"$'\n'; else RN_OVERLAP=0; fi
966
+ if [[ $i -eq 1 ]]; then RN_MS=$(( $(now_ms) - t0 )); fi
967
+ if [[ "$mode" == sequential ]]; then end_launch "$tag"; fi
968
+ done
969
+ for ((i = 1; i <= n; i++)); do
970
+ if ! alive "$(cat "$G2/$pfx-$i.pid")"; then RN_OVERLAP=0; fi
971
+ done
972
+ while read -r p; do
973
+ if [[ -n "$p" ]] && ! lock_held "$p/$LOCKNAME"; then RN_OVERLAP=0; fi
974
+ done <<< "$RN_PROFS"
975
+ for ((i = 1; i <= n; i++)); do end_launch "$pfx-$i"; done
976
+ }
977
+ rn_distinct() { [[ "$RN_RAN" -eq "$1" && "$(sort -u <<< "$RN_PROFS" | grep -c . || true)" -eq "$1" ]]; }
978
+ rn_overlap() { [[ "$RN_OVERLAP" -eq 1 ]]; }
979
+ prof0() { printf '%s/.cache/playwright-mcp-profile' "$1"; }
980
+ FAST=PW_PROFILE_SLOT0_WAIT_S=0.3
981
+
982
+ # --- real tree: overlapped launches ---
983
+ H="$G2/h-two"
984
+ run_n real2 "$REPO_ROOT" "$H" "$MCP_ARGS" 2 overlap "$FAST"
985
+ assert_true 'Guard 2: two overlapped launches under one HOME both ran the server and resolve DISTINCT --user-data-dir values' rn_distinct 2
986
+ assert_true 'Guard 2: the first launch took the persistent slot 0 profile' bash -c '[[ "$(head -n1 <<< "$1")" == "$2" ]]' _ "$RN_PROFS" "$(prof0 "$H")"
987
+ assert_true 'Guard 2: overlap proven — both launches were alive and both slot locks were held at the same instant' rn_overlap
988
+ assert_true 'Guard 2: the slot-0 lease wrote its owner file (the launcher PPID) with mode 600 and the slot directory is mode 700' bash -c '[[ "$(stat -c %a "$1/.pwslot.owner")" == 600 && "$(stat -c %a "$1")" == 700 && "$(cat "$1/.pwslot.owner")" == "$2" ]]' _ "$(prof0 "$H")" "$$"
989
+ H="$G2/h-three"
990
+ run_n real3 "$REPO_ROOT" "$H" "$MCP_ARGS" 3 overlap "$FAST"
991
+ assert_true 'Guard 2: a third concurrent launch also gets its own directory (three distinct)' rn_distinct 3
992
+ assert_true 'Guard 2: overlap proven for three launches' rn_overlap
993
+ assert_true 'Guard 2: after every launch exited, slot 0 is free again (the kernel released the lock)' lock_free "$(prof0 "$H")/$LOCKNAME"
994
+
995
+ # --- the lease belongs to the proxy process: fd 9 is held by it and is NOT inherited by the server child ---
996
+ H="$G2/h-fd"
997
+ launch fd1 "$REPO_ROOT" "$H" "$MCP_ARGS" "$FAST"
998
+ fd_ready=0; if wait_ready fd1; then fd_ready=1; fi
999
+ fd_proxy="$(cat "$G2/fd1.pid")"; fd_child="$(head -n1 "$G2/fd1.jsonl.pid" 2>/dev/null || true)"
1000
+ lease_in_proxy_only() { [[ "$fd_ready" -eq 1 && -n "$fd_child" && "$(readlink "/proc/$fd_proxy/fd/9" 2>/dev/null)" == *"/$LOCKNAME" && ! -e "/proc/$fd_child/fd/9" ]]; }
1001
+ assert_true 'Guard 2: the lease (fd 9 on the slot lock) is held by the proxy process and is not inherited by the server child, so it dies with the proxy' lease_in_proxy_only
1002
+ end_launch fd1
1003
+
1004
+ # --- SIGKILL of the proxy under lease: the kernel releases the lock (the server child does not hold it) ---
1005
+ H="$G2/h-kill"
1006
+ launch kill1 "$REPO_ROOT" "$H" "$MCP_ARGS" "$FAST"
1007
+ kill_ready=0; if wait_ready kill1; then kill_ready=1; fi
1008
+ kill_held=0; if lock_held "$(prof0 "$H")/$LOCKNAME"; then kill_held=1; fi
1009
+ kill_proxy="$(cat "$G2/kill1.pid")"
1010
+ kill -KILL "$kill_proxy" 2>/dev/null || true
1011
+ wait_dead "$kill_proxy" 3 || true
1012
+ sigkill_released() { if [[ "$kill_ready" -eq 1 && "$kill_held" -eq 1 ]] && ! alive "$kill_proxy" && lock_free "$(prof0 "$H")/$LOCKNAME"; then return 0; fi; return 1; }
1013
+ assert_true 'Guard 2: the lock was held while the proxy ran, and a SIGKILL of the proxy released it (the lease dies with the proxy)' sigkill_released
1014
+ end_launch kill1
1015
+
1016
+ # --- a launch kills nothing: four decoy shapes, each a different selector, all survive the launch and its teardown ---
1017
+ # D_SLOT0: chrome on the claimed slot's profile (the old reaper's own pattern) D_OTHER: chrome on ANOTHER slot's profile
1018
+ # D_SRV: a sibling playwright-mcp server (node .../bin/playwright-mcp --user-data-dir=<other slot>) D_FD: a Chrome holding a file open inside the claimed slot
1019
+ D_SLOT0=0; D_OTHER=0; D_SRV=0; D_FD=0
1020
+ spawn_decoys() { # <home>
1021
+ mkdir -p "$(prof0 "$1")"
1022
+ start_decoy "chrome --user-data-dir=$(prof0 "$1")"; D_SLOT0=$SPAWNED
1023
+ start_decoy "chrome --user-data-dir=$(prof0 "$1")-1"; D_OTHER=$SPAWNED
1024
+ start_decoy "node /synthetic/node_modules/@playwright/mcp/bin/playwright-mcp --user-data-dir=$(prof0 "$1")-2"; D_SRV=$SPAWNED
1025
+ start_holder "chrome --type=renderer" "$(prof0 "$1")/Cookies"; D_FD=$SPAWNED
1026
+ }
1027
+ H="$G2/h-decoy"
1028
+ spawn_decoys "$H"
1029
+ assert_true 'Guard 2 harness: the slot-0 decoy argv matches the pattern the old reaper used (chrome.*<slot0>), read from /proc, nothing signalled' python3 - "$D_SLOT0" "$(prof0 "$H")" <<'PY'
1030
+ import re, sys
1031
+ cmd = open(f"/proc/{sys.argv[1]}/cmdline", "rb").read().replace(b"\0", b" ").decode()
1032
+ sys.exit(0 if re.search(r"chrome.*" + re.escape(sys.argv[2]), cmd) else 1)
1033
+ PY
1034
+ fd_holder_holds() { [[ "$(readlink "/proc/$D_FD/fd/7" 2>/dev/null)" == "$(prof0 "$H")/Cookies" ]]; }
1035
+ assert_true 'Guard 2 harness: the file-holder decoy really holds a file open inside the claimed slot (fd 7 -> <slot0>/Cookies)' fd_holder_holds
1036
+ run_n decoy "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1037
+ assert_true 'Guard 2: the launch ran (the shim recorded its argv)' test "$RN_RAN" -eq 1
1038
+ assert_true 'Guard 2: the decoy Chrome on the claimed profile is still alive after the launch and its teardown' alive "$D_SLOT0"
1039
+ assert_true 'Guard 2: a decoy Chrome on ANOTHER slot'"'"'s profile is still alive after the launch and its teardown' alive "$D_OTHER"
1040
+ assert_true 'Guard 2: a decoy sibling playwright-mcp server (node .../bin/playwright-mcp --user-data-dir=<other slot>) is still alive after the launch and its teardown' alive "$D_SRV"
1041
+ assert_true 'Guard 2: a decoy holding a file open inside the claimed slot is still alive after the launch and its teardown' alive "$D_FD"
1042
+
1043
+ # --- stale SingletonLock (dead owner) is cleared and slot 0 reused ---
1044
+ H="$G2/h-stale"; mkdir -p "$(prof0 "$H")"
1045
+ sleep 0 & dead=$!; wait "$dead" || true
1046
+ if alive "$dead"; then dead=2147483646; fi
1047
+ ln -s "$HOST_NOW-$dead" "$(prof0 "$H")/SingletonLock"; : > "$(prof0 "$H")/SingletonCookie"; : > "$(prof0 "$H")/SingletonSocket"
1048
+ run_n stale "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1049
+ stale_state() { prof_is "$(prof0 "$H")" && no_link "$(prof0 "$H")" && [[ ! -e "$(prof0 "$H")/SingletonCookie" && ! -e "$(prof0 "$H")/SingletonSocket" ]]; }
1050
+ assert_true 'Guard 2 must-PASS: a stale SingletonLock naming a dead pid on this host is cleared (lock, cookie, socket) and slot 0 is reused' stale_state
1051
+
1052
+ # --- a live CHROME owner with a FREE flock (proxy gone, Chrome alive): the slot is skipped, the lock left alone ---
1053
+ H="$G2/h-owner"; mkdir -p "$(prof0 "$H")"
1054
+ start_chrome_owner; OWNER=$SPAWNED
1055
+ ln -s "$HOST_NOW-$OWNER" "$(prof0 "$H")/SingletonLock"
1056
+ run_n owner "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1057
+ owner_skipped() { prof_is "$(prof0 "$H")-1"; }
1058
+ owner_lock_kept() { link_is "$(prof0 "$H")" "$HOST_NOW-$OWNER" && alive "$OWNER"; }
1059
+ assert_true 'Guard 2 must-PASS: a free lock but a live-Chrome-owner SingletonLock skips the slot (the launch lands on slot 1)' owner_skipped
1060
+ assert_true 'Guard 2 must-PASS: the live owner'"'"'s SingletonLock was left in place and its owner was not touched' owner_lock_kept
1061
+ assert_true 'Guard 2: skipping slot 0 prints exactly ONE stderr line, naming the live Chrome and that the persistent profile'"'"'s logins are not available in this session' bash -c '[[ "$(grep -c "^playwright-mcp-profile-slot:" "$1")" == 1 && "$(grep "^playwright-mcp-profile-slot:" "$1")" == *"live Chrome"*"logins are not available in this session"* ]]' _ "$G2/owner-1.err"
1062
+
1063
+ # --- a recycled pid: the lock names a live pid that is NOT a Chrome -> stale, cleared, slot 0 reused ---
1064
+ H="$G2/h-recycled"; mkdir -p "$(prof0 "$H")"
1065
+ start_sleeper; RECYCLED=$SPAWNED
1066
+ ln -s "$HOST_NOW-$RECYCLED" "$(prof0 "$H")/SingletonLock"
1067
+ run_n recycled "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1068
+ recycled_state() { prof_is "$(prof0 "$H")" && no_link "$(prof0 "$H")" && alive "$RECYCLED"; }
1069
+ assert_true 'Guard 2 must-PASS: a SingletonLock naming a live pid whose comm is not Chrome (a recycled pid) is stale: cleared, slot 0 reused, the unrelated process untouched' recycled_state
1070
+
1071
+ # --- a live Chromium derivative (comm msedge, brave, vivaldi-bin, opera) or headless build (comm headless_shell) owner is a browser too: kept, never read as a recycled pid ---
1072
+ for _c in msedge brave vivaldi-bin opera headless_shell; do
1073
+ H="$G2/h-comm-$_c"; mkdir -p "$(prof0 "$H")"
1074
+ start_comm_owner "$_c"; COMM_OWNER=$SPAWNED
1075
+ ln -s "$HOST_NOW-$COMM_OWNER" "$(prof0 "$H")/SingletonLock"
1076
+ run_n "comm-$_c" "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1077
+ comm_kept() { prof_is "$(prof0 "$H")-1" && link_is "$(prof0 "$H")" "$HOST_NOW-$COMM_OWNER" && alive "$COMM_OWNER"; }
1078
+ assert_true "Guard 2 must-PASS: a free lock but a live owner whose comm is $_c skips the slot and keeps its SingletonLock (a browser, not a recycled pid)" comm_kept
1079
+ done
1080
+
1081
+ # --- a recycled pid whose comm merely CONTAINS a derivative's name ("operator", "knowledge-bas": opera / edge) is not a browser: stale, slot 0 reclaimed ---
1082
+ for _c in operator knowledge-bas; do
1083
+ H="$G2/h-notcomm-$_c"; mkdir -p "$(prof0 "$H")"
1084
+ start_comm_owner "$_c"; COMM_OWNER=$SPAWNED
1085
+ ln -s "$HOST_NOW-$COMM_OWNER" "$(prof0 "$H")/SingletonLock"
1086
+ run_n "notcomm-$_c" "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1087
+ notcomm_stale() { prof_is "$(prof0 "$H")" && no_link "$(prof0 "$H")" && alive "$COMM_OWNER"; }
1088
+ assert_true "Guard 2 must-PASS: a SingletonLock naming a live pid whose comm is $_c (a substring of a derivative's name, not a browser) is stale: cleared, slot 0 reused, the process untouched" notcomm_stale
1089
+ done
1090
+
1091
+ # --- a lock naming ANOTHER HOST is never removed, whatever the local pid says ---
1092
+ H="$G2/h-foreign"; mkdir -p "$(prof0 "$H")"
1093
+ ln -s "otherhost-$dead" "$(prof0 "$H")/SingletonLock"
1094
+ run_n foreign "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1095
+ foreign_state() { prof_is "$(prof0 "$H")-1" && link_is "$(prof0 "$H")" "otherhost-$dead"; }
1096
+ assert_true 'Guard 2 must-PASS: a SingletonLock naming another host (even with a pid that is dead here) is kept and the slot skipped' foreign_state
1097
+
1098
+ # --- a reconnect (the old holder exits within the 7 s wait) reacquires slot 0 rather than moving to slot 1 ---
1099
+ H="$G2/h-reconnect"
1100
+ launch rc-old "$REPO_ROOT" "$H" "$MCP_ARGS"
1101
+ wait_ready rc-old || true
1102
+ launch rc-new "$REPO_ROOT" "$H" "$MCP_ARGS"
1103
+ sleep 1
1104
+ end_launch rc-old
1105
+ rc_got=0; if wait_ready rc-new; then rc_got=1; fi
1106
+ rc_prof="$(if [[ $rc_got -eq 1 ]]; then prof_of rc-new; fi)"
1107
+ end_launch rc-new
1108
+ assert_true 'Guard 2 must-PASS: a reconnect — the old holder (same launcher) leaves within the wait — reacquires slot 0, not slot 1' test "$rc_got:$rc_prof" = "1:$(prof0 "$H")"
1109
+
1110
+ # --- owner-aware wait: the same session's predecessor is waited for, a DIFFERENT session's lock is not, an unknown owner is ---
1111
+ OTHER_SESSION=2147483646
1112
+ H="$G2/h-same"; hold_lock "$(prof0 "$H")" 2 "$$"
1113
+ run_n same "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=4
1114
+ same_waits() { prof_is "$(prof0 "$H")" && [[ "$RN_MS" -ge 1200 ]]; }
1115
+ assert_true 'Guard 2: slot 0 held by this session'"'"'s own launcher (owner file == $PPID) is waited for and reacquired when the holder leaves' same_waits
1116
+ H="$G2/h-other"; hold_lock "$(prof0 "$H")" 9 "$OTHER_SESSION"
1117
+ run_n other "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=4
1118
+ other_no_wait() { prof_is "$(prof0 "$H")-1" && [[ "$RN_MS" -lt 2500 ]]; }
1119
+ assert_true 'Guard 2: slot 0 held by a DIFFERENT session is skipped at once, without the wait (slot 1 in under 2.5 s with a 4 s budget)' other_no_wait
1120
+ assert_true 'Guard 2: that skip printed exactly ONE stderr line saying slot 0 was skipped and its logins are not available' bash -c '[[ "$(grep -c "^playwright-mcp-profile-slot:" "$1")" == 1 && "$(grep "^playwright-mcp-profile-slot:" "$1")" == *"slot 0"*"was skipped"*"logins are not available in this session"* ]]' _ "$G2/other-1.err"
1121
+ H="$G2/h-unknown"; hold_lock "$(prof0 "$H")" 2
1122
+ run_n unknown "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=4
1123
+ unknown_waits() { prof_is "$(prof0 "$H")" && [[ "$RN_MS" -ge 1200 ]]; }
1124
+ assert_true 'Guard 2: slot 0 held with NO owner file (unknown owner) is waited for (fails toward the persistent profile)' unknown_waits
1125
+ # a live-Chrome SingletonLock whose Chrome exits within the wait: same session -> slot 0 is reacquired after the recheck
1126
+ H="$G2/h-chromeexit"; mkdir -p "$(prof0 "$H")"; printf '%s\n' "$$" > "$(prof0 "$H")/.pwslot.owner"
1127
+ start_chrome_owner; CEXIT=$SPAWNED
1128
+ ln -s "$HOST_NOW-$CEXIT" "$(prof0 "$H")/SingletonLock"
1129
+ ( sleep 1.2; kill -KILL "$CEXIT" 2>/dev/null || true ) >/dev/null 2>&1 & disown "$!"
1130
+ run_n chromeexit "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=4
1131
+ chromeexit_rechecks() { prof_is "$(prof0 "$H")" && no_link "$(prof0 "$H")" && [[ "$RN_MS" -ge 900 ]]; }
1132
+ assert_true 'Guard 2: a live-Chrome SingletonLock on slot 0 gets the same wait-then-recheck: the Chrome exits within the wait and slot 0 is reacquired' chromeexit_rechecks
1133
+
1134
+ # --- a non-numeric PW_PROFILE_SLOT0_WAIT_S is noted and replaced by the default; it never degrades the lease ---
1135
+ H="$G2/h-badwait"; hold_lock "$(prof0 "$H")" 2 "$$"
1136
+ run_n badwait "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=abc
1137
+ badwait_state() { prof_is "$(prof0 "$H")" && grep -q "ignored PW_PROFILE_SLOT0_WAIT_S='abc'" "$G2/badwait-1.err"; }
1138
+ assert_true 'Guard 2: PW_PROFILE_SLOT0_WAIT_S=abc is noted on stderr and the 7 s default applies (the launch still reacquires slot 0)' badwait_state
1139
+
1140
+ # --- an unusable middle slot does not end the search: slot 1 is a regular file, the launch continues to slot 2 ---
1141
+ H="$G2/h-mid"; hold_lock "$(prof0 "$H")" 30 "$OTHER_SESSION"; : > "$(prof0 "$H")-1"
1142
+ run_n mid "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1143
+ assert_true 'Guard 2: slot 0 busy and slot 1 unusable (a regular file) -> the launch continues to slot 2 instead of the unique fallback' prof_is "$(prof0 "$H")-2"
1144
+
1145
+ # --- restricted PATHs: no flock, and no ps ---
1146
+ mkbin() { # <dir> [extra tool...]: the tools the launch string needs, plus the npx shim, and nothing else
1147
+ local d="$1" t tp; shift
1148
+ assert_fixture_dir "$d"
1149
+ mkdir -p "$d"
1150
+ for t in bash env python3 mkdir rm readlink sleep cat dirname basename head sed grep tr ls uname "$@"; do
1151
+ tp="$(command -v "$t" 2>/dev/null || true)"
1152
+ if [[ -n "$tp" ]]; then ln -sf "$tp" "$d/$t"; fi
1153
+ done
1154
+ cp "$G2/bin/npx" "$d/npx"
1155
+ }
1156
+ NOFLOCK="$G2/noflock-bin"; mkbin "$NOFLOCK" ps
1157
+ NOPS="$G2/nops-bin"; mkbin "$NOPS" flock
1158
+ no_flock_on_path() { ! PATH="$NOFLOCK" command -v flock >/dev/null 2>&1; }
1159
+ assert_true 'Guard 2 harness: flock really was absent from the restricted PATH' no_flock_on_path
1160
+ no_ps_on_path() { ! PATH="$NOPS" command -v ps >/dev/null 2>&1 && PATH="$NOPS" command -v flock >/dev/null 2>&1; }
1161
+ assert_true 'Guard 2 harness: ps really was absent (and flock present) on the second restricted PATH' no_ps_on_path
1162
+
1163
+ # no flock: the FIRST session still gets the persistent profile (slot 0 claimed without a lease) and says so
1164
+ H="$G2/h-noflock"
1165
+ LAUNCH_PATH="$NOFLOCK" run_n nf1 "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap
1166
+ assert_true 'Guard 2 must-PASS: with no flock on PATH the first launch starts on the persistent slot 0 profile (claimed without a lease)' prof_is "$(prof0 "$H")"
1167
+ assert_true 'Guard 2: that no-flock launch noted on stderr that it runs without a lease' grep -q 'without a lease' "$G2/nf1-1.err"
1168
+ # no flock and slot 0 owned by a live Chrome: the unique fallback $base-p<pid>, created mode 700
1169
+ start_chrome_owner; NFOWNER=$SPAWNED
1170
+ ln -s "$HOST_NOW-$NFOWNER" "$(prof0 "$H")/SingletonLock"
1171
+ LAUNCH_PATH="$NOFLOCK" run_n nf2 "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap
1172
+ nf2_state() { local pid; pid="$(cat "$G2/nf2-1.pid")"; prof_is "$(prof0 "$H")-p$pid" && mode_is "$(prof0 "$H")-p$pid" 700 && link_is "$(prof0 "$H")" "$HOST_NOW-$NFOWNER"; }
1173
+ assert_true 'Guard 2 must-PASS: with no flock and a live-Chrome-owned slot 0 the launch uses a unique $base-p<pid> directory (mode 700) and leaves the lock alone' nf2_state
1174
+
1175
+ # no ps: a live Chrome-owned lock is KEPT (the probe never needs ps), and a dead owner's lock is still cleared
1176
+ H="$G2/h-nops"; mkdir -p "$(prof0 "$H")"
1177
+ start_chrome_owner; NPOWNER=$SPAWNED
1178
+ ln -s "$HOST_NOW-$NPOWNER" "$(prof0 "$H")/SingletonLock"
1179
+ LAUNCH_PATH="$NOPS" run_n nops "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1180
+ nops_kept() { prof_is "$(prof0 "$H")-1" && link_is "$(prof0 "$H")" "$HOST_NOW-$NPOWNER" && alive "$NPOWNER"; }
1181
+ assert_true 'Guard 2 must-PASS: with ps absent a live Chrome-owned SingletonLock is kept and the slot skipped (an unprobeable owner is never treated as stale)' nops_kept
1182
+ H="$G2/h-nops-dead"; mkdir -p "$(prof0 "$H")"
1183
+ ln -s "$HOST_NOW-$dead" "$(prof0 "$H")/SingletonLock"
1184
+ LAUNCH_PATH="$NOPS" run_n nopsd "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1185
+ nopsd_cleared() { prof_is "$(prof0 "$H")" && no_link "$(prof0 "$H")"; }
1186
+ assert_true 'Guard 2 must-PASS: with ps absent a dead owner'"'"'s lock is still cleared (/proc and kill -0 answer without ps)' nopsd_cleared
1187
+
1188
+ # --- launches released at the SAME instant (a barrier): the flock capability probe must not make them contend ---
1189
+ # The rows above start their launches one after another, which is why a probe that locked one shared inode (/dev/null) and
1190
+ # read a lost race as "no usable flock" stayed green: concurrent launches then took the no-lease branch and shared slot 0.
1191
+ # bar_launch <tag> <home> <go-microsecond-epoch> <script> [VAR=val...]: sources <script>, records $prof, holds fd 9 (exec sleep) like the proxy
1192
+ BAR_PIDS="$WORK/bar-pids"; : > "$BAR_PIDS"
1193
+ bar_launch() {
1194
+ local tag="$1" home="$2" go="$3" script="$4"; shift 4
1195
+ rm -f "$G2/$tag.prof" "$G2/$tag.err"
1196
+ mkdir -p "$home"
1197
+ ( cd "$REPO_ROOT" && exec env "$@" HOME="$home" PW_PROFILE_SLOT0_WAIT_S=0.3 bash -c 'r=$(( ($1 - ${EPOCHREALTIME/./}) / 1000 - 150 )); if [ "$r" -gt 0 ]; then printf -v s "%d.%03d" $((r / 1000)) $((r % 1000)); sleep "$s"; fi; while [ "${EPOCHREALTIME/./}" -lt "$1" ]; do :; done; . "$2" || exit 1; printf "%s\n" "$prof" > "$3"; exec sleep 12' _ "$go" "$script" "$G2/$tag.prof" ) > /dev/null 2> "$G2/$tag.err" &
1198
+ printf '%s\n' "$!" >> "$BAR_PIDS"; own "$!"; disown "$!"
1199
+ }
1200
+ # barrier_run <pfx> <home> <n> <script> [VAR=val...]: n launches released together; sets BAR_RAN (profiles recorded), BAR_DISTINCT, BAR_NOTES ("no usable flock" notes)
1201
+ # A launcher that EXITED without recording a profile is a failure of the slot script (as wait_ready treats a dead launch): BAR_DEAD counts
1202
+ # them, each is named on stderr with the head of its error output, and the row reading BAR_RAN fails. Only a launcher still RUNNING after
1203
+ # 60 s is UNRESOLVED (the host is too loaded to judge the row, or the slot script hangs): the suite stops with that message, rc 2.
1204
+ BAR_RAN=0; BAR_DISTINCT=0; BAR_NOTES=0; BAR_DEAD=0
1205
+ BAR_WAIT_TICKS=600 # 0.1 s ticks; a helper control shortens it in a subshell
1206
+ barrier_run() {
1207
+ local pfx="$1" home="$2" n="$3" script="$4" i t=0 go pid; shift 4
1208
+ : > "$BAR_PIDS"; BAR_RAN=0; BAR_DISTINCT=0; BAR_NOTES=0; BAR_DEAD=0
1209
+ go=$(( ${EPOCHREALTIME/./} + 2500000 ))
1210
+ for ((i = 1; i <= n; i++)); do bar_launch "$pfx-$i" "$home" "$go" "$script" "$@"; done
1211
+ while [[ $t -lt $BAR_WAIT_TICKS ]]; do
1212
+ BAR_RAN=0; BAR_DEAD=0
1213
+ for ((i = 1; i <= n; i++)); do
1214
+ if [[ -s "$G2/$pfx-$i.prof" ]]; then BAR_RAN=$((BAR_RAN + 1))
1215
+ else
1216
+ pid="$(sed -n "${i}p" "$BAR_PIDS")"
1217
+ if ! alive "$pid"; then BAR_DEAD=$((BAR_DEAD + 1)); fi
1218
+ fi
1219
+ done
1220
+ if [[ $((BAR_RAN + BAR_DEAD)) -eq $n ]]; then break; fi
1221
+ sleep 0.1; t=$((t + 1))
1222
+ done
1223
+ if [[ $BAR_DEAD -gt 0 ]]; then
1224
+ for ((i = 1; i <= n; i++)); do
1225
+ if [[ ! -s "$G2/$pfx-$i.prof" ]]; then printf 'FAIL: barrier launcher %s-%d exited without recording a profile: %s\n' "$pfx" "$i" "$(head -c 200 "$G2/$pfx-$i.err" | tr '\n' ' ')" >&2; fi
1226
+ done
1227
+ elif [[ $BAR_RAN -ne $n ]]; then
1228
+ printf 'UNRESOLVED: only %d of %d barrier launches (%s) recorded a profile within %d s and the rest are still running: the host is too loaded to judge this row, or the slot script hangs; rerun when the host is idle\n' "$BAR_RAN" "$n" "$pfx" "$((BAR_WAIT_TICKS / 10))" >&2; exit 2
1229
+ fi
1230
+ BAR_DISTINCT="$(cat "$G2/$pfx"-*.prof 2> /dev/null | sort -u | grep -c . || true)"
1231
+ BAR_NOTES="$(cat "$G2/$pfx"-*.err 2> /dev/null | grep -c 'no usable flock' || true)"
1232
+ reap_list "$BAR_PIDS"
1233
+ }
1234
+ # controls for the abort paths of wait_ready and barrier_run, each driven by a stub in a subshell (a regression that made them never fire
1235
+ # would otherwise go unnoticed): a launch that DIED is rc 1 (a FAIL for its row), never rc 2; one STILL RUNNING past the limit is UNRESOLVED, rc 2
1236
+ CTL_SLOT_FAIL="$G2/ctl-slot-fail.sh"; printf 'return 1\n' > "$CTL_SLOT_FAIL"
1237
+ CTL_SLOT_HANG="$G2/ctl-slot-hang.sh"; printf 'exec sleep 6\n' > "$CTL_SLOT_HANG"
1238
+ launch ctlwr1 "$REPO_ROOT" "$G2/h-ctlwr1" 'exec sleep 5'
1239
+ ctl_wr1_rc=0; ( WAIT_READY_TICKS=10; wait_ready ctlwr1 ) 2> "$G2/ctlwr1.msg" || ctl_wr1_rc=$?
1240
+ end_launch ctlwr1
1241
+ ctl_wr_hang() { if [[ "$ctl_wr1_rc" -eq 2 ]] && grep -q '^UNRESOLVED: launch ctlwr1 was still running after 1 s .*or the slot script hangs' "$G2/ctlwr1.msg"; then return 0; fi; return 1; }
1242
+ assert_true 'Guard 2 control: wait_ready on a launch still running past its limit stops the suite with UNRESOLVED (rc 2) and names a hang as well as a loaded host' ctl_wr_hang
1243
+ launch ctlwr2 "$REPO_ROOT" "$G2/h-ctlwr2" 'exit 3'
1244
+ ctl_wr2_rc=0; ( WAIT_READY_TICKS=100; wait_ready ctlwr2 ) 2> /dev/null || ctl_wr2_rc=$?
1245
+ end_launch ctlwr2
1246
+ assert_true 'Guard 2 control: wait_ready on a launch that died is rc 1 (a FAIL for its row), never the UNRESOLVED rc 2' test "$ctl_wr2_rc" -eq 1
1247
+ ctl_bd_rc=0; ( BAR_WAIT_TICKS=100; barrier_run ctlbd "$G2/h-ctlbd" 2 "$CTL_SLOT_FAIL"; printf '%s:%s:%s\n' "$BAR_RAN" "$BAR_DEAD" "$BAR_DISTINCT" > "$G2/ctlbd.out" ) 2> "$G2/ctlbd.msg" || ctl_bd_rc=$?
1248
+ ctl_bd_dead() { if [[ "$ctl_bd_rc" -eq 0 && "$(cat "$G2/ctlbd.out" 2> /dev/null)" == "0:2:0" ]] && grep -q '^FAIL: barrier launcher ctlbd-1 exited without recording a profile' "$G2/ctlbd.msg" && ! grep -q UNRESOLVED "$G2/ctlbd.msg"; then return 0; fi; return 1; }
1249
+ assert_true 'Guard 2 control: barrier_run counts launchers that exited without recording a profile (BAR_DEAD=2, BAR_RAN=0), names each one as a FAIL and does not stop the suite' ctl_bd_dead
1250
+ ctl_bh_rc=0; ( BAR_WAIT_TICKS=40; barrier_run ctlbh "$G2/h-ctlbh" 2 "$CTL_SLOT_HANG" ) 2> "$G2/ctlbh.msg" || ctl_bh_rc=$?
1251
+ ctl_bh_hang() { if [[ "$ctl_bh_rc" -eq 2 ]] && grep -q '^UNRESOLVED: only 0 of 2 barrier launches (ctlbh) recorded a profile within 4 s and the rest are still running: .*or the slot script hangs' "$G2/ctlbh.msg"; then return 0; fi; return 1; }
1252
+ assert_true 'Guard 2 control: barrier_run with launchers still running past the limit stops the suite with UNRESOLVED (rc 2) and names a hang as well as a loaded host' ctl_bh_hang
1253
+ BAR_N=12
1254
+ barrier_run bar "$G2/h-bar" "$BAR_N" "$REPO_ROOT/$SLOT_REL"
1255
+ assert_true "Guard 2: $BAR_N launches released at the same instant under one HOME resolve $BAR_N DISTINCT profiles (no two share a slot)" test "$BAR_RAN:$BAR_DISTINCT" = "$BAR_N:$BAR_N"
1256
+ assert_true 'Guard 2: those simultaneous launches printed ZERO "no usable flock" notes (flock is present, so a lost probe race must not take the no-lease branch)' test "$BAR_NOTES" -eq 0
1257
+ # the probe's own contention, made deterministic: a flock whose capability probe (-E 75 -w 0 8) answers 75, as util-linux does when another launch holds /dev/null
1258
+ PROBEBUSY="$G2/probe-busy-bin"; mkdir -p "$PROBEBUSY"
1259
+ printf '#!/usr/bin/env bash\nif [[ "$*" == "-E 75 -w 0 8" ]]; then exit 75; fi\nexec %q "$@"\n' "$(command -v flock)" > "$PROBEBUSY/flock"; chmod +x "$PROBEBUSY/flock"
1260
+ probe_busy_state() { [[ "$(PATH="$PROBEBUSY:$PATH" flock -E 75 -w 0 8 8< /dev/null; echo $?)" == 75 && "$(PATH="$PROBEBUSY:$PATH" flock -n -E 75 "$G2/hc-unlocked-probe" true; echo $?)" == 0 ]]; }
1261
+ : > "$G2/hc-unlocked-probe"
1262
+ assert_true 'Guard 2 harness: the probe-busy flock answers 75 to the capability probe only and runs every other flock call for real' probe_busy_state
1263
+ barrier_run pb "$G2/h-pb" 2 "$REPO_ROOT/$SLOT_REL" "PATH=$PROBEBUSY:$PATH"
1264
+ assert_true 'Guard 2: a capability probe that answers 75 (a concurrent probe held the lock) still means flock works: two launches get two leased profiles and no "no usable flock" note' test "$BAR_RAN:$BAR_DISTINCT:$BAR_NOTES" = "2:2:0"
1265
+ # the other side of the probe: a flock without -E support (busybox, a brew flock) rejects the probe with an error that is neither 0 nor 75
1266
+ # (rc 1 here) and must still read as unusable; every other call goes to the real flock, so only the probe can tell
1267
+ NOE="$G2/no-e-bin"; mkdir -p "$NOE"
1268
+ printf '#!/usr/bin/env bash\nfor a in "$@"; do if [[ "$a" == -E ]]; then exit 1; fi; done\nexec %q "$@"\n' "$(command -v flock)" > "$NOE/flock"; chmod +x "$NOE/flock"
1269
+ noe_state() { [[ "$(PATH="$NOE:$PATH" flock -E 75 -w 0 8 8< /dev/null; echo $?)" == 1 && "$(PATH="$NOE:$PATH" flock -n "$G2/hc-unlocked-probe" true; echo $?)" == 0 ]]; }
1270
+ assert_true 'Guard 2 harness: the no--E flock exits 1 for any argv containing -E and runs every other flock call for real' noe_state
1271
+ H="$G2/h-noe"
1272
+ LAUNCH_PATH="$NOE:$G2/bin:$PATH" run_n noe "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1273
+ noe_claim() { prof_is "$(prof0 "$H")" && grep -q 'no usable flock' "$G2/noe-1.err" && grep -q 'without a lease' "$G2/noe-1.err"; }
1274
+ assert_true 'Guard 2 must-PASS: a flock that errors out on -E reads as unusable: the "no usable flock" note, and slot 0 claimed without a lease' noe_claim
1275
+
1276
+ # --- all 32 slots busy: the launch falls back to a unique directory, never blocking, never killing ---
1277
+ H="$G2/h-full"; mkdir -p "$H/.cache"
1278
+ cat > "$HELP/holdn.py" <<'PY'
1279
+ import fcntl, os, sys, time
1280
+ base, lockname, ready, n = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
1281
+ fds = []
1282
+ for i in range(n):
1283
+ d = base if i == 0 else f"{base}-{i}"
1284
+ os.makedirs(d, exist_ok=True)
1285
+ fd = os.open(os.path.join(d, lockname), os.O_RDWR | os.O_CREAT, 0o644)
1286
+ fcntl.flock(fd, fcntl.LOCK_EX)
1287
+ fds.append(fd)
1288
+ open(ready, "w").write("ready")
1289
+ time.sleep(300)
1290
+ PY
1291
+ # hold_slots <home> <n> <ready-file>: a lock holder (own child) on slots 0..n-1 of <home>
1292
+ hold_slots() {
1293
+ mkdir -p "$1/.cache"
1294
+ python3 "$HELP/holdn.py" "$(prof0 "$1")" "$LOCKNAME" "$3" "$2" >/dev/null 2>&1 &
1295
+ own "$!"; disown "$!"
1296
+ local _i; for _i in $(seq 1 50); do if [[ -e "$3" ]]; then break; fi; sleep 0.1; done
1297
+ }
1298
+ hold_slots "$H" 32 "$G2/hold32.ready"
1299
+ run_n full "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1300
+ full_state() { local p b; p="$(head -n1 <<< "$RN_PROFS")"; b="$(prof0 "$H")"; if [[ "$RN_RAN" -eq 1 && "$p" =~ ^"$b"-p[0-9]+$ ]] && mode_is "$p" 700 && grep -q 'all 32 slots are busy' "$G2/full-1.err"; then return 0; fi; return 1; }
1301
+ assert_true 'Guard 2 must-PASS: with all 32 slots busy the launch starts on a unique $base-p<pid> directory (non-numeric infix, mode 700, note naming the cause)' full_state
1302
+ # slots >= 1 never wait: 8 busy slots with a 0.3 s slot-0 budget -> slot 8 in well under the 31 x 0.3 s a waiting loop would take
1303
+ H="$G2/h-timing"
1304
+ hold_slots "$H" 8 "$G2/hold8.ready"
1305
+ run_n timing "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1306
+ # timing_ok / timing2_ok are wall-clock rows with thin margins (a launch must resolve in under 1.5 s / 2.5 s while a waiting loop would take
1307
+ # over 2.7 s / 4 s). They are the only observable of "slots 1+ never wait", so they stay, but a host running at several times normal
1308
+ # latency (CPU oversubscription) can trip them without any defect in the slot script: rerun on an idle host before reading them as a regression.
1309
+ timing_ok() { prof_is "$(prof0 "$H")-8" && [[ "$RN_MS" -lt 1500 ]]; }
1310
+ assert_true 'Guard 2: slots 1+ never wait — eight busy slots resolve to slot 8 in under 1.5 s (a loop that waited on each slot would take over 2.7 s)' timing_ok
1311
+
1312
+ # slots >= 1 never wait on a live-Chrome SingletonLock either: slot 0 belongs to another session (no wait), slot 1 has a live Chrome and no
1313
+ # owner file (an unknown owner would be waited for if slots 1+ waited), so the launch resolves to slot 2 at once despite a 4 s budget
1314
+ H="$G2/h-timing2"; mkdir -p "$(prof0 "$H")" "$(prof0 "$H")-1"
1315
+ printf '%s\n' "$OTHER_SESSION" > "$(prof0 "$H")/.pwslot.owner"
1316
+ start_chrome_owner; T2A=$SPAWNED; start_chrome_owner; T2B=$SPAWNED
1317
+ ln -s "$HOST_NOW-$T2A" "$(prof0 "$H")/SingletonLock"; ln -s "$HOST_NOW-$T2B" "$(prof0 "$H")-1/SingletonLock"
1318
+ run_n timing2 "$REPO_ROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=4
1319
+ # (same wall-clock caveat as timing_ok above)
1320
+ timing2_ok() { prof_is "$(prof0 "$H")-2" && [[ "$RN_MS" -lt 2500 ]]; }
1321
+ assert_true 'Guard 2: slots 1+ never wait on a live-Chrome SingletonLock — slots 0 and 1 held that way resolve to slot 2 in under 2.5 s with a 4 s budget' timing2_ok
1322
+
1323
+ # --- sourced-script hygiene: nothing leaks into the exec'd shell, no errexit/nounset, fd 9 stays open ---
1324
+ H="$G2/h-hygiene"; mkdir -p "$H" "$G2/hyg"
1325
+ ( cd "$REPO_ROOT" && HOME="$H" PW_PROFILE_SLOT0_WAIT_S=0.3 bash -c ': | :; compgen -v | sort > "$1/v0"; declare -F | sort > "$1/f0"; export -p | grep -v "^declare -x _=" | sort > "$1/e0"; . "$2" || exit 1; compgen -v | sort > "$1/v1"; declare -F | sort > "$1/f1"; export -p | grep -v "^declare -x _=" | sort > "$1/e1"; printf "flags=%s fd9=%s\n" "$-" "$([ -e /proc/self/fd/9 ] && echo open || echo closed)"' _ "$G2/hyg" "$SLOT_REL" ) > "$G2/hyg/out" 2>&1 || true
1326
+ hyg_flags() { if [[ "$(cat "$G2/hyg/out")" =~ ^flags=([a-zA-Z]*)\ fd9=open$ ]] && [[ "${BASH_REMATCH[1]}" != *e* && "${BASH_REMATCH[1]}" != *u* ]]; then return 0; fi; return 1; }
1327
+ assert_true 'Guard 2: after sourcing, errexit/nounset are off and fd 9 (the lease) is open in the surviving shell' hyg_flags
1328
+ assert_true 'Guard 2: sourcing adds no shell function (declare -F before and after are identical)' cmp -s "$G2/hyg/f0" "$G2/hyg/f1"
1329
+ assert_true 'Guard 2: sourcing exports nothing (export -p before and after are identical)' cmp -s "$G2/hyg/e0" "$G2/hyg/e1"
1330
+ only_prof_new() { [[ "$(comm -13 "$G2/hyg/v0" "$G2/hyg/v1")" == "prof" && -z "$(comm -23 "$G2/hyg/v0" "$G2/hyg/v1")" ]]; }
1331
+ assert_true 'Guard 2: sourcing adds exactly one shell variable, prof (compgen -v before and after differ by that name only)' only_prof_new
1332
+
1333
+ # --- Guard 2 mutation matrix (every edit lands exactly once in the pristine script; each row has a pristine counterpart above) ---
1334
+ A_END='for _pwslot_v in $(compgen -A function _pwslot_); do unset -f "$_pwslot_v"; done'
1335
+ # 1 — the contention flock deleted: every launch lands on slot 0
1336
+ slot_mutant 1-no-flock 'flock -E 75 -n 9' 'true'
1337
+ if [[ -n "$MROOT" ]]; then
1338
+ run_n g2m1 "$MROOT" "$G2/h-m1" "$MCP_ARGS" 2 overlap "$FAST"
1339
+ m1_shared() { if [[ "$RN_RAN" -eq 2 ]] && ! rn_distinct 2; then return 0; fi; return 1; }
1340
+ red 'Guard 2 mutant 1: the flock deleted → two launches share one --user-data-dir' m1_shared
1341
+ fi
1342
+ # 2 — a pattern kill of the decoy re-added at the end of the script
1343
+ slot_mutant 2-pkill-decoy "$A_END" 'pkill -9 -f "[c]hrome.*$prof" 2>/dev/null || true; '"$A_END"
1344
+ if [[ -n "$MROOT" ]]; then
1345
+ H="$G2/h-m2"; spawn_decoys "$H"
1346
+ run_n g2m2 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1347
+ m2_decoy_dead() { if [[ "$RN_RAN" -eq 1 ]] && wait_dead "$D_SLOT0" 3; then return 0; fi; return 1; }
1348
+ red 'Guard 2 mutant 2: a pkill of chrome.*<slot0> re-added → the decoy dies' m2_decoy_dead
1349
+ fi
1350
+ # 3 — a live-owner SingletonLock treated as stale (the owner-alive test inverted): the lock is removed
1351
+ slot_mutant 3-live-owner-stale 'if [ "$_pwslot_alive" -eq 0 ]; then return 1; fi' 'if [ "$_pwslot_alive" -ne 0 ]; then return 1; fi'
1352
+ if [[ -n "$MROOT" ]]; then
1353
+ H="$G2/h-m3"; mkdir -p "$(prof0 "$H")"; start_chrome_owner; OWNER=$SPAWNED; ln -s "$HOST_NOW-$OWNER" "$(prof0 "$H")/SingletonLock"
1354
+ run_n g2m3 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1355
+ m3_lock_gone() { if [[ "$RN_RAN" -eq 1 ]] && no_link "$(prof0 "$H")"; then return 0; fi; return 1; }
1356
+ red 'Guard 2 mutant 3: a live-owner SingletonLock treated as stale → the lock file is removed' m3_lock_gone
1357
+ fi
1358
+ # 4 — exhaustion made non-unique (two slots, then slot 0 is reused): a third concurrent launch collides
1359
+ slot_mutant 4-third-collides '_pwslot_max=32' '_pwslot_max=2' '_pwslot_fallback="$_pwslot_base-p$$"' '_pwslot_fallback="$_pwslot_base"'
1360
+ if [[ -n "$MROOT" ]]; then
1361
+ run_n g2m4 "$MROOT" "$G2/h-m4" "$MCP_ARGS" 3 overlap "$FAST"
1362
+ m4_collides() { if [[ "$RN_RAN" -eq 3 ]] && ! rn_distinct 3; then return 0; fi; return 1; }
1363
+ red 'Guard 2 mutant 4: the third concurrent launch collides after two compliant ones → fewer than three distinct directories' m4_collides
1364
+ fi
1365
+ # 5 — dispatch: the slot script is not sourced (the string reduced to the old literal profile)
1366
+ cases=$((cases + 1)); mutants_declared=$((mutants_declared + 1))
1367
+ M5_ARGS="$(python3 - "$MCP_ARGS" "$SLOT_REL" <<'PY'
1368
+ import re, sys
1369
+ s, slot = sys.argv[1:]
1370
+ print(re.sub(r"\.\s+" + re.escape(slot) + r"\s*\|\|\s*exit 1;", "prof=$HOME/.cache/playwright-mcp-profile;", s))
1371
+ PY
1372
+ )"
1373
+ if [[ "$M5_ARGS" != "$MCP_ARGS" && "$M5_ARGS" == *'prof=$HOME/.cache/playwright-mcp-profile;'* ]]; then
1374
+ ok 'Guard 2 mutant 5-no-source landed'
1375
+ H="$G2/h-m5"
1376
+ run_n g2m5 "$REPO_ROOT" "$H" "$M5_ARGS" 2 overlap "$FAST"
1377
+ m5_unslotted() { if [[ "$RN_RAN" -eq 2 ]] && ! rn_distinct 2 && ! [[ -e "$(prof0 "$H")/$LOCKNAME" ]]; then return 0; fi; return 1; }
1378
+ red 'Guard 2 mutant 5: the slot script not sourced → both launches share the profile and no slot lock exists' m5_unslotted
1379
+ else
1380
+ bad 'Guard 2 mutant 5-no-source did NOT land'
1381
+ fi
1382
+ # 6 (harness) — the two launches run sequentially: the overlap proof must be FALSE, proving it can tell the difference
1383
+ run_n g2m6 "$REPO_ROOT" "$G2/h-m6" "$MCP_ARGS" 2 sequential "$FAST"
1384
+ m6_no_overlap() { if [[ "$RN_RAN" -eq 2 ]] && ! rn_overlap; then return 0; fi; return 1; }
1385
+ red 'Guard 2 mutant 6 (harness): launches run sequentially instead of overlapped → the overlap proof fails (it observes liveness and held locks)' m6_no_overlap
1386
+ # 7 — the owner-alive check deleted: a free lock with a live-owner SingletonLock reuses the slot
1387
+ slot_mutant 7-no-owner-check 'elif ! _pwslot_owner_busy "$_pwslot_tg"; then' 'elif true; then'
1388
+ if [[ -n "$MROOT" ]]; then
1389
+ H="$G2/h-m7"; mkdir -p "$(prof0 "$H")"; start_chrome_owner; OWNER=$SPAWNED; ln -s "$HOST_NOW-$OWNER" "$(prof0 "$H")/SingletonLock"
1390
+ run_n g2m7 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1391
+ red 'Guard 2 mutant 7: a free lock but a live-owner SingletonLock reuses the slot → the launch lands on the owned profile' prof_is "$(prof0 "$H")"
1392
+ fi
1393
+ # 8 — kills every OTHER slot's Chrome (a pattern held in a variable, so Guard 1's text scan cannot see it)
1394
+ slot_mutant 8-kill-other-slot "$A_END" '_pwslot_pat="chrome.*$_pwslot_base-[0-9]"; pkill -9 -f "$_pwslot_pat"; '"$A_END"
1395
+ if [[ -n "$MROOT" ]]; then
1396
+ H="$G2/h-m8"; spawn_decoys "$H"
1397
+ run_n g2m8 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1398
+ m8_other_dead() { if [[ "$RN_RAN" -eq 1 ]] && wait_dead "$D_OTHER" 3; then return 0; fi; return 1; }
1399
+ red 'Guard 2 mutant 8: a pkill of every other slot'"'"'s Chrome → the other-slot decoy dies' m8_other_dead
1400
+ fi
1401
+ # 9 — kills sibling Playwright servers
1402
+ slot_mutant 9-kill-sibling-server "$A_END" 'pkill -9 -f "bin/playwright-mcp.*$HOME"; '"$A_END"
1403
+ if [[ -n "$MROOT" ]]; then
1404
+ H="$G2/h-m9"; spawn_decoys "$H"
1405
+ run_n g2m9 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1406
+ m9_srv_dead() { if [[ "$RN_RAN" -eq 1 ]] && wait_dead "$D_SRV" 3; then return 0; fi; return 1; }
1407
+ red 'Guard 2 mutant 9: a pkill of bin/playwright-mcp servers → the sibling-server decoy dies' m9_srv_dead
1408
+ fi
1409
+ # 10 — kills whatever holds a file open inside the claimed profile (one bounded /proc fd selector: no pkill, no pgrep, no process name;
1410
+ # a per-process readlink walk costs ~18 s at ~600 host processes and outlasts the launch window, so the selector must not scale with the host)
1411
+ slot_mutant 10-kill-fd-holders "$A_END" 'for _pwslot_h in $(find /proc/[0-9]*/fd -maxdepth 1 -lname "$prof/*" 2>/dev/null | cut -d/ -f3); do if [ "$_pwslot_h" != "$$" ]; then kill -9 "$_pwslot_h"; fi; done; '"$A_END"
1412
+ if [[ -n "$MROOT" ]]; then
1413
+ H="$G2/h-m10"; spawn_decoys "$H"
1414
+ run_n g2m10 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1415
+ m10_fd_dead() { if [[ "$RN_RAN" -eq 1 ]] && wait_dead "$D_FD" 3; then return 0; fi; return 1; }
1416
+ red 'Guard 2 mutant 10: a kill of every process holding a file open in the claimed slot → the file-holding decoy dies' m10_fd_dead
1417
+ fi
1418
+ # 11 — the hostname field ignored: a lock naming another host is judged by the local pid alone
1419
+ slot_mutant 11-foreign-host 'if [ -z "$_pwslot_me" ] || [ "${_pwslot_t%-*}" != "$_pwslot_me" ]; then' 'if false; then'
1420
+ if [[ -n "$MROOT" ]]; then
1421
+ H="$G2/h-m11"; mkdir -p "$(prof0 "$H")"; ln -s "otherhost-$dead" "$(prof0 "$H")/SingletonLock"
1422
+ run_n g2m11 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1423
+ m11_removed() { if [[ "$RN_RAN" -eq 1 ]] && no_link "$(prof0 "$H")"; then return 0; fi; return 1; }
1424
+ red 'Guard 2 mutant 11: the hostname ignored → a foreign host'"'"'s lock with a locally dead pid is removed' m11_removed
1425
+ fi
1426
+ # 12 — any live pid counts as a Chrome owner (the pre-review behaviour): a recycled pid blocks slot 0
1427
+ slot_mutant 12-any-live-pid "*[Cc]hrom* | *headless_shell* | msedge* | [Mm]icrosoft\\ [Ee]dge* | [Bb]rave* | [Vv]ivaldi* | [Oo]pera | [Oo]pera-* | [Oo]pera\\ * | '')" '*)'
1428
+ if [[ -n "$MROOT" ]]; then
1429
+ H="$G2/h-m12"; mkdir -p "$(prof0 "$H")"; start_sleeper; RECYCLED=$SPAWNED; ln -s "$HOST_NOW-$RECYCLED" "$(prof0 "$H")/SingletonLock"
1430
+ run_n g2m12 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1431
+ red 'Guard 2 mutant 12: every live pid counts as a Chrome owner → a recycled non-Chrome pid pushes the launch off slot 0' prof_is "$(prof0 "$H")-1"
1432
+ fi
1433
+ # 13 — liveness by ps alone (the fail-OPEN probe the review found): with ps absent every owner is judged dead
1434
+ slot_mutant 13-ps-fails-open $' _pwslot_alive=1\n if ! [ -d "/proc/$_pwslot_p" ]; then' $' _pwslot_alive=0\n if ps -p "$_pwslot_p" >/dev/null 2>&1; then _pwslot_alive=1; fi\n if false; then'
1435
+ if [[ -n "$MROOT" ]]; then
1436
+ H="$G2/h-m13"; mkdir -p "$(prof0 "$H")"; start_chrome_owner; OWNER=$SPAWNED; ln -s "$HOST_NOW-$OWNER" "$(prof0 "$H")/SingletonLock"
1437
+ LAUNCH_PATH="$NOPS" run_n g2m13 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1438
+ m13_removed() { if [[ "$RN_RAN" -eq 1 ]] && no_link "$(prof0 "$H")"; then return 0; fi; return 1; }
1439
+ red 'Guard 2 mutant 13: liveness decided by ps alone, ps absent → a live Chrome'"'"'s lock is removed (the fail-open defect)' m13_removed
1440
+ fi
1441
+ # 14 — the owner file ignored: every contended slot-0 launch waits, whoever holds it
1442
+ slot_mutant 14-owner-ignored '[ "$_pwslot_ow" = "$_pwslot_self" ]' 'true'
1443
+ if [[ -n "$MROOT" ]]; then
1444
+ H="$G2/h-m14"; hold_lock "$(prof0 "$H")" 9 "$OTHER_SESSION"
1445
+ run_n g2m14 "$MROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=4
1446
+ m14_waited() { [[ "$RN_RAN" -eq 1 && "$RN_MS" -ge 2500 ]]; }
1447
+ red 'Guard 2 mutant 14: the owner file ignored → a different session'"'"'s lock is waited for the full budget' m14_waited
1448
+ fi
1449
+ # 15 — slots >= 1 wait too
1450
+ slot_mutant 15-slots-wait 'if [ "$_pwslot_n" -eq 0 ] && _pwslot_owner_wait "$_pwslot_d"; then' 'if _pwslot_owner_wait "$_pwslot_d"; then'
1451
+ if [[ -n "$MROOT" ]]; then
1452
+ H="$G2/h-m15"
1453
+ hold_slots "$H" 8 "$G2/hold8m.ready"
1454
+ run_n g2m15 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1455
+ m15_slow() { [[ "$RN_RAN" -eq 1 && "$RN_MS" -ge 1500 ]]; }
1456
+ red 'Guard 2 mutant 15: slots 1+ wait on their locks → the eight-busy launch takes 1.5 s or more' m15_slow
1457
+ fi
1458
+ # 16 — one unusable slot aborts the whole search
1459
+ slot_mutant 16-rc2-aborts 'if [ "$_pwslot_i" -eq 0 ] || [ "$_pwslot_flockbad" -eq 1 ]; then break; fi' 'break'
1460
+ if [[ -n "$MROOT" ]]; then
1461
+ H="$G2/h-m16"; hold_lock "$(prof0 "$H")" 30 "$OTHER_SESSION"; : > "$(prof0 "$H")-1"
1462
+ run_n g2m16 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1463
+ m16_fallback() { [[ "$RN_RAN" -eq 1 && "$(head -n1 <<< "$RN_PROFS")" =~ ^"$(prof0 "$H")"-p[0-9]+$ ]]; }
1464
+ red 'Guard 2 mutant 16: an unusable slot ends the search → the launch lands on the unique fallback instead of slot 2' m16_fallback
1465
+ fi
1466
+ # 17 — PW_PROFILE_SLOT0_WAIT_S not validated: a typo reaches flock and degrades the lease
1467
+ slot_mutant 17-wait-unvalidated "'' | . | *[!0-9.]* | *.*.*)" '__never__)'
1468
+ if [[ -n "$MROOT" ]]; then
1469
+ H="$G2/h-m17"; hold_lock "$(prof0 "$H")" 2 "$$"
1470
+ run_n g2m17 "$MROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=abc
1471
+ m17_degraded() { [[ "$RN_RAN" -eq 1 && "$(head -n1 <<< "$RN_PROFS")" =~ ^"$(prof0 "$H")"-p[0-9]+$ ]]; }
1472
+ red 'Guard 2 mutant 17: the wait not validated → a non-numeric value sends the launch to the unique fallback' m17_degraded
1473
+ fi
1474
+ # 18 — the no-flock host loses the persistent profile (the pre-review behaviour)
1475
+ slot_mutant 18-noflock-no-slot0 $'if [ -z "$_pwslot_skip0" ]; then\n rm -f' $'if false; then\n rm -f'
1476
+ if [[ -n "$MROOT" ]]; then
1477
+ H="$G2/h-m18"
1478
+ LAUNCH_PATH="$NOFLOCK" run_n g2m18 "$MROOT" "$H" "$MCP_ARGS" 1 overlap
1479
+ m18_lost() { [[ "$RN_RAN" -eq 1 && "$(head -n1 <<< "$RN_PROFS")" =~ ^"$(prof0 "$H")"-p[0-9]+$ ]]; }
1480
+ red 'Guard 2 mutant 18: no slot-0 claim without flock → the first session on a no-flock host gets a unique empty profile' m18_lost
1481
+ fi
1482
+ # 19 — the fallback name aliases a slot directory
1483
+ slot_mutant 19-fallback-aliases '_pwslot_fallback="$_pwslot_base-p$$"' '_pwslot_fallback="$_pwslot_base-$$"'
1484
+ if [[ -n "$MROOT" ]]; then
1485
+ H="$G2/h-m19"
1486
+ hold_slots "$H" 32 "$G2/hold32m.ready"
1487
+ run_n g2m19 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1488
+ m19_numeric() { [[ "$RN_RAN" -eq 1 && "$(head -n1 <<< "$RN_PROFS")" =~ ^"$(prof0 "$H")"-[0-9]+$ ]]; }
1489
+ red 'Guard 2 mutant 19: the fallback named $base-$$ → its name is a number a slot could also have' m19_numeric
1490
+ fi
1491
+ # 20 — the lease leaks into a child (a lingering process holding fd 9): a SIGKILL of the proxy no longer frees the slot
1492
+ slot_mutant 20-lease-leaks "$A_END" 'sleep 3 9>&9 & '"$A_END"
1493
+ if [[ -n "$MROOT" ]]; then
1494
+ H="$G2/h-m20"
1495
+ launch g2m20 "$MROOT" "$H" "$MCP_ARGS" "$FAST"
1496
+ m20_ready=0; if wait_ready g2m20; then m20_ready=1; fi
1497
+ m20_proxy="$(cat "$G2/g2m20.pid")"
1498
+ kill -KILL "$m20_proxy" 2>/dev/null || true
1499
+ wait_dead "$m20_proxy" 3 || true
1500
+ m20_still_held() { if [[ "$m20_ready" -eq 1 ]] && lock_held "$(prof0 "$H")/$LOCKNAME"; then return 0; fi; return 1; }
1501
+ red 'Guard 2 mutant 20: a child inherits the lease → after a SIGKILL of the proxy the slot lock is still held' m20_still_held
1502
+ end_launch g2m20
1503
+ sleep 3.2 # the leaked holder is a 3 s sleep the mutated script started; let it expire
1504
+ fi
1505
+ # 21 — the default wait lowered, the old token kept in a trailing comment (a grep over the whole file still finds it)
1506
+ slot_mutant 21-default-3 'PW_PROFILE_SLOT0_WAIT_S:-7}"' 'PW_PROFILE_SLOT0_WAIT_S:-3}" # PW_PROFILE_SLOT0_WAIT_S:-7}'
1507
+ if [[ -n "$MROOT" ]]; then
1508
+ m21_default_gone() { ! default_wait_is "$MROOT/$SLOT_REL" 7; }
1509
+ red 'Guard 2 mutant 21: the default lowered to 3 with the old token left in a comment → the comment-stripped default row fails' m21_default_gone
1510
+ m21_pair_broken() { ! wait_pairs_grace "$MROOT/$SLOT_REL" "$REPO_ROOT/$PROXY_REL_PATH"; }
1511
+ red 'Guard 2 mutant 21: the same lowered default no longer covers the proxy teardown → the pairing row fails' m21_pair_broken
1512
+ fi
1513
+ # 22 — the capability probe read strictly (rc 0 only): a probe that loses the shared /dev/null lock (rc 75) means "no usable flock"
1514
+ slot_mutant 22-probe-strict $' 0 | 75) return 0 ;;' $' 0) return 0 ;;'
1515
+ if [[ -n "$MROOT" ]]; then
1516
+ # Only the deterministic form is a mutation row. A "real race" row (the strict probe among $BAR_N simultaneous launches) is red only
1517
+ # when the race actually happens: on a loaded host about one trial in four stays clean and the suite would go red for a reason that is
1518
+ # not in the code. The real-tree $BAR_N-barrier rows above prove the fixed probe; this row proves the strict one dies.
1519
+ barrier_run g2m22b "$G2/h-m22b" 2 "$MROOT/$SLOT_REL" "PATH=$PROBEBUSY:$PATH"
1520
+ m22_probe_busy() { [[ "$BAR_RAN" -eq 2 && "$BAR_NOTES" -ge 1 ]]; }
1521
+ red 'Guard 2 mutant 22: the strict rc-0 probe, a probe answering 75 → "no usable flock" is printed and the launch takes the no-lease branch (the deterministic form)' m22_probe_busy
1522
+ fi
1523
+ # 23 — the headless build dropped from the Chrome-owner comm list: a live headless_shell owner reads as a recycled pid
1524
+ slot_mutant 23-comm-no-headless '*headless_shell* | ' ''
1525
+ if [[ -n "$MROOT" ]]; then
1526
+ H="$G2/h-m23"; mkdir -p "$(prof0 "$H")"; start_comm_owner headless_shell; OWNER=$SPAWNED; ln -s "$HOST_NOW-$OWNER" "$(prof0 "$H")/SingletonLock"
1527
+ run_n g2m23 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1528
+ m23_removed() { if [[ "$RN_RAN" -eq 1 ]] && no_link "$(prof0 "$H")"; then return 0; fi; return 1; }
1529
+ red 'Guard 2 mutant 23: headless_shell dropped from the owner comm list → a live headless owner'"'"'s lock is removed' m23_removed
1530
+ fi
1531
+ # 24 — the Chromium derivatives dropped from the comm list: a live msedge owner reads as a recycled pid
1532
+ slot_mutant 24-comm-no-derivatives 'msedge* | [Mm]icrosoft\ [Ee]dge* | [Bb]rave* | [Vv]ivaldi* | [Oo]pera | [Oo]pera-* | [Oo]pera\ * | ' ''
1533
+ if [[ -n "$MROOT" ]]; then
1534
+ H="$G2/h-m24"; mkdir -p "$(prof0 "$H")"; start_comm_owner msedge; OWNER=$SPAWNED; ln -s "$HOST_NOW-$OWNER" "$(prof0 "$H")/SingletonLock"
1535
+ run_n g2m24 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1536
+ m24_removed() { if [[ "$RN_RAN" -eq 1 ]] && no_link "$(prof0 "$H")"; then return 0; fi; return 1; }
1537
+ red 'Guard 2 mutant 24: the Chromium derivatives dropped from the owner comm list → a live msedge owner'"'"'s lock is removed' m24_removed
1538
+ fi
1539
+ # 25 — slots >= 1 wait in the SingletonLock re-check loop (the slot-0-only guard dropped there; mutant 15 covers the flock path)
1540
+ slot_mutant 25-recheck-waits 'if [ "$_pwslot_n" -eq 0 ] && [ "$_pwslot_waitok" -eq 0 ] && [ "$_pwslot_left" -lt 0 ]; then' 'if [ "$_pwslot_waitok" -eq 0 ] && [ "$_pwslot_left" -lt 0 ]; then'
1541
+ if [[ -n "$MROOT" ]]; then
1542
+ H="$G2/h-m25"; mkdir -p "$(prof0 "$H")" "$(prof0 "$H")-1"
1543
+ printf '%s\n' "$OTHER_SESSION" > "$(prof0 "$H")/.pwslot.owner"
1544
+ start_chrome_owner; M25A=$SPAWNED; start_chrome_owner; M25B=$SPAWNED
1545
+ ln -s "$HOST_NOW-$M25A" "$(prof0 "$H")/SingletonLock"; ln -s "$HOST_NOW-$M25B" "$(prof0 "$H")-1/SingletonLock"
1546
+ run_n g2m25 "$MROOT" "$H" "$MCP_ARGS" 1 overlap PW_PROFILE_SLOT0_WAIT_S=4
1547
+ m25_slow() { [[ "$RN_RAN" -eq 1 && "$RN_MS" -ge 2500 ]]; }
1548
+ red 'Guard 2 mutant 25: slots 1+ wait in the SingletonLock re-check → the launch takes 2.5 s or more' m25_slow
1549
+ fi
1550
+ # 26 — the probe accepts any exit status: a flock that errors out on -E (busybox) reads as usable, so the lease path runs on a flock that cannot take it
1551
+ slot_mutant 26-probe-any-rc $' 0 | 75) return 0 ;;' $' *) return 0 ;;'
1552
+ if [[ -n "$MROOT" ]]; then
1553
+ H="$G2/h-m26"
1554
+ LAUNCH_PATH="$NOE:$G2/bin:$PATH" run_n g2m26 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1555
+ m26_lease_path() { [[ "$RN_RAN" -eq 1 && "$(head -n1 <<< "$RN_PROFS")" =~ ^"$(prof0 "$H")"-p[0-9]+$ ]]; }
1556
+ red 'Guard 2 mutant 26: any probe status reads as usable → a flock without -E takes the lease path and the launch lands on the unique fallback instead of slot 0' m26_lease_path
1557
+ fi
1558
+ # 27 — the derivative globs unanchored (substrings again): a recycled pid named operator reads as an Opera
1559
+ slot_mutant 27-comm-substring '[Oo]pera | [Oo]pera-* | [Oo]pera\ * | ' '*[Oo]pera* | '
1560
+ if [[ -n "$MROOT" ]]; then
1561
+ H="$G2/h-m27"; mkdir -p "$(prof0 "$H")"; start_comm_owner operator; OWNER=$SPAWNED; ln -s "$HOST_NOW-$OWNER" "$(prof0 "$H")/SingletonLock"
1562
+ run_n g2m27 "$MROOT" "$H" "$MCP_ARGS" 1 overlap "$FAST"
1563
+ red 'Guard 2 mutant 27: the derivative globs unanchored → a recycled pid named operator pushes the launch off slot 0' prof_is "$(prof0 "$H")-1"
1564
+ fi
1565
+
1566
+ # ===========================================================================
1567
+ # Guard 3 — the registrations
1568
+ # ===========================================================================
1569
+ assert_true 'Guard 3: .mcp.json carries "env": {"PLAYWRIGHT_MCP_PING_TIMEOUT_MS": "0"} (a guard for future pin bumps: inert on stdio in 0.0.78/0.0.83)' reg_ok "$PROJECT_MCP" project
1570
+ assert_true 'Guard 3: plugins/soleur/.mcp.json carries the same env and a single --chromium-fallback before the -- separator' reg_ok "$PLUGIN_MCP" plugin
1571
+ reg_mutant 'Guard 3 mutant 1: the project registration loses its env' "$PROJECT_MCP" project 'e.pop("env", None)'
1572
+ reg_mutant 'Guard 3 mutant 2: the project env value drifts from 0' "$PROJECT_MCP" project 'e["env"] = {"PLAYWRIGHT_MCP_PING_TIMEOUT_MS": "30000"}'
1573
+ reg_mutant 'Guard 3 mutant 3: the plugin registration loses its env' "$PLUGIN_MCP" plugin 'e.pop("env", None)'
1574
+ reg_mutant 'Guard 3 mutant 4: the plugin env key is misspelled' "$PLUGIN_MCP" plugin 'e["env"] = {"PLAYWRIGHT_MCP_PING_TIMEOUT": "0"}'
1575
+ reg_mutant 'Guard 3 mutant 5: the plugin registration loses --chromium-fallback' "$PLUGIN_MCP" plugin 'e["args"].remove("--chromium-fallback")'
1576
+
1577
+ # ---------------------------------------------------------------------------
1578
+ # Harness hygiene: every process this suite started is gone after reap_own, and the hygiene predicate can tell
1579
+ # ---------------------------------------------------------------------------
1580
+ reap_own
1581
+ # own_all_dead <file of pids>: none of the recorded pids is alive
1582
+ own_all_dead() { local p; while read -r p; do if [[ -n "$p" ]] && alive "$p"; then return 1; fi; done < "$1"; return 0; }
1583
+ npx_pids_dead() { local p; while read -r p; do if [[ -n "$p" ]] && alive "$p"; then return 1; fi; done < <(cat "$WORK"/g2/*.jsonl.pid 2>/dev/null || true); return 0; }
1584
+ assert_true 'Hygiene: every pid this suite recorded is dead after reap_own' own_all_dead "$OWN"
1585
+ assert_true 'Hygiene: every npx-shim server pid this suite started is dead after reap_own' npx_pids_dead
1586
+ # mutation (harness): a neutered reaper leaves the recorded pid alive, so the predicate above is false — it can tell
1587
+ leak_observed() {
1588
+ local f="$WORK/hc-leak-own" pid
1589
+ : > "$f"
1590
+ sleep 300 >/dev/null 2>&1 & pid=$!; printf '%s\n' "$pid" >> "$f"; disown "$pid"
1591
+ # the neutered reaper does nothing here
1592
+ if own_all_dead "$f"; then kill -KILL "$pid" 2>/dev/null || true; return 1; fi
1593
+ reap_list "$f"
1594
+ wait_dead "$pid" 3 || true
1595
+ own_all_dead "$f"
1596
+ }
1597
+ red 'Hygiene mutant (harness): a neutered reap_own leaves a recorded pid alive, so own_all_dead is false; the real reaper then clears it' leak_observed
1598
+
1599
+ # ---------------------------------------------------------------------------
1600
+ # Verdict. Reported with printf + exit, never through ok()/bad() (ADR-193).
1601
+ # ---------------------------------------------------------------------------
1602
+ printf '\n%d passed, %d failed, %d cases (%d mutants, %d mutation rows)\n' "$pass" "$fail" "$cases" "$mutants_declared" "$red_rows"
1603
+
1604
+ if [[ $((pass + fail)) -ne $cases ]]; then
1605
+ printf '[FATAL] vacuity accounting: pass+fail (%d) != cases (%d) — a row did not report\n' "$((pass + fail))" "$cases" >&2
1606
+ exit 1
1607
+ fi
1608
+ EXPECTED_MUTANTS=57 # Guard 1: 22 static + 4 dynamic mutants; Guard 2: mutants 1-5 and 7-27 (6 is a harness row); Guard 3: mutants 1-5
1609
+ EXPECTED_RED_ROWS=61 # the 57 mutants, one extra row for Guard 2 mutant 21 (mutant 22 has the one deterministic row), plus the three harness rows (Guard 1, Guard 2 #6, Hygiene)
1610
+ if [[ $mutants_declared -ne $EXPECTED_MUTANTS || $red_rows -ne $EXPECTED_RED_ROWS ]]; then
1611
+ printf '[FATAL] mutation matrix: %d mutants / %d mutation rows ran, expected %d / %d — a row vanished\n' "$mutants_declared" "$red_rows" "$EXPECTED_MUTANTS" "$EXPECTED_RED_ROWS" >&2
1612
+ exit 1
1613
+ fi
1614
+ MIN_ASSERTIONS=203
1615
+ if [[ $cases -lt $MIN_ASSERTIONS ]]; then
1616
+ printf '[FATAL] vacuity floor: only %d cases executed, expected at least %d\n' "$cases" "$MIN_ASSERTIONS" >&2
1617
+ exit 1
1618
+ fi
1619
+ [[ $fail -eq 0 ]] || exit 1
1620
+ exit 0