@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,906 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * harness-discovery-smoke — prove Codex, Devin and OpenCode DISCOVER the skill set
4
+ * their manifests declare, by installing the plugin hermetically from this checkout and
5
+ * asking the vendor CLI what it registered (ADR-245; the unfiled item 6 of #8390's
6
+ * bundle).
7
+ *
8
+ * WHAT THIS PROVES, AND WHAT IT DOES NOT.
9
+ * - It proves REGISTRATION: the name is in the harness's skill list. It does not
10
+ * prove INVOCABILITY (ADR-236: `codex exec` ignores `disable-model-invocation`,
11
+ * and `devin skills list` reports `[user]` vs `[user,model]`), and it does not
12
+ * prove WHICH copy was loaded — for `go`/`help`/`sync` the shim body differs
13
+ * from the canonical skill, and a single listing cannot distinguish them.
14
+ * - It proves nothing about UNIQUENESS. A newly added `codex/skills/plan/` is
15
+ * legitimately k=2 and passes here by construction; cross-root collision policy
16
+ * stays with `components.test.ts` (ADR-224 decision 5, ACKED_CROSS_ROOT_DUPES).
17
+ *
18
+ * OPENCODE IS A DIFFERENT SHAPE, and the differences are each forced by a measurement
19
+ * (§11.0): ids are BARE directory basenames with no namespace, so `plan` names both this
20
+ * plugin's skill and the operator's own — the arm keys on the absolute `path`, not the id.
21
+ * The listing also requires a live session: `GET /api/skill` answers an empty array until
22
+ * one exists, so the arm opens one rather than reading a listing that would be a false
23
+ * alarm about a correct install.
24
+ *
25
+ * The pure functions below are exported and unit-tested offline against synthesized
26
+ * fixtures (`plugins/soleur/test/harness-discovery-smoke.test.ts`); the CLI-driving
27
+ * half is the same code under a real binary. NOTE: importing this module binds a
28
+ * per-process scratch session (`ensureScratchSession()` at load, below) — importers
29
+ * of the pure functions get the environment mutation too; it is a no-op nested under
30
+ * an already-bound root.
31
+ *
32
+ * `scripts/codex-plugin-smoke.mjs` is deliberately UNTOUCHED: it is the operator-local
33
+ * check of an already-installed plugin and its hooks, and needs `~/.codex` state this
34
+ * gate refuses to depend on.
35
+ */
36
+
37
+ import { execFileSync, spawn, spawnSync } from "child_process";
38
+ import { randomBytes } from "crypto";
39
+ import {
40
+ closeSync,
41
+ existsSync,
42
+ mkdirSync,
43
+ mkdtempSync,
44
+ openSync,
45
+ readFileSync,
46
+ readdirSync,
47
+ realpathSync,
48
+ rmSync,
49
+ writeFileSync,
50
+ } from "fs";
51
+ import { tmpdir } from "os";
52
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
53
+ import { ensureScratchSession } from "../test/lib/scratch-session";
54
+ import { gitCleanEnv } from "../test/lib/git-clean-env";
55
+ import { PLUGIN_ROOT_TOKEN } from "./sync-opencode-adapter";
56
+
57
+ // Bind a per-process scratch session root (#7004, ADR-250) BEFORE anything
58
+ // allocates: this script runs via `bun <file>` (ci.yml), where the bunfig
59
+ // [test].preload does NOT run, so this statement IS the adoption — scratchDir()
60
+ // below then lands every harness-discovery-* home/repo under an owned
61
+ // soleur-run.<pid>.* root the reapers can attribute when a run is SIGKILLed
62
+ // (clean exits were already covered by SCRATCH[]/cleanupScratch()). A nested
63
+ // run under a runner inherits the parent's root and never deletes it.
64
+ ensureScratchSession();
65
+
66
+ export const REPO_ROOT = resolve(import.meta.dir, "../../..");
67
+ const PLUGIN_ROOT = resolve(REPO_ROOT, "plugins/soleur");
68
+
69
+ /** Per-call wall-clock cap. A vendor CLI that hangs must not hold the CI job open. */
70
+ export const CLI_TIMEOUT_MS = 90_000;
71
+
72
+ export type Harness = "codex" | "devin" | "opencode";
73
+
74
+ /**
75
+ * Scratch directories this process created, removed on the way out.
76
+ *
77
+ * `mkdtempSync` has no owner otherwise: a CI runner is thrown away so nothing shows there, but
78
+ * the same script run locally (which is how the parsers get debugged) leaves a fresh `CODEX_HOME`
79
+ * and a scratch git repo in `$TMPDIR` on every invocation. Registered rather than removed inline
80
+ * because every arm below can return early, including the throwing ones.
81
+ */
82
+ const SCRATCH: string[] = [];
83
+ function scratchDir(prefix: string): string {
84
+ const d = mkdtempSync(join(tmpdir(), prefix));
85
+ SCRATCH.push(d);
86
+ return d;
87
+ }
88
+ function cleanupScratch(): void {
89
+ for (const d of SCRATCH.splice(0)) {
90
+ try {
91
+ rmSync(d, { recursive: true, force: true });
92
+ } catch {
93
+ // Best effort: a leaked scratch dir must never change this gate's verdict.
94
+ }
95
+ }
96
+ }
97
+
98
+ export interface Verdict {
99
+ missing: string[];
100
+ extra: string[];
101
+ badMultiplicity: string[];
102
+ /** Which multiplicity mode the run exhibited, once inferred. */
103
+ mode: "dedup" | "additive" | "single" | "indeterminate";
104
+ }
105
+
106
+ /**
107
+ * The expectation, derived from the manifest's own `skills` roots.
108
+ *
109
+ * Keyed on the DIRECTORY BASENAME, never on frontmatter `name:`: a skill that loses
110
+ * its `name:` must still appear in the expected set, so the CLI omitting it reads as
111
+ * MISSING rather than as a shrunken expectation that quietly agrees.
112
+ *
113
+ * The value is the number of declared roots containing that name — its `k`. The
114
+ * Codex `go`/`help`/`sync` duplication is therefore DERIVED from the manifest rather
115
+ * than acked by hand, so the allowance expires on its own when #8236 deletes a root.
116
+ */
117
+ export function expectedSkills(manifestPath: string, pluginRoot = PLUGIN_ROOT): Map<string, number> {
118
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { skills?: string[] };
119
+ const roots = manifest.skills ?? [];
120
+ if (roots.length === 0) throw new Error(`${manifestPath} declares no skills roots`);
121
+ const counts = new Map<string, number>();
122
+ for (const root of roots) {
123
+ const dir = resolve(pluginRoot, root);
124
+ // THROW, never `continue`. A typo'd or deleted root silently contributing
125
+ // zero is the "shrunken expectation that quietly agrees" this function's
126
+ // header refuses one level down (at the skill-NAME level): the manifest
127
+ // declares the root, so its absence is a defect in the thing under test,
128
+ // not a reason to expect less of it. Measured consequence of the old form:
129
+ // `"skills": ["./skilz", …]` collapsed the expectation to the 3 shim stems,
130
+ // all 3 were discovered, and the gate exited 0 on a 99-skill loss.
131
+ if (!existsSync(dir)) {
132
+ throw new Error(`${manifestPath}: declared skills root '${root}' does not exist at ${dir}`);
133
+ }
134
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
135
+ if (!entry.isDirectory()) continue;
136
+ if (!existsSync(join(dir, entry.name, "SKILL.md"))) continue;
137
+ const name = `soleur:${entry.name}`;
138
+ counts.set(name, (counts.get(name) ?? 0) + 1);
139
+ }
140
+ }
141
+ // Roots that exist but hold no skill. Same argument as the throw above, one
142
+ // level out: an empty expectation makes `verdict()` trivially clean and the
143
+ // whole gate exits 0 having compared nothing against nothing.
144
+ if (counts.size === 0) {
145
+ throw new Error(`${manifestPath}: declared roots ${JSON.stringify(roots)} contain no SKILL.md`);
146
+ }
147
+ return counts;
148
+ }
149
+
150
+ /**
151
+ * Canonicalise a path for comparison: symlinked scratch roots (`/var` → `/private/var`
152
+ * on macOS, a linked worktree anywhere) make the server's reported `path` and this
153
+ * process's `PLUGIN_ROOT` differ as STRINGS while denoting the same file.
154
+ */
155
+ function canonical(p: string): string {
156
+ try {
157
+ return realpathSync(p);
158
+ } catch {
159
+ return resolve(p);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * A path relative to the plugin root, with `/` separators, or `undefined` when it lies
165
+ * outside. Returning `undefined` rather than a `../…` string is what EXCLUDES global
166
+ * leakage: an operator's `~/.claude/skills/plan/SKILL.md` and the harness's own
167
+ * `/builtin/opencode.md` both carry ids this plugin also uses, so counting anything the
168
+ * declaration did not place under the plugin root would report them as registrations.
169
+ */
170
+ function pluginRelative(pluginRoot: string, p: string): string | undefined {
171
+ const rel = relative(canonical(pluginRoot), canonical(p));
172
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return undefined;
173
+ return rel.split(sep).join("/");
174
+ }
175
+
176
+ /**
177
+ * The expectation for the OpenCode arm, derived from the generated fragment's own
178
+ * `skills` roots (measurement §11.0). Keyed on the plugin-relative `path`, because the
179
+ * server's ids are bare basenames that collide across roots (§2.3).
180
+ *
181
+ * The `__SOLEUR_PLUGIN_ROOT__` sentinel is resolved here with the same substitution the
182
+ * installer performs (D6): the committed fragment is machine-independent, and the arm
183
+ * must judge the bytes a user would actually install.
184
+ */
185
+ export function expectedOpenCodeSkillFiles(manifestPath: string, pluginRoot = PLUGIN_ROOT): Map<string, number> {
186
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { skills?: string[] };
187
+ const roots = manifest.skills ?? [];
188
+ if (roots.length === 0) throw new Error(`${manifestPath} declares no skills roots`);
189
+ const counts = new Map<string, number>();
190
+ for (const root of roots) {
191
+ const dir = resolve(pluginRoot, root.split(PLUGIN_ROOT_TOKEN).join(pluginRoot));
192
+ // THROW, never `continue` — the same argument as `expectedSkills()` above: a
193
+ // declared root the installer would have written is an assertion, and a typo'd
194
+ // one silently shrinking the expectation is how a 103-skill loss reads green.
195
+ if (!existsSync(dir)) {
196
+ throw new Error(`${manifestPath}: declared skills root '${root}' does not exist at ${dir}`);
197
+ }
198
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
199
+ if (!entry.isDirectory()) continue;
200
+ const file = join(dir, entry.name, "SKILL.md");
201
+ if (!existsSync(file)) continue;
202
+ const rel = pluginRelative(pluginRoot, file);
203
+ if (rel) counts.set(rel, 1);
204
+ }
205
+ }
206
+ if (counts.size === 0) {
207
+ throw new Error(`${manifestPath}: declared roots ${JSON.stringify(roots)} contain no SKILL.md`);
208
+ }
209
+ return counts;
210
+ }
211
+
212
+ /**
213
+ * `GET /api/skill` returns `{location, data: [{id, name, description, path, content}]}`
214
+ * (measurement §11.4), one record per registration — and `content` is the skill's full
215
+ * text, so the payload runs to megabytes and must be read from a FILE, never a pipe.
216
+ *
217
+ * Keyed on `path`, not `id`: ids are bare basenames with no namespace, so the same id
218
+ * is carried by this plugin's skill and by any operator-level copy. Counting is by
219
+ * relative path, so a duplicate registration of the SAME file still shows up.
220
+ *
221
+ * Unreadable input yields an EMPTY MAP rather than a throw: whether the bytes were a
222
+ * listing at all is `isStructurallyParseable()`'s question, and folding the two together
223
+ * is what would let a dead server grade as a set mismatch.
224
+ */
225
+ export function parseOpenCodeSkillList(raw: string, pluginRoot = PLUGIN_ROOT): Map<string, number> {
226
+ const counts = new Map<string, number>();
227
+ let payload: { data?: unknown };
228
+ try {
229
+ payload = JSON.parse(raw) as { data?: unknown };
230
+ } catch {
231
+ return counts;
232
+ }
233
+ if (!Array.isArray(payload?.data)) return counts;
234
+ for (const entry of payload.data as Array<{ path?: unknown }>) {
235
+ if (typeof entry?.path !== "string") continue;
236
+ const rel = pluginRelative(pluginRoot, entry.path);
237
+ if (!rel) continue;
238
+ counts.set(rel, (counts.get(rel) ?? 0) + 1);
239
+ }
240
+ return counts;
241
+ }
242
+
243
+ /**
244
+ * `codex debug prompt-input` emits the skills list inside an ESCAPED JSON STRING —
245
+ * `\n`, not real newlines — so a line-anchored regex over the raw bytes returns zero.
246
+ * Decoding first is the whole reason this is a function rather than a grep.
247
+ */
248
+ export function parseCodexPromptInput(raw: string): Map<string, number> {
249
+ let text = raw;
250
+ const decoded = raw.replace(/\\n/g, "\n");
251
+ if (decoded !== raw) text = decoded;
252
+ const counts = new Map<string, number>();
253
+ for (const m of text.matchAll(/^\s*-\s+(soleur:[a-z0-9-]+):/gm)) {
254
+ counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
255
+ }
256
+ return counts;
257
+ }
258
+
259
+ /** `devin skills list` prints ` /soleur:<name> …` one per registration. */
260
+ export function parseDevinSkillsList(raw: string): Map<string, number> {
261
+ const counts = new Map<string, number>();
262
+ for (const m of raw.matchAll(/^\s+\/(soleur:[a-z0-9-]+)(?:\s|$)/gm)) {
263
+ counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
264
+ }
265
+ return counts;
266
+ }
267
+
268
+ /** Did the listing carry the structure a parse depends on, at all? */
269
+ export function isStructurallyParseable(harness: Harness, raw: string, discovered: Map<string, number>): boolean {
270
+ if (discovered.size > 0) return true;
271
+ if (harness === "codex") return /<skills_instructions>/.test(raw);
272
+ if (harness === "opencode") {
273
+ // The marker is the payload's `data` array. An empty one is a READING, not an
274
+ // outage: a correct install answers `{location, data: []}` before a session
275
+ // exists (measurement §11.1), and a total loss must grade exit 1 naming the
276
+ // missing skills rather than `unparseable-output` (exit 3, UNRESOLVED) — the
277
+ // same AP-021 inversion the Devin arm below refuses.
278
+ try {
279
+ const payload = JSON.parse(raw) as { data?: unknown };
280
+ return Array.isArray(payload?.data);
281
+ } catch {
282
+ return false;
283
+ }
284
+ }
285
+ // Devin has a structural marker too, and using it is what keeps the two arms
286
+ // honest. Without it, "the CLI answered and listed NOTHING" — a total loss of
287
+ // all 102 registrations, the worst regression this gate exists to catch —
288
+ // graded `unparseable-output` (exit 3, UNRESOLVED) on Devin while the
289
+ // identical Codex regression graded exit 1 with the names listed. That is the
290
+ // AP-021 inversion the exit taxonomy below forbids in the other direction,
291
+ // and it would have been the triage signal for #8574's soak.
292
+ return /^\s*Available skills:/m.test(raw);
293
+ }
294
+
295
+ /**
296
+ * ONE MODE PER HARNESS RUN.
297
+ *
298
+ * A name found in k declared roots may be listed once (the harness dedups) or k times
299
+ * (it is additive) — but not a MIXTURE. Checking "1 or k" per name independently would
300
+ * pass a listing with `go` twice and `help` once, which is exactly the loader ambiguity
301
+ * ADR-224 decision 5 exists to detect. So the mode is INFERRED across every multi-root
302
+ * name, and a mixture goes to badMultiplicity.
303
+ *
304
+ * Measured 2026-09-23: Codex 0.156.1 is additive (go/help/sync twice, 105 entries for
305
+ * 102 names); Devin 3000.11.1 dedups (102 entries, each once).
306
+ */
307
+ export function verdict(expected: Map<string, number>, discovered: Map<string, number>): Verdict {
308
+ const missing: string[] = [];
309
+ const extra: string[] = [];
310
+ const badMultiplicity: string[] = [];
311
+
312
+ for (const name of discovered.keys()) if (!expected.has(name)) extra.push(name);
313
+
314
+ const multiRoot = [...expected.entries()].filter(([, k]) => k >= 2);
315
+ let mode: Verdict["mode"] = "indeterminate";
316
+ if (multiRoot.length === 0) {
317
+ // No name is declared by more than one root, so there is no cross-root
318
+ // multiplicity to infer and `indeterminate` would read as a defect. This is
319
+ // the OpenCode shape: one canonical tree, no shim roots.
320
+ mode = "single";
321
+ } else {
322
+ // `n === 0` is MISSING, and is reported as such below. Feeding it into the
323
+ // mode inference too makes an ordinary omission arrive as "3 missing, 0
324
+ // extra, 3 bad-multiplicity" — a bogus multiplicity story layered over the
325
+ // real cause, which is the operator's first read of a red run.
326
+ const observed = multiRoot
327
+ .map(([name, k]) => ({ name, k, n: discovered.get(name) ?? 0 }))
328
+ .filter((o) => o.n > 0);
329
+ const allOne = observed.every((o) => o.n === 1);
330
+ const allK = observed.every((o) => o.n === o.k);
331
+ if (allOne) mode = "dedup";
332
+ else if (allK) mode = "additive";
333
+ else {
334
+ mode = "indeterminate";
335
+ for (const o of observed) {
336
+ if (o.n !== 1 && o.n !== o.k) {
337
+ badMultiplicity.push(`${o.name}: listed ${o.n} times, expected 1 (dedup) or ${o.k} (additive)`);
338
+ }
339
+ }
340
+ if (badMultiplicity.length === 0) {
341
+ badMultiplicity.push(
342
+ `mixed multiplicity mode across multi-root names: ${observed
343
+ .map((o) => `${o.name}=${o.n}/k${o.k}`)
344
+ .join(", ")} — a harness must dedup consistently or be additive consistently`,
345
+ );
346
+ }
347
+ }
348
+ }
349
+
350
+ for (const [name, k] of expected) {
351
+ const n = discovered.get(name) ?? 0;
352
+ if (n === 0) {
353
+ missing.push(name);
354
+ continue;
355
+ }
356
+ const allowed = mode === "dedup" ? [1] : mode === "additive" ? [k] : [1, k];
357
+ if (!allowed.includes(n)) {
358
+ badMultiplicity.push(`${name}: listed ${n} times, expected ${allowed.join(" or ")}`);
359
+ }
360
+ }
361
+
362
+ return { missing, extra, badMultiplicity, mode };
363
+ }
364
+
365
+ export interface ExitInput {
366
+ cliPresent: boolean;
367
+ installFailed?: boolean;
368
+ /** The probe command itself failed (non-zero, or killed at CLI_TIMEOUT_MS). */
369
+ probeFailed?: boolean;
370
+ version?: string;
371
+ pin?: string;
372
+ parseable: boolean;
373
+ verdict?: Verdict;
374
+ }
375
+
376
+ export interface ExitResult {
377
+ code: 0 | 1 | 3;
378
+ reason: string;
379
+ }
380
+
381
+ /**
382
+ * The exit decision, PURE and exported — the two rows carrying the whole "never exit 0
383
+ * on an empty or unparsed listing" contract are decided in the CLI-driving path
384
+ * otherwise, where no unit test can reach them.
385
+ *
386
+ * ADR-177 taxonomy. Exit 3 is UNRESOLVED and is reserved for a listing that is
387
+ * STRUCTURALLY unreadable — no `<skills_instructions>` marker, or zero `soleur:` lines.
388
+ * A listing that PARSED but is missing names is a real partial loss and exits 1 naming
389
+ * them: mapping "discovered < 100" to exit 3 would report a 60-skill regression as
390
+ * "could not check", which names a cause the gate did not measure (AP-021).
391
+ */
392
+ export function resolveExit(input: ExitInput): ExitResult {
393
+ if (!input.cliPresent) return { code: 3, reason: "cli-missing" };
394
+ // "Could not read the version" is NOT "the version is fine". `cliVersion()`
395
+ // returns undefined whenever `--version` exits non-zero or prints no semver,
396
+ // and the old `pin && version && …` form skipped the comparison entirely in
397
+ // exactly that case — so `--pin` failed OPEN on an unreadable binary, which
398
+ // is the one mapping the ADR-177 taxonomy exists to forbid.
399
+ if (input.pin && !input.version) return { code: 3, reason: "version-unknown" };
400
+ if (input.pin && input.version && input.version !== input.pin) {
401
+ return { code: 3, reason: `version-mismatch:${input.version}!=${input.pin}` };
402
+ }
403
+ if (input.installFailed) return { code: 3, reason: "install-failed" };
404
+ // A probe that died (timeout, crash, non-zero) yields PARTIAL output, and a
405
+ // partial listing parses. Without this, a CLI that printed 40 of 102 names
406
+ // and then hung reported "set-mismatch: 62 missing" — an authoritative claim
407
+ // of a regression that did not happen, from a run that could not measure one.
408
+ if (input.probeFailed) return { code: 3, reason: "probe-failed" };
409
+ if (!input.parseable) return { code: 3, reason: "unparseable-output" };
410
+ const v = input.verdict;
411
+ if (!v) return { code: 3, reason: "unparseable-output" };
412
+ const problems = v.missing.length + v.extra.length + v.badMultiplicity.length;
413
+ if (problems > 0) {
414
+ return {
415
+ code: 1,
416
+ reason: `set-mismatch: ${v.missing.length} missing, ${v.extra.length} extra, ${v.badMultiplicity.length} bad-multiplicity`,
417
+ };
418
+ }
419
+ return { code: 0, reason: `ok (mode=${v.mode})` };
420
+ }
421
+
422
+ /**
423
+ * Neutralise text before it reaches a GitHub Actions log.
424
+ *
425
+ * WHY THIS IS NOT PARANOIA. GitHub parses a WORKFLOW COMMAND from any log line whose first
426
+ * non-whitespace characters are `::` — `::error::`, `::add-mask::`, `::set-output` and, worst,
427
+ * `::stop-commands::`. Everything this script prints on a red path is derived from two places a
428
+ * pull request can write to:
429
+ *
430
+ * - the vendor CLI's raw output, which ECHOES skill names, and
431
+ * - `verdict()`'s `missing` / `extra` / `badMultiplicity` arrays, whose members are built from
432
+ * `readdirSync` over `plugins/soleur/skills/` — i.e. from DIRECTORY NAMES in the checkout.
433
+ *
434
+ * A directory name may contain `:`, and on a fork PR the checkout is the fork's. So a fork could
435
+ * add `skills/<name-containing-a-newline-and-::error::…>/SKILL.md` and have this job emit
436
+ * annotations, mask arbitrary strings out of the log, or issue `::stop-commands::` and silence
437
+ * every real annotation after it. The job is advisory and cannot gate a merge, but a log that can
438
+ * be authored by the thing under test is not evidence.
439
+ *
440
+ * TWO LAYERS, because either alone has a hole:
441
+ *
442
+ * 1. Per-line neutralisation. A line that would start a command gets one leading space, which
443
+ * GitHub does NOT trim back into a command. Applied after normalising `\r`, since a lone CR
444
+ * also begins a new line on a terminal.
445
+ * 2. A `::stop-commands::<token>` fence around the whole block, with a 128-bit random token.
446
+ * Layer 1 is what makes the fence safe: without it, untrusted text containing the resume
447
+ * token would end the fence early, and with it no untrusted line can be a command at all.
448
+ *
449
+ * C0 control characters other than `\n` and `\t` are dropped outright — they carry no
450
+ * information here and are how a name hides what it really is in a terminal.
451
+ */
452
+ export function sanitizeForLog(text: string): string {
453
+ return text
454
+ .replace(/\r\n?/g, "\n")
455
+ // eslint-disable-next-line no-control-regex -- the C0 set is the subject, not a typo (cq-regex-unicode-separators-escape-only)
456
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
457
+ .split("\n")
458
+ .map((line) => (/^\s*::/.test(line) ? ` ${line}` : line))
459
+ .join("\n");
460
+ }
461
+
462
+ /** Write untrusted text to stderr inside a random-token workflow-command fence. */
463
+ function writeUntrusted(text: string): void {
464
+ const token = randomBytes(16).toString("hex");
465
+ process.stderr.write(`::stop-commands::${token}\n`);
466
+ process.stderr.write(sanitizeForLog(text));
467
+ if (!text.endsWith("\n")) process.stderr.write("\n");
468
+ process.stderr.write(`::${token}::\n`);
469
+ }
470
+
471
+ function run(cmd: string, args: string[], env: Record<string, string>, cwd = REPO_ROOT): { ok: boolean; out: string } {
472
+ try {
473
+ const out = execFileSync(cmd, args, {
474
+ cwd,
475
+ env,
476
+ encoding: "utf8",
477
+ timeout: CLI_TIMEOUT_MS,
478
+ maxBuffer: 32 * 1024 * 1024,
479
+ stdio: ["ignore", "pipe", "pipe"],
480
+ });
481
+ return { ok: true, out };
482
+ } catch (e) {
483
+ const err = e as { stdout?: string; stderr?: string; message?: string };
484
+ return { ok: false, out: `${err.stdout ?? ""}\n${err.stderr ?? ""}\n${err.message ?? ""}` };
485
+ }
486
+ }
487
+
488
+ function have(bin: string): boolean {
489
+ try {
490
+ execFileSync("command", ["-v", bin], { shell: "/bin/bash", stdio: "ignore" });
491
+ return true;
492
+ } catch {
493
+ return false;
494
+ }
495
+ }
496
+
497
+ function cliVersion(bin: string, env: Record<string, string> = { PATH: process.env.PATH ?? "" }): string | undefined {
498
+ const r = run(bin, ["--version"], env);
499
+ if (!r.ok) return undefined;
500
+ const m = r.out.match(/\d+\.\d+\.\d+/);
501
+ return m ? m[0] : undefined;
502
+ }
503
+
504
+ function driveCodex(pin?: string): { input: ExitInput; raw: string } {
505
+ if (!have("codex")) return { input: { cliPresent: false, parseable: false }, raw: "" };
506
+ const home = scratchDir("harness-discovery-codex-");
507
+ const env = { PATH: process.env.PATH ?? "", HOME: home, CODEX_HOME: home };
508
+ const version = cliVersion("codex");
509
+ if (pin && version && version !== pin) {
510
+ return { input: { cliPresent: true, parseable: false, version, pin }, raw: "" };
511
+ }
512
+ const add = run("codex", ["plugin", "marketplace", "add", REPO_ROOT], env);
513
+ const install = run("codex", ["plugin", "add", "soleur@soleur"], env);
514
+ if (!add.ok || !install.ok) {
515
+ return { input: { cliPresent: true, installFailed: true, parseable: false, version, pin }, raw: `${add.out}\n${install.out}` };
516
+ }
517
+ const probe = run("codex", ["debug", "prompt-input"], env);
518
+ const discovered = parseCodexPromptInput(probe.out);
519
+ const parseable = isStructurallyParseable("codex", probe.out, discovered);
520
+ const expected = expectedSkills(resolve(PLUGIN_ROOT, ".codex-plugin/plugin.json"));
521
+ const usable = probe.ok && parseable;
522
+ return {
523
+ input: {
524
+ cliPresent: true,
525
+ probeFailed: !probe.ok,
526
+ parseable,
527
+ version,
528
+ pin,
529
+ verdict: usable ? verdict(expected, discovered) : undefined,
530
+ },
531
+ raw: probe.out,
532
+ };
533
+ }
534
+
535
+ function driveDevin(pin?: string): { input: ExitInput; raw: string } {
536
+ if (!have("devin")) return { input: { cliPresent: false, parseable: false }, raw: "" };
537
+ const home = scratchDir("harness-discovery-devin-");
538
+ const scratch = scratchDir("harness-discovery-devin-repo-");
539
+ const env = {
540
+ PATH: process.env.PATH ?? "",
541
+ HOME: home,
542
+ XDG_CONFIG_HOME: join(home, "config"),
543
+ XDG_DATA_HOME: join(home, "data"),
544
+ };
545
+ const version = cliVersion("devin");
546
+ if (pin && version && version !== pin) {
547
+ return { input: { cliPresent: true, parseable: false, version, pin }, raw: "" };
548
+ }
549
+ const init = run("git", ["init", "-q", scratch], env, scratch);
550
+ if (!init.ok) {
551
+ // Otherwise the scratch dir is not a repo, Devin reads no config, and the
552
+ // empty listing grades `unparseable-output` — naming a cause we did not
553
+ // measure when the real one is right here.
554
+ return { input: { cliPresent: true, installFailed: true, parseable: false, version, pin }, raw: init.out };
555
+ }
556
+ mkdirSync(join(scratch, ".devin"), { recursive: true });
557
+ writeFileSync(
558
+ join(scratch, ".devin/config.json"),
559
+ // The repo-scoped local source: `devin plugins install` requires auth, this does not.
560
+ JSON.stringify({ requiredPlugins: [{ source: "local", path: PLUGIN_ROOT }] }, null, 2),
561
+ );
562
+ const probe = run("devin", ["skills", "list"], env, scratch);
563
+ const discovered = parseDevinSkillsList(probe.out);
564
+ const parseable = isStructurallyParseable("devin", probe.out, discovered);
565
+ const expected = expectedSkills(resolve(PLUGIN_ROOT, ".devin-plugin/plugin.json"));
566
+ const usable = probe.ok && parseable;
567
+ return {
568
+ input: {
569
+ cliPresent: true,
570
+ probeFailed: !probe.ok,
571
+ parseable,
572
+ version,
573
+ pin,
574
+ verdict: usable ? verdict(expected, discovered) : undefined,
575
+ },
576
+ raw: probe.out,
577
+ };
578
+ }
579
+
580
+ /** Synchronous sleep, so the server-poll loop keeps this file's all-sync shape. */
581
+ function sleepMs(ms: number): void {
582
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
583
+ }
584
+
585
+ /** How long a spawned `opencode serve` has to announce its port and password. */
586
+ export const SERVER_BOOT_TIMEOUT_MS = 60_000;
587
+
588
+ /**
589
+ * How long the location has to finish SCANNING its skill roots before the reading is
590
+ * taken. Measured §11.6: the scan is asynchronous and the first read is always empty;
591
+ * a hermetic install settles to all 103 canonical skills in **~2 s**, and the poll exits
592
+ * the moment the expected population is reached. The cap is therefore a ceiling on a
593
+ * failure, not a per-run cost.
594
+ */
595
+ export const SKILL_SETTLE_TIMEOUT_MS = 120_000;
596
+
597
+ /**
598
+ * The absolute path to a binary.
599
+ *
600
+ * `opencode` on PATH is almost always a version-manager shim, and a shim resolves its
601
+ * install through `$HOME` — which this arm redirects for hermeticity, and which makes the
602
+ * shim try to REINSTALL (measured: the run hangs, then dies with `Disk quota exceeded`).
603
+ * `readlink -f` follows the shim chain to the real launcher, which needs no HOME at all.
604
+ */
605
+ export function resolveBin(bin: string): string | undefined {
606
+ const r = run(
607
+ "bash",
608
+ ["-c", 'p=$(command -v "$1") || exit 1; r=$(readlink -f "$p" 2>/dev/null) || r="$p"; [ -n "$r" ] || r="$p"; printf "%s" "$r"', "bash", bin],
609
+ { PATH: process.env.PATH ?? "" },
610
+ );
611
+ const out = r.out.trim();
612
+ return r.ok && out !== "" ? out : undefined;
613
+ }
614
+
615
+ /**
616
+ * Run a command with its stdout going straight to a FILE.
617
+ *
618
+ * `run()`'s pipe is NOT safe for this payload. `execFileSync` silently truncates a large
619
+ * stdout — observed at ~800 kB and ~946 kB, different each run — and still reports
620
+ * SUCCESS, so the JSON arrives with an unclosed string and `JSON.parse` fails, which
621
+ * grades a correct install UNRESOLVED (measurement §11.6). The same call through a file
622
+ * descriptor yields all 3,069,017 bytes, valid. Hence the file, and hence not `run()`.
623
+ */
624
+ function runToFile(
625
+ cmd: string,
626
+ args: string[],
627
+ env: Record<string, string>,
628
+ cwd: string,
629
+ outPath: string,
630
+ ): { ok: boolean; err: string } {
631
+ const fd = openSync(outPath, "w");
632
+ try {
633
+ const r = spawnSync(cmd, args, {
634
+ cwd,
635
+ env,
636
+ stdio: ["ignore", fd, "pipe"],
637
+ timeout: CLI_TIMEOUT_MS,
638
+ encoding: "utf8",
639
+ });
640
+ return { ok: r.status === 0, err: r.stderr ?? "" };
641
+ } finally {
642
+ closeSync(fd);
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Drive a real `opencode serve` and read `GET /api/skill` from it.
648
+ *
649
+ * Every step is forced by the measured recipe (measurement §11.0/§11.6):
650
+ *
651
+ * - `serve` is a LONG-RUNNING process, so this is the one arm that is not a single
652
+ * `execFileSync`. It is spawned detached into a log file and polled; the log is also
653
+ * where the port and password come from.
654
+ * - `--port 0` lets the OS pick, and the banner names the chosen port, so two CI jobs
655
+ * cannot race for a fixed one.
656
+ * - Every `/api/*` route is `UnauthorizedError` without that password, and `opencode
657
+ * api` calls `GET /api/info` first.
658
+ * - The listing is built by an ASYNCHRONOUS scan of the location's skill roots, so the
659
+ * first read is always empty and a single read reports a correct install as a total
660
+ * loss. The poll below is the whole reason this is not a single call.
661
+ * - The listing is read from a FILE, never from `run()`'s pipe — see `runToFile`.
662
+ *
663
+ * Deliberately NOT part of the recipe: `POST /api/session` (measured unnecessary — an
664
+ * idle boot settles) and `POST /api/location/reload` (measured to CLEAR the listing
665
+ * before rebuilding it, which would make the poll race its own reset).
666
+ */
667
+ /**
668
+ * Is a child pid still alive? The `error`/`exit` events cannot fire while the boot loop blocks
669
+ * the thread, so liveness is asked synchronously instead of awaited: `kill(pid, 0)` performs no
670
+ * signal and throws `ESRCH` for a process that is gone.
671
+ */
672
+ function processAlive(pid: number | undefined): boolean {
673
+ if (pid === undefined) return false;
674
+ try {
675
+ process.kill(pid, 0);
676
+ return true;
677
+ } catch {
678
+ return false;
679
+ }
680
+ }
681
+
682
+ function openCodeSkillProbe(
683
+ bin: string,
684
+ project: string,
685
+ env: Record<string, string>,
686
+ expected: Map<string, number>,
687
+ ): { ok: boolean; raw: string } {
688
+ const logPath = join(project, "serve.log");
689
+ const fd = openSync(logPath, "w");
690
+ const child = spawn(bin, ["serve", "--port", "0", "--hostname", "127.0.0.1"], {
691
+ cwd: project,
692
+ env,
693
+ stdio: ["ignore", fd, fd],
694
+ detached: true,
695
+ });
696
+ let spawnError: Error | undefined;
697
+ child.on("error", (e: Error) => {
698
+ spawnError = e;
699
+ });
700
+ child.unref();
701
+ closeSync(fd);
702
+
703
+ const readLog = (): string => {
704
+ try {
705
+ return readFileSync(logPath, "utf8");
706
+ } catch {
707
+ return "";
708
+ }
709
+ };
710
+
711
+ try {
712
+ let url: string | undefined;
713
+ let password: string | undefined;
714
+ const bootDeadline = Date.now() + SERVER_BOOT_TIMEOUT_MS;
715
+ while (Date.now() < bootDeadline && !(url && password)) {
716
+ const text = readLog();
717
+ url ??= text.match(/server listening on (http:\/\/[^\s]+)/)?.[1];
718
+ password ??= text.match(/server password (\S+)/)?.[1];
719
+ if (url && password) break;
720
+ sleepMs(250);
721
+ }
722
+ if (!url || !password) {
723
+ // `spawnError` is set by the child's `error` event, and no event handler can run while
724
+ // this loop blocks the thread in `Atomics.wait` (`sleepMs`) — so the check is HERE, after
725
+ // the loop, where it is reachable. A child that never spawned (no pid) or already
726
+ // exited is a DEAD PROBE (UNRESOLVED), not a slow boot: grading it as "did not announce
727
+ // itself" would report a correct install as a set mismatch.
728
+ if (spawnError !== undefined || !processAlive(child.pid)) {
729
+ const why = spawnError?.message ?? `process ${String(child.pid)} is not running`;
730
+ return { ok: false, raw: `server failed to start: ${why}\n${readLog().slice(0, 4096)}` };
731
+ }
732
+ return {
733
+ ok: false,
734
+ raw: `server did not announce itself within ${SERVER_BOOT_TIMEOUT_MS}ms:\n${readLog().slice(0, 4096)}`,
735
+ };
736
+ }
737
+
738
+ const apiEnv = { ...env, OPENCODE_PASSWORD: password };
739
+ const outPath = join(project, "skills.json");
740
+ const deadline = Date.now() + SKILL_SETTLE_TIMEOUT_MS;
741
+ let last = "";
742
+ let failures = 0;
743
+ for (;;) {
744
+ const listing = runToFile(bin, ["api", "--server", url, "GET", "/api/skill"], apiEnv, project, outPath);
745
+ let text = "";
746
+ try {
747
+ text = readFileSync(outPath, "utf8");
748
+ } catch {
749
+ text = "";
750
+ }
751
+ if (listing.ok) {
752
+ failures = 0;
753
+ last = text;
754
+ if (parseOpenCodeSkillList(last).size >= expected.size) break;
755
+ } else {
756
+ last = `listing failed (status != 0): ${listing.err}\n--- partial payload ---\n${text.slice(0, 2048)}`;
757
+ // A transport that keeps failing is a dead probe, not an empty listing: five
758
+ // consecutive failures end the arm as UNRESOLVED rather than as a set mismatch.
759
+ if (++failures >= 5) return { ok: false, raw: last };
760
+ }
761
+ if (Date.now() >= deadline) break;
762
+ sleepMs(1000);
763
+ }
764
+ return { ok: true, raw: last };
765
+ } finally {
766
+ // Detached, so the child leads its own process group: signalling the group also
767
+ // reaps anything the server forked, and leaves no listener behind on a red run.
768
+ if (child.pid) {
769
+ try {
770
+ process.kill(-child.pid, "SIGTERM");
771
+ } catch {
772
+ try {
773
+ child.kill("SIGTERM");
774
+ } catch {
775
+ // Already gone — nothing to clean up.
776
+ }
777
+ }
778
+ }
779
+ }
780
+ }
781
+
782
+ function driveOpenCode(pin?: string): { input: ExitInput; raw: string } {
783
+ if (!have("opencode")) return { input: { cliPresent: false, parseable: false }, raw: "" };
784
+ const home = scratchDir("harness-discovery-opencode-");
785
+ const project = scratchDir("harness-discovery-opencode-repo-");
786
+ const configDir = join(home, ".config", "opencode");
787
+ // A fully hermetic HOME + XDG set: the operator's own config, and the `~/.claude/skills`
788
+ // / `~/.agents/skills` trees this harness also reads, are then out of reach — measured
789
+ // §11.4 to be the only control needed for the reading to be this plugin's 103 skills
790
+ // plus the harness's own 2 built-ins, with no "other" bucket at all. The absolute
791
+ // binary (`resolveBin`) is what makes a hermetic HOME safe: a PATH shim resolves its
792
+ // install THROUGH `$HOME` and reinstalls into the scratch dir without one.
793
+ const env = gitCleanEnv({
794
+ HOME: home,
795
+ PATH: process.env.PATH ?? "",
796
+ XDG_CONFIG_HOME: join(home, ".config"),
797
+ XDG_DATA_HOME: join(home, ".local", "share"),
798
+ XDG_STATE_HOME: join(home, ".local", "state"),
799
+ });
800
+ const bin = resolveBin("opencode") ?? "opencode";
801
+ const version = cliVersion(bin, env);
802
+ if (pin && version && version !== pin) {
803
+ return { input: { cliPresent: true, parseable: false, version, pin }, raw: "" };
804
+ }
805
+ const init = run("git", ["init", "-q", project], env, project);
806
+ if (!init.ok) {
807
+ // `opencode serve` opens a project, and the scratch dir must be a repo for that to
808
+ // mean anything. A failure here is install-shaped, not a parse one.
809
+ return { input: { cliPresent: true, installFailed: true, parseable: false, version, pin }, raw: init.out };
810
+ }
811
+ // The user-facing installer, not a hand-rolled copy of what it does: the arm must
812
+ // judge the bytes an operator actually gets, sentinel and all.
813
+ const install = run(
814
+ process.execPath,
815
+ [resolve(PLUGIN_ROOT, "scripts/install-opencode-adapter.ts"), "--target", configDir],
816
+ env,
817
+ );
818
+ if (!install.ok) {
819
+ return { input: { cliPresent: true, installFailed: true, parseable: false, version, pin }, raw: install.out };
820
+ }
821
+ const expected = expectedOpenCodeSkillFiles(resolve(PLUGIN_ROOT, "opencode/opencode.json"));
822
+ const probe = openCodeSkillProbe(bin, project, env, expected);
823
+ const discovered = parseOpenCodeSkillList(probe.raw);
824
+ const parseable = isStructurallyParseable("opencode", probe.raw, discovered);
825
+ const usable = probe.ok && parseable;
826
+ return {
827
+ input: {
828
+ cliPresent: true,
829
+ probeFailed: !probe.ok,
830
+ parseable,
831
+ version,
832
+ pin,
833
+ verdict: usable ? verdict(expected, discovered) : undefined,
834
+ },
835
+ raw: probe.raw,
836
+ };
837
+ }
838
+
839
+ const USAGE = "usage: harness-discovery-smoke.ts --harness codex|devin|opencode [--pin <version>]\n";
840
+
841
+ /**
842
+ * Argument parsing, PURE and exported so the fail-open cases below are reachable from a unit test.
843
+ *
844
+ * Both of them were live, and both fail in the direction that reports a green:
845
+ *
846
+ * - `argv[argv.indexOf("--harness") + 1]` reads `argv[0]` when the flag is ABSENT, because
847
+ * `indexOf` returns -1 and -1 + 1 is 0. `harness-discovery-smoke.ts codex` therefore ran the
848
+ * Codex arm with no flag at all, and any other bare first argument silently selected nothing.
849
+ * - `--pin` with no value yields `undefined`, which is indistinguishable from "no pin given", so
850
+ * a typo'd flag SKIPS the version comparison entirely. A pin that silently does not apply is
851
+ * worse than no pin: the job still prints its reassuring pin in the log.
852
+ *
853
+ * A flag-shaped value (`--pin --harness`) is likewise a mistake, never a version.
854
+ */
855
+ export function parseArgs(argv: readonly string[]): { harness: Harness; pin?: string } | { error: string } {
856
+ const hIdx = argv.indexOf("--harness");
857
+ if (hIdx === -1) return { error: "missing --harness" };
858
+ const harness = argv[hIdx + 1];
859
+ if (harness !== "codex" && harness !== "devin" && harness !== "opencode") {
860
+ return { error: `--harness must be codex, devin or opencode, got ${JSON.stringify(harness ?? null)}` };
861
+ }
862
+ const pIdx = argv.indexOf("--pin");
863
+ if (pIdx === -1) return { harness };
864
+ const pin = argv[pIdx + 1];
865
+ if (pin === undefined || pin.startsWith("--")) {
866
+ return { error: `--pin requires a version, got ${JSON.stringify(pin ?? null)}` };
867
+ }
868
+ return { harness, pin };
869
+ }
870
+
871
+ function main(): void {
872
+ const parsed = parseArgs(process.argv.slice(2));
873
+ if ("error" in parsed) {
874
+ process.stderr.write(`harness-discovery: ${parsed.error}\n${USAGE}`);
875
+ process.exit(2);
876
+ }
877
+ const { harness, pin } = parsed;
878
+
879
+ let result: ExitResult;
880
+ let input: ExitInput;
881
+ let raw: string;
882
+ try {
883
+ const arms = { codex: driveCodex, devin: driveDevin, opencode: driveOpenCode } as const;
884
+ ({ input, raw } = arms[harness](pin));
885
+ result = resolveExit(input);
886
+ } finally {
887
+ // Before the writes below, and before any exit: `process.exit()` runs no `finally`.
888
+ cleanupScratch();
889
+ }
890
+
891
+ if (input.verdict) {
892
+ // Untrusted: `missing`/`extra` members come from directory names in the checkout.
893
+ writeUntrusted(JSON.stringify({ harness, ...input.verdict }, null, 2));
894
+ }
895
+ // Trusted: every field here is produced by this file.
896
+ process.stderr.write(`harness-discovery[${harness}]: exit=${result.code} reason=${result.reason}\n`);
897
+ if (result.code !== 0) {
898
+ // The first 4 KB of raw vendor output, so a red job is diagnosable from the log
899
+ // itself and needs no artifact upload. Untrusted — see `sanitizeForLog`.
900
+ process.stderr.write(`--- raw ${harness} output (first 4 KB) ---\n`);
901
+ writeUntrusted(raw.slice(0, 4096));
902
+ }
903
+ process.exit(result.code);
904
+ }
905
+
906
+ if (import.meta.main) main();