@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,964 @@
1
+ #!/usr/bin/env python3
2
+ """Redacting stdio JSON-RPC proxy in front of the Playwright MCP server (#7980).
3
+
4
+ python3 playwright-mcp-redact-proxy.py [--chromium-fallback] [--user-data-dir-name <basename>] -- npx @playwright/mcp@0.0.78 [server args...]
5
+
6
+ `--chromium-fallback` (a boolean, given at most once) appends `--browser chromium --sandbox` to the server
7
+ argv, ahead of the proxy's own appended flags, when ALL of: the server argv carries no `--browser`,
8
+ $PLAYWRIGHT_MCP_BROWSER is unset, the platform is Linux or macOS, and no Google Chrome executable
9
+ exists at the platform's known path (`/opt/google/chrome/chrome`, `/Applications/Google
10
+ Chrome.app/Contents/MacOS/Google Chrome`). Without it the server defaults to the `chrome` channel and
11
+ a host with no Google Chrome gets no browser at all; with it Playwright maps `chromium` to its bundled
12
+ build, and `--sandbox` keeps that build's sandbox ON (Playwright defaults it off on Linux for the bundled
13
+ build); a host that cannot run the sandbox fails to launch rather than run unsandboxed. The appended
14
+ `--browser chromium` also overrides a browser chosen in an explicit `--config` /
15
+ $PLAYWRIGHT_MCP_CONFIG file (only argv and env are consulted, and the CLI wins over the file) on a
16
+ Chrome-less host. On any other platform nothing is appended. $PLAYWRIGHT_MCP_PROXY_CHROME_PATHS (an os.pathsep
17
+ list) replaces the known-path list, for the suite. `--browser` is not a sink flag and
18
+ `--executable-path` stays refused, and `--no-sandbox`, $PLAYWRIGHT_MCP_SANDBOX and a config
19
+ `chromiumSandbox: false` stay refused (a caller-supplied `--sandbox` stays allowed: it can only turn the
20
+ sandbox on), so no guarantee below moves. Flag absent: the child argv is exactly
21
+ what it was before.
22
+
23
+ `--user-data-dir-name` resolves its basename under $XDG_CACHE_HOME (else
24
+ `$HOME/.cache`, both read from os.environ) and injects
25
+ `--user-data-dir=<absolute path>` into the server argv, giving the wrapped
26
+ browser its own profile directory without a literal path in the registration.
27
+ The flag refuses to start on a basename given twice, empty, `.`, containing
28
+ `..` or a path separator, or starting with `-`; on a basename combined with an
29
+ explicit `--user-data-dir` in the server argv; on a relative
30
+ $XDG_CACHE_HOME/$HOME; and when neither variable is set. The resolved root is
31
+ normalized with os.path.realpath, mirroring the XDG semantics of
32
+ scripts/lib/scratch-root.sh. With the flag absent the child argv is exactly
33
+ what it was before.
34
+
35
+ Sits on the transport between Claude Code and the server and rewrites the text of
36
+ every `tools/call` result through the SAME `redact_text` the agent-browser Bash
37
+ path uses (loaded by path from the sibling `redact-a11y-snapshot.py`; this file
38
+ defines no predicate of its own). Three fail-closed arms, no bypass variable:
39
+
40
+ * startup -- `refuse_argv_and_env`, `load_redactor`, `self_test`: stderr
41
+ `refusing to start: <reason>`, exit 2, no child spawned (these three catch
42
+ Exception, SystemExit and KeyboardInterrupt; a BaseException subclass outside
43
+ them is unreachable here and would die with exit 1 and a traceback before any
44
+ child is spawned, so nothing is relayed);
45
+ * per call -- `refuse_request` / `rewrite_result`: an `isError` result naming the
46
+ tool, built by `error_result`, whose reason never quotes input;
47
+ * transport -- `teardown`: the child's whole process group is ended on stdin
48
+ EOF, a signal, or child exit, with the child's exit code clamped.
49
+
50
+ Reasons never quote page content, on stdout or stderr: Claude Code persists this
51
+ process's stderr (and the server's, which it inherits) under
52
+ `~/.cache/claude-cli-nodejs/<project>/mcp-logs-playwright/*.jsonl`.
53
+
54
+ Decision, reach per surface, the enumerated raw-sink closure and the named
55
+ residuals: ADR-213 (#7980 addendum) and `agent-browser/SKILL.md` §"Wrapping the
56
+ server". Stdlib only, single `selectors` loop, POSIX.
57
+ """
58
+ from __future__ import annotations
59
+
60
+ import contextlib
61
+ import importlib.util
62
+ import json
63
+ import os
64
+ import selectors
65
+ import signal
66
+ import subprocess
67
+ import sys
68
+ import time
69
+ from typing import Any, Dict, List, NoReturn, Optional, Tuple
70
+
71
+ PREFIX = "playwright-mcp-redact-proxy:"
72
+ REDACTOR_BASENAME = "redact-a11y-snapshot.py"
73
+ # 64 MiB per line: a memory budget. A hostile page can inflate a tree and an
74
+ # OOM-killed proxy runs no teardown (CWE-400). A line whose newline arrives in the
75
+ # same read that crosses the cap is still parsed whole, so the real ceiling is
76
+ # the cap plus one read (READ_CHUNK); the 4 MiB result cap then withholds it.
77
+ MAX_LINE_BYTES = 64 * 1024 * 1024
78
+ READ_CHUNK = 65536
79
+ # Seconds between SIGTERM and SIGKILL for the child's process group. The suite
80
+ # shortens it with PLAYWRIGHT_MCP_PROXY_GRACE_S; it changes timing only.
81
+ GRACE_S = 5.0
82
+ STDIN_CLOSE_WAIT_S = 0.2
83
+ REFUSED_HEAD = "refused by playwright-mcp-redact-proxy:"
84
+ WITHHELD_HEAD = "withheld by playwright-mcp-redact-proxy:"
85
+ TOOLS_LIST_MARKER = (
86
+ " [Soleur: output is redacted in flight by the a11y-snapshot redactor;"
87
+ " filename is refused — call browser_snapshot with no filename.]"
88
+ )
89
+ TRAILER = "[Soleur: redacted in flight by playwright-mcp-redact-proxy]"
90
+ REFUSED_NEXT = "Behind this proxy call browser_snapshot with no filename, and call tools without _meta."
91
+ WITHHELD_NEXT = (
92
+ "The tool itself may have run: its result was withheld, not its action. Before"
93
+ " retrying, call browser_snapshot with no filename to see the page state. If"
94
+ " browser_snapshot is over the redactor's size cap, narrow it with target: or depth:."
95
+ )
96
+ SNAPSHOT_LINK_NOTICE = (
97
+ "- [Snapshot withheld by playwright-mcp-redact-proxy: the server wrote a raw tree"
98
+ " file under its output directory (default .playwright-mcp/); do not read it]"
99
+ )
100
+ UNRECOGNISED = "unrecognised result shape"
101
+ RESULT_KEYS = {"content", "isError", "isClose"}
102
+ TEXT_KEYS = {"type", "text", "annotations"}
103
+ BINARY_KEYS = {"type", "data", "mimeType", "annotations"}
104
+ ERROR_KEYS = {"code", "message"}
105
+ # Server-to-client messages relayed, each rebuilt from its method and ids only
106
+ # (`relay_server_message`). 0.0.78 emits no notification at all and one server
107
+ # request, `roots/list`; anything else could carry page text around the rewrite,
108
+ # so it is dropped (a request is answered on the server's side so the server
109
+ # does not wait on it).
110
+ PASSTHROUGH_REQUESTS = {"roots/list"}
111
+ PASSTHROUGH_NOTIFICATIONS = {"notifications/tools/list_changed", "notifications/cancelled"}
112
+ # `--caps` values that open no raw sink: `vision` adds coordinate tools. devtools
113
+ # (tracing, annotate), pdf and storage write raw page state the proxy never sees.
114
+ SAFE_CAPS = {"vision"}
115
+ # Where Playwright's default `chrome` channel looks for Google Chrome. `--chromium-fallback` appends
116
+ # `--browser chromium --sandbox` only when none of these exist; any other platform is a no-op.
117
+ GOOGLE_CHROME_PATHS = {
118
+ "linux": ["/opt/google/chrome/chrome"],
119
+ "darwin": ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
120
+ }
121
+
122
+
123
+ # Control characters (including U+2028/U+2029, which split a JSONL line) are
124
+ # scrubbed from everything log() writes: a client-supplied tool name or a
125
+ # serverInfo field must not forge a `refusing to start:` line in the persisted
126
+ # MCP logs Claude Code keeps for this process's stderr.
127
+ _LOG_SCRUB = {c: " " for c in list(range(0x20)) + [0x7F, 0x2028, 0x2029]}
128
+
129
+
130
+ # json.loads can legitimately return None (JSON null), so a failed parse needs its own marker.
131
+ _NOT_JSON = object()
132
+
133
+
134
+ def log(msg: str) -> None:
135
+ sys.stderr.write(f"{PREFIX} {msg.translate(_LOG_SCRUB)}\n")
136
+ sys.stderr.flush()
137
+
138
+
139
+ def _log_quiet(msg: str) -> None:
140
+ # log() that cannot raise: for lines written on the teardown path, where an exception would skip the reap and the exit.
141
+ # RuntimeError is in the guard because teardown can run from the SIGTERM handler, where a reentrant stderr write raises it.
142
+ # A closed, full, busy or missing stderr (None when fd 2 is closed) must not abort teardown.
143
+ with contextlib.suppress(OSError, ValueError, RuntimeError, AttributeError):
144
+ log(msg)
145
+
146
+
147
+ def _note(exc: BaseException, site: str) -> None:
148
+ # Class name and a static site label only. Never str(exc)/repr(exc)/input text: exception text can carry page
149
+ # content, and Claude Code persists this stderr. `site` is a string literal at every call site.
150
+ _log_quiet(f"swallowed {type(exc).__name__} at {site}")
151
+
152
+
153
+ def _flush_notes(pending: List[Tuple[BaseException, str]]) -> None:
154
+ # Emits the (exc, literal-label) pairs a handler collected while it must not write to stderr (see teardown).
155
+ for exc, label in pending:
156
+ _note(exc, label)
157
+
158
+
159
+ def refuse_start(reason: str) -> NoReturn:
160
+ log(f"refusing to start: {reason}")
161
+ os._exit(2)
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # B1 -- argv
166
+ # ---------------------------------------------------------------------------
167
+ def usage(msg: str) -> NoReturn:
168
+ refuse_start(f"usage: playwright-mcp-redact-proxy.py [--chromium-fallback] [--user-data-dir-name <basename>] -- <server argv...> ({msg})")
169
+
170
+
171
+ def profile_dir_from_args(args: List[str], env: Dict[str, str]) -> Optional[str]:
172
+ """Resolve `--user-data-dir-name <basename>` to an absolute dir under the cache root.
173
+
174
+ XDG semantics mirror scripts/lib/scratch-root.sh: $XDG_CACHE_HOME wins when
175
+ set, otherwise `$HOME/.cache`; a relative root or an
176
+ unresolvable `~` refuses rather than resolving against the caller's CWD.
177
+ Returns None when the flag is absent (the child argv stays untouched).
178
+ """
179
+ name: Optional[str] = None
180
+ i = 0
181
+ while i < len(args):
182
+ a = args[i]
183
+ if a == "--user-data-dir-name" or a.startswith("--user-data-dir-name="):
184
+ if name is not None:
185
+ refuse_start("--user-data-dir-name given twice")
186
+ if a == "--user-data-dir-name":
187
+ if i + 1 >= len(args):
188
+ refuse_start("--user-data-dir-name needs a basename argument")
189
+ name = args[i + 1]
190
+ i += 2
191
+ else:
192
+ name = a.split("=", 1)[1]
193
+ i += 1
194
+ else:
195
+ refuse_start(f"unrecognised proxy flag before `--`: {a}")
196
+ if name is None:
197
+ return None
198
+ if not name or name == "." or ".." in name:
199
+ refuse_start("--user-data-dir-name must be a basename (empty, '.' or '..' refuses)")
200
+ if "/" in name or "\\" in name:
201
+ refuse_start("--user-data-dir-name must be a basename (a path separator refuses)")
202
+ if name.startswith("-"):
203
+ refuse_start("--user-data-dir-name must be a basename (a leading '-' refuses)")
204
+ xdg = env.get("XDG_CACHE_HOME")
205
+ if xdg:
206
+ root = xdg
207
+ else:
208
+ home = env.get("HOME")
209
+ if not home:
210
+ refuse_start("cannot resolve the profile root: neither XDG_CACHE_HOME nor HOME is set")
211
+ root = os.path.join(home, ".cache")
212
+ if not os.path.isabs(root):
213
+ refuse_start("the profile root is not absolute (a relative XDG_CACHE_HOME or HOME refuses)")
214
+ return os.path.join(os.path.realpath(root), name)
215
+
216
+
217
+ def take_chromium_fallback(args: List[str]) -> Tuple[List[str], bool]:
218
+ """Remove the boolean `--chromium-fallback` from the proxy's own flags; refuse it given twice.
219
+
220
+ The value of `--user-data-dir-name` is never inspected here (it stays for profile_dir_from_args
221
+ to validate, so a basename spelled like a flag still refuses there).
222
+ """
223
+ rest: List[str] = []
224
+ found = False
225
+ i = 0
226
+ while i < len(args):
227
+ a = args[i]
228
+ if a == "--chromium-fallback":
229
+ if found:
230
+ refuse_start("--chromium-fallback given twice")
231
+ found = True
232
+ else:
233
+ rest.append(a)
234
+ if a == "--user-data-dir-name" and i + 1 < len(args):
235
+ rest.append(args[i + 1])
236
+ i += 1
237
+ i += 1
238
+ return rest, found
239
+
240
+
241
+ def chromium_fallback_applies(server: List[str], env: Dict[str, str]) -> bool:
242
+ """True when the server would default to a Google Chrome that is not installed.
243
+
244
+ Only Linux and macOS are modelled; any other platform is a no-op. An explicit `--browser`
245
+ (either spelling) or $PLAYWRIGHT_MCP_BROWSER means the caller chose, so nothing is appended.
246
+ """
247
+ plat = "linux" if sys.platform.startswith("linux") else sys.platform
248
+ if plat not in GOOGLE_CHROME_PATHS:
249
+ return False
250
+ if flag_present(server, "--browser") or env.get("PLAYWRIGHT_MCP_BROWSER"):
251
+ return False
252
+ seam = env.get("PLAYWRIGHT_MCP_PROXY_CHROME_PATHS")
253
+ paths = seam.split(os.pathsep) if seam is not None else GOOGLE_CHROME_PATHS[plat]
254
+ return not any(p and os.path.isfile(p) and os.access(p, os.X_OK) for p in paths)
255
+
256
+
257
+ def parse_argv(argv: List[str]) -> Tuple[List[str], Optional[str], bool]:
258
+ if "--" not in argv:
259
+ usage("no `--` separator")
260
+ sep = argv.index("--")
261
+ server = argv[sep + 1 :]
262
+ if not server:
263
+ usage("empty server argv")
264
+ own, chromium_fallback = take_chromium_fallback(argv[:sep])
265
+ return server, profile_dir_from_args(own, dict(os.environ)), chromium_fallback
266
+
267
+
268
+ # ---------------------------------------------------------------------------
269
+ # B2 -- the one predicate, bound by path; four names, all at startup
270
+ # ---------------------------------------------------------------------------
271
+ def load_redactor() -> Tuple[Any, Any, int, str]:
272
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), REDACTOR_BASENAME)
273
+ if not os.path.isfile(path):
274
+ refuse_start(f"sibling {REDACTOR_BASENAME} is missing (drifted plugin install?)")
275
+ try:
276
+ spec = importlib.util.spec_from_file_location("redact_a11y_snapshot", path)
277
+ if spec is None or spec.loader is None:
278
+ raise ImportError("no loader")
279
+ mod = importlib.util.module_from_spec(spec)
280
+ spec.loader.exec_module(mod)
281
+ except (Exception, SystemExit, KeyboardInterrupt) as exc: # a module-level sys.exit is a refusal too
282
+ refuse_start(f"cannot load {REDACTOR_BASENAME}: {type(exc).__name__}")
283
+ missing = [n for n in ("redact_text", "looks_like_a11y_tree", "MAX_INPUT_BYTES", "REDACTED") if not hasattr(mod, n)]
284
+ if missing:
285
+ refuse_start(f"{REDACTOR_BASENAME} does not export {', '.join(missing)}")
286
+ return mod.redact_text, mod.looks_like_a11y_tree, int(mod.MAX_INPUT_BYTES), str(mod.REDACTED)
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # B3 -- self-test: the predicate must redact a sentinel row AND remove the value
291
+ # ---------------------------------------------------------------------------
292
+ def self_test(redact_text: Any, looks_like_a11y_tree: Any, redacted: str) -> None:
293
+ row = '- textbox "Token" [ref=e1]: ZZQP-SENTINEL-7980'
294
+ try:
295
+ out = redact_text(row)
296
+ except (Exception, SystemExit, KeyboardInterrupt) as exc:
297
+ refuse_start(f"redactor self-test raised {type(exc).__name__}")
298
+ if redacted not in out or "ZZQP-SENTINEL-7980" in out:
299
+ refuse_start("redactor self-test did not redact the sentinel row")
300
+ try:
301
+ tree_true = looks_like_a11y_tree(row)
302
+ prose_false = looks_like_a11y_tree("### Page\n- Page URL: x")
303
+ except (Exception, SystemExit, KeyboardInterrupt) as exc:
304
+ refuse_start(f"looks_like_a11y_tree raised {type(exc).__name__}")
305
+ if not tree_true or prose_false:
306
+ refuse_start("looks_like_a11y_tree self-test failed (tree row / prose row)")
307
+
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # B2 (second half) -- raw sinks refused before spawn
311
+ # ---------------------------------------------------------------------------
312
+ def argv_values(server: List[str], flag: str) -> List[str]:
313
+ """Every value given to `flag` as `--flag v` or `--flag=v`, in order."""
314
+ values: List[str] = []
315
+ for i, a in enumerate(server):
316
+ if a.startswith(flag + "="):
317
+ values.append(a[len(flag) + 1 :])
318
+ elif a == flag and i + 1 < len(server):
319
+ values.append(server[i + 1])
320
+ return values
321
+
322
+
323
+ def debug_pattern_enables_pw(pattern: str) -> bool:
324
+ """True when a `debug` namespace pattern can enable any `pw:` logger.
325
+
326
+ The `debug` package splits DEBUG on whitespace and commas, skips `-`-prefixed
327
+ exclusions, and treats `*` as its only wildcard, matching anywhere. So a
328
+ pattern enables some `pw:<x>` namespace iff it reaches a `*` before
329
+ diverging from `pw:`, or consumes `pw:` literally.
330
+ """
331
+ target = "pw:"
332
+ for i, ch in enumerate(pattern):
333
+ if ch == "*":
334
+ return True
335
+ if i >= len(target):
336
+ return True
337
+ if ch != target[i]:
338
+ return False
339
+ return False
340
+
341
+
342
+ def caps_open_sinks(values: List[str]) -> bool:
343
+ for v in values:
344
+ for cap in v.replace(",", " ").split():
345
+ if cap not in SAFE_CAPS:
346
+ return True
347
+ return False
348
+
349
+
350
+ def flag_present(server: List[str], flag: str) -> bool:
351
+ """`flag` given as `--flag` or `--flag=value` anywhere in the server argv."""
352
+ return any(a == flag or a.startswith(flag + "=") for a in server)
353
+
354
+
355
+ # Server argv flags that open a raw sink around this relay, substitute the
356
+ # browser or transport the proxy owns, or inject code/credential material into
357
+ # pages. Each is refused in either `--flag` or `--flag=value` form; the
358
+ # PLAYWRIGHT_MCP_* env equivalent is refused alongside (the child inherits the
359
+ # session env wholesale, so an ambient export is the same setting with no
360
+ # registration-level audit trail).
361
+ SINK_FLAGS = [
362
+ ("--save-session", "PLAYWRIGHT_MCP_SAVE_SESSION", "writes every response to a session.md on disk"),
363
+ ("--storage-state", "PLAYWRIGHT_MCP_STORAGE_STATE", "persists cookies and storage state to a file"),
364
+ ("--secrets", "PLAYWRIGHT_MCP_SECRETS_FILE", "loads secrets from a file the model can be steered to fill into pages"),
365
+ ("--output-dir", "PLAYWRIGHT_MCP_OUTPUT_DIR", "names a directory for raw output files"),
366
+ ("--init-script", "PLAYWRIGHT_MCP_INIT_SCRIPT", "runs a script in every page, around this relay"),
367
+ ("--init-page", "PLAYWRIGHT_MCP_INIT_PAGE", "evaluates a file in every page, around this relay"),
368
+ ("--cdp-endpoint", "PLAYWRIGHT_MCP_CDP_ENDPOINT", "connects to a browser this proxy did not launch"),
369
+ ("--endpoint", "PLAYWRIGHT_MCP_ENDPOINT", "connects to a remote Playwright server this proxy did not launch"),
370
+ ("--extension", "PLAYWRIGHT_MCP_EXTENSION", "connects to a running browser this proxy did not launch"),
371
+ ("--executable-path", "PLAYWRIGHT_MCP_EXECUTABLE_PATH", "substitutes the browser binary this proxy launches"),
372
+ ("--allow-unrestricted-file-access", "PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS", "lets pages reach arbitrary local files"),
373
+ ("--grant-permissions", "PLAYWRIGHT_MCP_GRANT_PERMISSIONS", "grants pages permissions (clipboard, geolocation) the model did not audit"),
374
+ ("--save-trace", "PLAYWRIGHT_MCP_SAVE_TRACE", "writes a Playwright Trace of the session to disk"),
375
+ ("--save-video", "PLAYWRIGHT_MCP_SAVE_VIDEO", "writes session video to the output directory"),
376
+ ("--ignore-https-errors", "PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS", "drops TLS validation on page requests"),
377
+ ("--no-sandbox", "PLAYWRIGHT_MCP_SANDBOX", "controls the browser sandbox"),
378
+ ("--daemon", "PLAYWRIGHT_MCP_DAEMON", "serves the tools over a socket, around this stdio relay"),
379
+ ]
380
+ # Env-only refusals: the argv/config form stays allowed because it is explicit
381
+ # and audited in the registration; the ambient env form is not.
382
+ SINK_ENV_ONLY = [
383
+ ("PLAYWRIGHT_MCP_USER_DATA_DIR", "sets a profile outside the audited registration argv"),
384
+ ("PLAYWRIGHT_MCP_SNAPSHOT_MODE", "the proxy owns --snapshot-mode"),
385
+ ]
386
+ # Valued flags: a bare one as the LAST server arg would bind the appended
387
+ # `--user-data-dir=<dir>` (or `--snapshot-mode`) as its value instead of
388
+ # refusing cleanly. Boolean flags are deliberately absent — a trailing
389
+ # `--headless` does not consume the next token.
390
+ VALUE_FLAGS = {
391
+ "--allowed-hosts", "--allowed-origins", "--blocked-origins", "--browser",
392
+ "--caps", "--cdp-endpoint", "--cdp-header", "--cdp-timeout", "--codegen",
393
+ "--config", "--console-level", "--daemon", "--device", "--endpoint",
394
+ "--executable-path", "--grant-permissions", "--host", "--image-responses",
395
+ "--init-page", "--init-script", "--output-dir", "--output-max-size",
396
+ "--output-mode", "--port", "--proxy-bypass", "--proxy-server", "--save-video",
397
+ "--secrets", "--snapshot-mode", "--storage-state", "--test-id-attribute",
398
+ "--timeout-action", "--timeout-navigation", "--user-agent", "--user-data-dir",
399
+ "--viewport-size",
400
+ }
401
+
402
+
403
+ def refuse_argv_and_env(server: List[str], profile_dir: Optional[str], env: Dict[str, str]) -> None:
404
+ if profile_dir is not None and (argv_values(server, "--user-data-dir") or flag_present(server, "--user-data-dir")):
405
+ refuse_start("--user-data-dir-name cannot be combined with an explicit --user-data-dir in the server argv")
406
+ if server and server[-1] in VALUE_FLAGS:
407
+ refuse_start(f"{server[-1]} expects a value that would bind an appended flag")
408
+ for flag, envvar, why in SINK_FLAGS:
409
+ if flag_present(server, flag):
410
+ refuse_start(f"{flag} {why}")
411
+ if env.get(envvar):
412
+ refuse_start(f"{envvar} is set ({why})")
413
+ for envvar, why in SINK_ENV_ONLY:
414
+ if env.get(envvar):
415
+ refuse_start(f"{envvar} is set ({why})")
416
+ if argv_values(server, "--port") or flag_present(server, "--port") or env.get("PLAYWRIGHT_MCP_PORT"):
417
+ refuse_start("--port serves the tools over HTTP, around this stdio relay")
418
+ if argv_values(server, "--host") or flag_present(server, "--host") or env.get("PLAYWRIGHT_MCP_HOST"):
419
+ refuse_start("--host binds an HTTP transport around this stdio relay")
420
+ if caps_open_sinks(argv_values(server, "--caps")) or caps_open_sinks([env.get("PLAYWRIGHT_MCP_CAPS", "")]):
421
+ refuse_start("--caps enables a capability other than vision (devtools, pdf and storage write raw page state)")
422
+ if any(v != "stdout" for v in argv_values(server, "--output-mode") + [env.get("PLAYWRIGHT_MCP_OUTPUT_MODE", "stdout")]):
423
+ refuse_start("--output-mode file writes snapshots and logs to disk")
424
+ config_path: Optional[str] = None
425
+ named = argv_values(server, "--config")
426
+ if named:
427
+ config_path = named[-1]
428
+ elif env.get("PLAYWRIGHT_MCP_CONFIG"):
429
+ config_path = env["PLAYWRIGHT_MCP_CONFIG"]
430
+ if config_path:
431
+ if not os.path.isfile(config_path):
432
+ refuse_start("the config file named by --config or PLAYWRIGHT_MCP_CONFIG does not exist")
433
+ try:
434
+ with open(config_path, "rb") as fh:
435
+ cfg = json.loads(fh.read().decode("utf-8-sig"))
436
+ except (OSError, ValueError, RecursionError):
437
+ # The server falls back to an INI parser, where saveSession is a typed
438
+ # key; a file this proxy cannot read is a file it cannot vet.
439
+ refuse_start("the config file is not JSON, so its raw-sink settings cannot be checked")
440
+ if not isinstance(cfg, dict):
441
+ refuse_start("the config file is not a JSON object, so its raw-sink settings cannot be checked")
442
+ if cfg.get("saveSession"):
443
+ refuse_start("config saveSession is set (a raw session.md sink)")
444
+ if cfg.get("saveVideo"):
445
+ refuse_start("config saveVideo is set (video frames of every page are written to disk)")
446
+ if cfg.get("saveTrace"):
447
+ refuse_start("config saveTrace is set (a raw trace written to disk)")
448
+ if cfg.get("secrets"):
449
+ refuse_start("config secrets injects credential material into pages")
450
+ if cfg.get("outputDir"):
451
+ refuse_start("config outputDir names a directory for raw output files")
452
+ if cfg.get("allowUnrestrictedFileAccess"):
453
+ refuse_start("config allowUnrestrictedFileAccess lets pages reach arbitrary local files")
454
+ if cfg.get("extension"):
455
+ refuse_start("config extension connects to a running browser this proxy did not launch")
456
+ caps = cfg.get("capabilities")
457
+ if caps is not None and (not isinstance(caps, list) or caps_open_sinks([str(c) for c in caps])):
458
+ refuse_start("config capabilities enables a capability other than vision")
459
+ srv = cfg.get("server")
460
+ if isinstance(srv, dict) and (srv.get("port") is not None or srv.get("host") is not None):
461
+ refuse_start("config server.port/server.host serves the tools over HTTP, around this stdio relay")
462
+ browser = cfg.get("browser")
463
+ if isinstance(browser, dict):
464
+ if browser.get("cdpEndpoint") or browser.get("remoteEndpoint"):
465
+ refuse_start("config browser.cdpEndpoint/browser.remoteEndpoint connects to a browser this proxy did not launch")
466
+ if browser.get("initPage") or browser.get("initScript"):
467
+ refuse_start("config browser.initPage/browser.initScript injects code into every page")
468
+ if profile_dir is not None and browser.get("userDataDir"):
469
+ refuse_start("config browser.userDataDir conflicts with --user-data-dir-name")
470
+ ctx = browser.get("contextOptions")
471
+ if isinstance(ctx, dict):
472
+ if ctx.get("storageState"):
473
+ refuse_start("config browser.contextOptions.storageState persists cookies and storage state to a file")
474
+ if ctx.get("permissions"):
475
+ refuse_start("config browser.contextOptions.permissions grants page permissions the model did not audit")
476
+ if ctx.get("ignoreHTTPSErrors"):
477
+ refuse_start("config browser.contextOptions.ignoreHTTPSErrors drops TLS validation on page requests")
478
+ lo = browser.get("launchOptions")
479
+ if isinstance(lo, dict):
480
+ if lo.get("executablePath"):
481
+ refuse_start("config browser.launchOptions.executablePath substitutes the browser binary")
482
+ if lo.get("chromiumSandbox") is False:
483
+ refuse_start("config browser.launchOptions.chromiumSandbox disables the browser sandbox")
484
+ args = lo.get("args")
485
+ if isinstance(args, list) and any(str(a).startswith("--remote-debugging") for a in args):
486
+ refuse_start("config launchOptions.args opens a remote debugging channel around this relay")
487
+ patterns = env.get("DEBUG", "").replace(",", " ").split()
488
+ if any(debug_pattern_enables_pw(p) for p in patterns if not p.startswith("-")):
489
+ refuse_start("DEBUG enables a pw:* logger, which prints unredacted results to stderr")
490
+ if env.get("DEBUG_FILE"):
491
+ refuse_start("DEBUG_FILE is set (a raw debug sink on disk)")
492
+
493
+
494
+ # ---------------------------------------------------------------------------
495
+ # B9 -- ONE builder for every synthesized client-bound tools/call result
496
+ # ---------------------------------------------------------------------------
497
+ def error_result(rid: Any, tool: str, reason: str, *, refused: bool = False) -> bytes:
498
+ kind = "refused" if refused else "withheld"
499
+ head = REFUSED_HEAD if refused else WITHHELD_HEAD
500
+ text = f"### Error\n{head} {reason} (tool: {tool})\n{REFUSED_NEXT if refused else WITHHELD_NEXT}"
501
+ log(f"{kind} tool={tool} reason={reason}")
502
+ obj = {"jsonrpc": "2.0", "id": rid, "result": {"content": [{"type": "text", "text": text}], "isError": True}}
503
+ return json.dumps(obj, separators=(",", ":"), ensure_ascii=True).encode()
504
+
505
+
506
+ def jsonrpc_error(rid: Any, code: int, message: str) -> bytes:
507
+ obj = {"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": message}}
508
+ return json.dumps(obj, separators=(",", ":"), ensure_ascii=True).encode()
509
+
510
+
511
+ def write_line(fobj: Any, data: bytes) -> None:
512
+ fobj.write(data + b"\n")
513
+ fobj.flush()
514
+
515
+
516
+ # ---------------------------------------------------------------------------
517
+ # B5 -- client -> server
518
+ # ---------------------------------------------------------------------------
519
+ def id_key(rid: Any) -> str:
520
+ """The JSON text of the id: keeps 2 and "2" apart and is hashable for any id shape."""
521
+ return json.dumps(rid, sort_keys=True)
522
+
523
+
524
+ def refuse_request(req: Dict[str, Any]) -> Optional[bytes]:
525
+ """Return an error_result line for a request that must never reach the server, else None."""
526
+ if req.get("method") != "tools/call":
527
+ return None
528
+ params = req.get("params") if isinstance(req.get("params"), dict) else {}
529
+ name = str(params.get("name", ""))
530
+ args = params.get("arguments")
531
+ if not isinstance(args, dict):
532
+ return None
533
+ if "_meta" in args:
534
+ # measured: `_meta.json` returns the tree as one JSON-escaped string the
535
+ # line-anchored predicate cannot see.
536
+ return error_result(req.get("id"), name, "arguments._meta is an undocumented hook that returns the tree in a shape the redactor cannot see; call the tool without _meta", refused=True)
537
+ if name == "browser_snapshot" and "filename" in args:
538
+ return error_result(req.get("id"), name, "filename writes the raw tree to disk; call browser_snapshot with no filename", refused=True)
539
+ return None
540
+
541
+
542
+ def json_strings(node: Any) -> List[str]:
543
+ if isinstance(node, str):
544
+ return [node]
545
+ if isinstance(node, dict):
546
+ return [s for v in node.values() for s in json_strings(v)]
547
+ if isinstance(node, list):
548
+ return [s for v in node for s in json_strings(v)]
549
+ return []
550
+
551
+
552
+ class Proxy:
553
+ def __init__(self, server: List[str], redactor: Tuple[Any, Any, int, str], profile_dir: Optional[str] = None, chromium_fallback: bool = False) -> None:
554
+ self.redact_text, self.looks_like_a11y_tree, self.max_input_bytes, self.redacted = redactor
555
+ self.pending: Dict[str, Tuple[Any, str, str]] = {} # id_key -> (raw id, method, tool name)
556
+ self.out = sys.stdout.buffer
557
+ self.child: Optional[subprocess.Popen] = None
558
+ self.pgid = 0
559
+ self.tearing_down = False
560
+ self.grace = GRACE_S
561
+ try:
562
+ self.grace = float(os.environ.get("PLAYWRIGHT_MCP_PROXY_GRACE_S", GRACE_S))
563
+ except ValueError:
564
+ log("ignored a non-numeric PLAYWRIGHT_MCP_PROXY_GRACE_S")
565
+ # Installed BEFORE the spawn, so a signal in between still ends the group.
566
+ signal.signal(signal.SIGTERM, self.on_signal)
567
+ signal.signal(signal.SIGINT, self.on_signal)
568
+ argv = list(server)
569
+ if chromium_fallback and chromium_fallback_applies(server, dict(os.environ)):
570
+ argv += ["--browser", "chromium", "--sandbox"]
571
+ log("no Google Chrome found; adding --browser chromium --sandbox (the bundled build, sandbox kept on; if the first browser call reports a sandbox failure, install Google Chrome - the sandbox is never disabled)")
572
+ if profile_dir is not None:
573
+ argv.append("--user-data-dir=" + profile_dir)
574
+ argv += ["--snapshot-mode", "none"]
575
+ self.child = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, bufsize=0, start_new_session=True)
576
+ self.pgid = os.getpgid(self.child.pid)
577
+ log(f"child pgid {self.pgid}")
578
+
579
+ def on_signal(self, _signum: int, _frame: Any) -> None:
580
+ self.teardown("signal")
581
+
582
+ # -- B5 -----------------------------------------------------------------
583
+ def pump_client_to_server(self, line: bytes) -> None:
584
+ assert self.child is not None and self.child.stdin is not None
585
+ try:
586
+ req = json.loads(line.decode("utf-8", errors="replace"))
587
+ except (ValueError, RecursionError):
588
+ # Never forwarded: a line this proxy cannot read is a line it cannot
589
+ # vet for filename/_meta, and a JSON parser the server uses may still
590
+ # accept it (e.g. an integer past Python's digit limit).
591
+ log(f"dropped unparsable client line ({len(line)} bytes)")
592
+ return
593
+ if isinstance(req, list):
594
+ log(f"dropped list line from client ({len(line)} bytes)")
595
+ return
596
+ if not isinstance(req, dict):
597
+ log(f"dropped non-object client line ({len(line)} bytes)")
598
+ return
599
+ refusal = refuse_request(req)
600
+ if refusal is not None:
601
+ write_line(self.out, refusal)
602
+ return
603
+ if "method" in req and "id" in req:
604
+ rid = req["id"]
605
+ key = id_key(rid)
606
+ if key in self.pending:
607
+ log("refused a request reusing a pending id")
608
+ write_line(self.out, jsonrpc_error(rid, -32600, "request id is already pending (refused by playwright-mcp-redact-proxy)"))
609
+ return
610
+ params = req.get("params") if isinstance(req.get("params"), dict) else {}
611
+ self.pending[key] = (rid, str(req["method"]), str(params.get("name", "")))
612
+ write_line(self.child.stdin, line)
613
+
614
+ # -- B6/B7/B8 -----------------------------------------------------------
615
+ def pump_server_to_client(self, line: bytes) -> None:
616
+ try:
617
+ msg = json.loads(line.decode("utf-8", errors="replace"))
618
+ except (ValueError, RecursionError):
619
+ log(f"dropped unparsable server line ({len(line)} bytes)")
620
+ return
621
+ if isinstance(msg, list):
622
+ answered = 0
623
+ for el in msg:
624
+ if isinstance(el, dict) and ("result" in el or "error" in el):
625
+ entry = self.pending.pop(self.safe_key(el.get("id")), None)
626
+ if entry is not None:
627
+ write_line(self.out, error_result(entry[0], entry[2], "the server answered inside a JSON list line, which the proxy does not relay"))
628
+ answered += 1
629
+ log(f"dropped list line from server ({len(line)} bytes, {answered} pending answered)")
630
+ return
631
+ if not isinstance(msg, dict):
632
+ log(f"dropped non-object server line ({len(line)} bytes)")
633
+ return
634
+ if "result" not in msg and "error" not in msg:
635
+ self.relay_server_message(msg)
636
+ return
637
+ entry = self.pending.pop(self.safe_key(msg.get("id")), None)
638
+ if entry is None:
639
+ log(f"dropped unknown-id response ({len(line)} bytes)")
640
+ return
641
+ rid, method, tool = entry
642
+ if "method" in msg:
643
+ write_line(self.out, error_result(rid, tool, UNRECOGNISED + " (response carries a method)"))
644
+ return
645
+ result = msg.get("result")
646
+ if method == "tools/call" or (isinstance(result, dict) and "content" in result):
647
+ # Routed by SHAPE as well as by method: a content-bearing result is
648
+ # rewritten whatever request it answers.
649
+ write_line(self.out, self.rewrite_result(msg, line, tool))
650
+ elif "error" in msg:
651
+ write_line(self.out, self.vet_error(msg, line, tool))
652
+ elif method == "tools/list":
653
+ write_line(self.out, self.annotate_tools_list(msg, line))
654
+ elif method == "initialize":
655
+ info = result if isinstance(result, dict) else {}
656
+ si = info.get("serverInfo", {}) if isinstance(info.get("serverInfo"), dict) else {}
657
+ log(f"wrapping {si.get('name', '?')} {si.get('version', '?')} protocol {info.get('protocolVersion', '?')}")
658
+ write_line(self.out, self.vet_other_result(msg, line, rid, tool))
659
+ else:
660
+ write_line(self.out, self.vet_other_result(msg, line, rid, tool))
661
+
662
+ def safe_key(self, rid: Any) -> str:
663
+ try:
664
+ return id_key(rid)
665
+ except (TypeError, ValueError):
666
+ return "\x00unkeyable"
667
+
668
+ def plain_id(self, value: Any) -> bool:
669
+ """An id the proxy may echo: an integer, or a string with no tree row or redactable value."""
670
+ if isinstance(value, bool):
671
+ return False
672
+ if isinstance(value, int):
673
+ return True
674
+ return isinstance(value, str) and not self.looks_like_a11y_tree(value) and self.redact_text(value) == value
675
+
676
+ def relay_server_message(self, msg: Dict[str, Any]) -> None:
677
+ """A server request or notification: relay only the ones 0.0.78 can send,
678
+ rebuilt from method and ids alone so no params field (a cancel reason, a
679
+ _meta) carries page text past the proxy."""
680
+ method = msg.get("method")
681
+ if "id" in msg:
682
+ if method in PASSTHROUGH_REQUESTS and self.plain_id(msg.get("id")):
683
+ write_line(self.out, json.dumps({"jsonrpc": "2.0", "id": msg["id"], "method": method}, separators=(",", ":")).encode())
684
+ return
685
+ log("dropped a server request other than roots/list")
686
+ assert self.child is not None and self.child.stdin is not None
687
+ try:
688
+ write_line(self.child.stdin, jsonrpc_error(msg.get("id"), -32601, "method not relayed by playwright-mcp-redact-proxy"))
689
+ except (OSError, ValueError) as exc:
690
+ _note(exc, "reply-unrelayed-request")
691
+ return
692
+ if method in PASSTHROUGH_NOTIFICATIONS:
693
+ rebuilt: Dict[str, Any] = {"jsonrpc": "2.0", "method": method}
694
+ params = msg.get("params")
695
+ if method == "notifications/cancelled":
696
+ if not isinstance(params, dict) or not self.plain_id(params.get("requestId")):
697
+ log("dropped a cancellation whose requestId is not a plain id")
698
+ return
699
+ rebuilt["params"] = {"requestId": params["requestId"]}
700
+ write_line(self.out, json.dumps(rebuilt, separators=(",", ":")).encode())
701
+ return
702
+ log("dropped a server notification outside the relayed set")
703
+
704
+ def vet_error(self, msg: Dict[str, Any], line: bytes, tool: str) -> bytes:
705
+ err = msg.get("error")
706
+ if "result" in msg or not isinstance(err, dict) or not set(err.keys()) <= ERROR_KEYS:
707
+ return error_result(msg.get("id"), tool, "error response had an unrecognised shape")
708
+ if not isinstance(err.get("code"), int) or isinstance(err.get("code"), bool):
709
+ return error_result(msg.get("id"), tool, "error response had an unrecognised shape")
710
+ message = err.get("message", "")
711
+ if not isinstance(message, str) or self.looks_like_a11y_tree(message) or self.redact_text(message) != message:
712
+ return error_result(msg.get("id"), tool, "error message carried tree-shaped text")
713
+ # Rebuilt, not relayed: no other top-level key of `msg` reaches the client.
714
+ return json.dumps({"jsonrpc": "2.0", "id": msg.get("id"), "error": {"code": err["code"], "message": message}}, separators=(",", ":"), ensure_ascii=True).encode()
715
+
716
+ def vet_other_result(self, msg: Dict[str, Any], line: bytes, rid: Any, tool: str) -> bytes:
717
+ """A matched non-tools/call result (initialize, ping, a future method):
718
+ forwarded only when no string anywhere in the frame is tree-shaped or
719
+ redactable — a future server's `resources/read`-style payload must not
720
+ carry page text around the result-shape whitelist."""
721
+ if any(self.looks_like_a11y_tree(s) or self.redact_text(s) != s for s in json_strings(msg)):
722
+ return error_result(rid, tool, UNRECOGNISED + " (non-tools/call result carried redactable text)")
723
+ return line
724
+
725
+ def escaped_tree_in(self, text: str) -> bool:
726
+ """True when a `### Result` body (or a JSON line) carries a JSON-escaped tree.
727
+
728
+ `browser_run_code_unsafe` and `browser_evaluate` return JSON; an
729
+ `ariaSnapshot()` returned that way is one escaped string the line-anchored
730
+ predicate cannot see, the same shape the `_meta` refusal exists for.
731
+ """
732
+ candidates: List[str] = []
733
+ section: List[str] = []
734
+ in_result = False
735
+ for tline in text.split("\n") + ["### end"]:
736
+ if tline.startswith("### "):
737
+ if in_result:
738
+ candidates.append("\n".join(section))
739
+ in_result = tline == "### Result"
740
+ section = []
741
+ elif in_result:
742
+ section.append(tline)
743
+ stripped = tline.strip().rstrip(",")
744
+ if stripped[:1] in ('"', "{", "["):
745
+ candidates.append(stripped)
746
+ if stripped.startswith('"') and '":' in stripped:
747
+ candidates.append("{" + stripped + "}")
748
+ for cand in candidates:
749
+ try:
750
+ parsed = json.loads(cand)
751
+ except (ValueError, RecursionError):
752
+ parsed = _NOT_JSON # a per-line probe over page text: most lines are not JSON, the negative result is the normal one
753
+ if parsed is _NOT_JSON:
754
+ continue
755
+ # No newline requirement: one row returned by `ariaSnapshot()` on a
756
+ # single locator is a whole tree to the redactor's predicate.
757
+ if any(self.looks_like_a11y_tree(s) for s in json_strings(parsed)):
758
+ return True
759
+ return False
760
+
761
+ def rewrite_result(self, msg: Dict[str, Any], line: bytes, tool: str) -> bytes:
762
+ rid = msg.get("id")
763
+ try:
764
+ if "error" in msg:
765
+ return self.vet_error(msg, line, tool)
766
+ result = msg.get("result")
767
+ if not isinstance(result, dict) or not set(result.keys()) <= RESULT_KEYS:
768
+ return error_result(rid, tool, UNRECOGNISED)
769
+ if not isinstance(result.get("isError", False), bool):
770
+ return error_result(rid, tool, UNRECOGNISED)
771
+ content = result.get("content")
772
+ if not isinstance(content, list):
773
+ return error_result(rid, tool, UNRECOGNISED)
774
+ changed = False
775
+ tree_seen = False
776
+ new_content: List[Dict[str, Any]] = []
777
+ for block in content:
778
+ if not isinstance(block, dict):
779
+ return error_result(rid, tool, UNRECOGNISED)
780
+ btype = block.get("type")
781
+ if btype == "text":
782
+ if not set(block.keys()) <= TEXT_KEYS or not isinstance(block.get("text"), str):
783
+ return error_result(rid, tool, UNRECOGNISED)
784
+ text = block["text"]
785
+ if len(text.encode("utf-8", "surrogatepass")) > self.max_input_bytes:
786
+ return error_result(rid, tool, "result exceeds the redactor's size cap")
787
+ if self.escaped_tree_in(text):
788
+ return error_result(rid, tool, "result carries a JSON-escaped accessibility tree")
789
+ lines = text.split("\n")
790
+ in_snapshot = False
791
+ for i, tline in enumerate(lines):
792
+ if tline.startswith("### "):
793
+ in_snapshot = tline == "### Snapshot"
794
+ elif in_snapshot and tline.startswith("- [Snapshot]("):
795
+ # The disk sink reappearing (a version bump). The file
796
+ # is already written; hide the path, keep the result.
797
+ lines[i] = SNAPSHOT_LINK_NOTICE
798
+ log(f"replaced a snapshot file link tool={tool}")
799
+ relinked = "\n".join(lines)
800
+ if self.looks_like_a11y_tree(relinked):
801
+ tree_seen = True
802
+ new_text = self.redact_text(relinked)
803
+ if new_text != text:
804
+ changed = True
805
+ block = dict(block)
806
+ block["text"] = new_text
807
+ elif btype in ("image", "audio"):
808
+ if not set(block.keys()) <= BINARY_KEYS:
809
+ return error_result(rid, tool, UNRECOGNISED)
810
+ else:
811
+ return error_result(rid, tool, UNRECOGNISED)
812
+ new_content.append(block)
813
+ if tree_seen:
814
+ new_content.append({"type": "text", "text": TRAILER})
815
+ changed = True
816
+ if not changed:
817
+ return line
818
+ new_result = dict(result)
819
+ new_result["content"] = new_content
820
+ out = dict(msg)
821
+ out["result"] = new_result
822
+ return json.dumps(out, separators=(",", ":"), ensure_ascii=True, allow_nan=False).encode()
823
+ except Exception as exc: # noqa: BLE001 - any failure withholds
824
+ return error_result(rid, tool, f"redaction raised {type(exc).__name__}")
825
+
826
+ def annotate_tools_list(self, msg: Dict[str, Any], line: bytes) -> bytes:
827
+ try:
828
+ for t in msg["result"]["tools"]:
829
+ if t.get("name") == "browser_snapshot" and isinstance(t.get("description"), str):
830
+ t["description"] = t["description"] + TOOLS_LIST_MARKER
831
+ return json.dumps(msg, separators=(",", ":"), ensure_ascii=True).encode()
832
+ except Exception as exc: # noqa: BLE001 - fail safe: original bytes (no content key reaches here)
833
+ log(f"annotate failed: {type(exc).__name__}")
834
+ return line
835
+
836
+ # -- B10 ----------------------------------------------------------------
837
+ def group_alive(self) -> bool:
838
+ try:
839
+ os.killpg(self.pgid, 0)
840
+ return True
841
+ except ProcessLookupError:
842
+ return False
843
+ except PermissionError:
844
+ return True
845
+
846
+ def wait_group_empty(self, seconds: float) -> bool:
847
+ deadline = time.monotonic() + seconds
848
+ while time.monotonic() < deadline:
849
+ if self.child is not None and self.child.poll() is None:
850
+ # A 50 ms poll: the child still running is the normal outcome of each tick.
851
+ with contextlib.suppress(subprocess.TimeoutExpired):
852
+ self.child.wait(timeout=0.05)
853
+ if not self.group_alive():
854
+ return True
855
+ time.sleep(0.05)
856
+ return not self.group_alive()
857
+
858
+ def teardown(self, why: str) -> NoReturn:
859
+ if self.tearing_down:
860
+ os._exit(1)
861
+ self.tearing_down = True
862
+ child = self.child
863
+ if child is None:
864
+ log(f"no child to tear down ({why})")
865
+ os._exit(1)
866
+ # Notes are collected here and written after the kill ladder: teardown also runs from on_signal, and a
867
+ # stderr write that raises ahead of os.killpg would skip the group kill, the proxy's core invariant.
868
+ pending: List[Tuple[BaseException, str]] = []
869
+ try:
870
+ if child.stdin and not child.stdin.closed:
871
+ child.stdin.close()
872
+ except OSError as exc:
873
+ pending.append((exc, "teardown-stdin-close"))
874
+ # The designed route into the SIGTERM step: a child that ignores EOF is the case the ladder exists for.
875
+ with contextlib.suppress(subprocess.TimeoutExpired):
876
+ child.wait(timeout=STDIN_CLOSE_WAIT_S)
877
+ # Always signal the GROUP: the direct child (npx) may already have exited
878
+ # while Chrome, its grandchild, still holds the group.
879
+ # The group is already empty after a clean exit: the normal outcome of every clean session.
880
+ with contextlib.suppress(ProcessLookupError):
881
+ os.killpg(self.pgid, signal.SIGTERM)
882
+ if not self.wait_group_empty(self.grace):
883
+ try:
884
+ os.killpg(self.pgid, signal.SIGKILL)
885
+ except ProcessLookupError as exc:
886
+ pending.append((exc, "teardown-sigkill"))
887
+ _log_quiet(f"group {self.pgid} survived SIGTERM; SIGKILL sent") # after the kill
888
+ self.wait_group_empty(self.grace)
889
+ try:
890
+ child.wait(timeout=self.grace)
891
+ except subprocess.TimeoutExpired as exc:
892
+ pending.append((exc, "teardown-final-wait"))
893
+ rc = child.returncode
894
+ if rc is None:
895
+ rc = 1
896
+ sig = -rc if rc < 0 else 0
897
+ _flush_notes(pending)
898
+ _log_quiet(f"child exited rc={rc} signal={sig} ({why})")
899
+ code = rc if rc >= 0 else 128 - rc
900
+ try:
901
+ self.out.flush()
902
+ except (OSError, ValueError) as exc:
903
+ _note(exc, "teardown-flush")
904
+ os._exit(code)
905
+
906
+ # -- B4 -- the single loop ------------------------------------------------
907
+ def run(self) -> NoReturn:
908
+ assert self.child is not None and self.child.stdout is not None
909
+ sel = selectors.DefaultSelector()
910
+ sel.register(sys.stdin.buffer.fileno(), selectors.EVENT_READ, "client")
911
+ sel.register(self.child.stdout.fileno(), selectors.EVENT_READ, "server")
912
+ bufs = {"client": bytearray(), "server": bytearray()}
913
+ discarding = {"client": False, "server": False}
914
+ while True:
915
+ for key, _mask in sel.select():
916
+ side = key.data
917
+ try:
918
+ data = os.read(key.fd, READ_CHUNK)
919
+ except OSError:
920
+ data = b""
921
+ if not data:
922
+ self.teardown("stdin EOF" if side == "client" else "child stdout EOF")
923
+ buf = bufs[side]
924
+ buf += data
925
+ while True:
926
+ nl = buf.find(b"\n")
927
+ if nl < 0:
928
+ if len(buf) > MAX_LINE_BYTES and not discarding[side]:
929
+ discarding[side] = True
930
+ log(f"discarding oversize {side} line (> {MAX_LINE_BYTES} bytes, buffered {len(buf)})")
931
+ if side == "server":
932
+ for k, (rid, _m, tool) in list(self.pending.items()):
933
+ write_line(self.out, error_result(rid, tool, "oversize"))
934
+ self.pending.pop(k, None)
935
+ del buf[:]
936
+ elif discarding[side]:
937
+ del buf[:]
938
+ break
939
+ line = bytes(buf[:nl])
940
+ del buf[: nl + 1]
941
+ if discarding[side]:
942
+ discarding[side] = False
943
+ continue
944
+ try:
945
+ if side == "client":
946
+ self.pump_client_to_server(line)
947
+ else:
948
+ self.pump_server_to_client(line)
949
+ except Exception as exc: # noqa: BLE001 - one bad line must not strand the lines behind it
950
+ log(f"pump error: {type(exc).__name__}")
951
+ if self.child.poll() is not None:
952
+ self.teardown("pump error after child exit")
953
+
954
+
955
+ def main(argv: List[str]) -> int:
956
+ server, profile_dir, chromium_fallback = parse_argv(argv)
957
+ redactor = load_redactor()
958
+ self_test(redactor[0], redactor[1], redactor[3])
959
+ refuse_argv_and_env(server, profile_dir, dict(os.environ))
960
+ Proxy(server, redactor, profile_dir, chromium_fallback).run()
961
+
962
+
963
+ if __name__ == "__main__":
964
+ sys.exit(main(sys.argv[1:]))