@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,1236 @@
1
+ #!/usr/bin/env bash
2
+ # operator-script.sh — shared primitives for Soleur-generated operator scripts.
3
+ #
4
+ # SOURCED, NEVER EXECUTED. The shebang is the line-1 convention of every sibling
5
+ # in this directory (`proc.sh`, `session-state.sh`, `domain-model-lib.sh`) and
6
+ # is what lets shellcheck infer the dialect; the file carries no exec bit. There
7
+ # is ONE distribution mode (the generated script `source`s this file), so there
8
+ # is no inlined copy to drift from and no `STAGES` byte identity marker to pin.
9
+ # Plan 2026-09-18-feat-ship-operator-bootstrap-wizard-merge-danger, revision R6.
10
+ #
11
+ # Home per ADR-178 §1: a shared bash primitive consumed by shipped plugin code
12
+ # lives inside `plugins/soleur/`, alongside `proc.sh`, `session-state.sh` and
13
+ # `domain-model-lib.sh`.
14
+ #
15
+ # API CONTRACT: this file exports `SOLEUR_OP_LIB_API=1`. Every consumer asserts
16
+ # `[[ ${SOLEUR_OP_LIB_API:-0} -eq 1 ]]` right after its `source` line and exits
17
+ # 64 with `SOLEUR_BOOTSTRAP_LIB_INCOMPATIBLE need=1 got=<n>` otherwise. EQUALITY,
18
+ # not `-ge`: the library auto-updates with the plugin while a generated script
19
+ # is frozen in the founder's repository, so "library newer than script" is the
20
+ # only incompatibility that can occur — and a `-ge` gate admits exactly that
21
+ # case, then dies mid-stage on a renamed helper (measured by the review lead).
22
+ # Bump the number only on a change that breaks an existing consumer (a renamed
23
+ # helper, a changed positional contract), never on an additive one.
24
+ #
25
+ # PORTABILITY: bash 3.2 (the /bin/bash macOS ships), the floor `proc.sh` in this
26
+ # directory declares. So: no negative array subscripts (`${a[-1]}` is 4.3+), no
27
+ # `${var^^}` (4.0+), no `mapfile`, no `declare -A`. `readlink -f` may be absent
28
+ # on older macOS — every use falls back to the unresolved path.
29
+ #
30
+ # Sourced (NEVER executed) by:
31
+ # - plugins/soleur/skills/operator-bootstrap/template.sh
32
+ # the template every generated <feature>/bootstrap.sh is authored from
33
+ # - plugins/soleur/skills/provision-hetzner/scripts/provision-hetzner.sh
34
+ # the proving consumer (plan D11)
35
+ # - every script the `soleur:operator-bootstrap` skill generates
36
+ #
37
+ # SOURCING PRECONDITIONS — the caller satisfies all of these BEFORE the
38
+ # `source` line (the shape of apps/cla-evidence/scripts/_cf-admin-token.sh):
39
+ # 1. `set -euo pipefail` is already in effect. This file sets no shell options
40
+ # of its own except `umask`, because a sourced library that changes the
41
+ # caller's error handling changes the caller's control flow.
42
+ # 2. The caller carries its OWN xtrace-refusal prologue ABOVE the `source`
43
+ # line. It is DUPLICATED there, never moved in here: `PROLOGUE_MAX_CMDS = 0`
44
+ # in scripts/lint-shell-trace-credential-refusal.py makes a `source` line
45
+ # itself a counted command, and `find_preamble` only ever scans a file's
46
+ # OWN lines — a caller sourcing a fully compliant library still fails Rule
47
+ # A (measured, revision R26). The caller does NOT strip SSL_CERT_FILE /
48
+ # SSL_CERT_DIR / CURL_CA_BUNDLE: `gh` and `hcloud` are Go clients that read
49
+ # those for their root CA pool, and a founder behind a TLS-inspecting proxy
50
+ # needs them (the linter requires only the xtrace refusal — measured).
51
+ # 3. `grep`, `sed`, `date`, `dirname`, `mkdir`, `mv`, `chmod`, `mktemp`, `rm`,
52
+ # `readlink`, `printenv`, `od`, `id`, `stat`, `cut`, `tr` and one of
53
+ # `sha256sum` / `shasum` are on PATH — the binaries this file actually
54
+ # calls, and only those. `gh` is required only by the two GitHub helpers.
55
+ #
56
+ # LIBRARY INVARIANT (revision R26) — THIS FILE NEVER EXPANDS A SECRET-SHAPED
57
+ # VARIABLE NAME. The credential linter's `^scripts/lib/` exclusion is
58
+ # repo-root-anchored, so a library under `plugins/soleur/scripts/lib/` IS
59
+ # scanned. If this file ever expanded a `*_TOKEN` / `*_KEY` / `*_SECRET` /
60
+ # `*_PASSWORD` / `*_PAT` name — or used `${!name}` indirection — it would come
61
+ # into scope and need its own `exit 78`, which on a `source` terminates the
62
+ # CALLER. So: secret values arrive as POSITIONAL PARAMETERS bound to neutral
63
+ # local names (`value`), skip variables are read with `printenv` rather than
64
+ # `${!name}`, and every expansion of a caller-named credential stays in the
65
+ # caller.
66
+ #
67
+ # EXIT CODES a generated script may return (revision R42 — the repo has no
68
+ # central table and code 3 is already overloaded, so the table lives HERE ONLY;
69
+ # operator-bootstrap/SKILL.md and template.sh point at this section rather than
70
+ # restating it):
71
+ #
72
+ # 0 success.
73
+ # 1 usage error; a refused call — e.g. a secret-shaped name handed to the
74
+ # GitHub *variable* helper, which writes on argv (remedy: fix the call); or
75
+ # the operator DECLINED a barrier or a destructive-write ack (the
76
+ # `SOLEUR_BOOTSTRAP_ABORTED stage=<kind>` marker names which).
77
+ # 3 DPA-gate rejection (tenant provisioning scripts only). CONFLICT, stated
78
+ # rather than renumbered: apps/cla-evidence/scripts/sentinel-pr.sh returns
79
+ # 3 for "missing tool on PATH" and "not inside a git repository" — a class
80
+ # apps/cla-evidence/infra/bootstrap.sh returns 64 for. Do NOT re-derive 3
81
+ # as "missing tool".
82
+ # 64 missing input — a required binary, a required environment variable, a
83
+ # missing `--stage`, or a prompt that cannot be answered because stdin is
84
+ # not a TTY. Remedy: the sentence under the marker names it. For a
85
+ # class-1/class-3 prompt, set the named variable. The legacy class-2
86
+ # destructive-write ack (`soleur_op_ack_or_die`, scripts not yet moved to
87
+ # the staged contract) has NO variable to set and needs a typed `yes` at a
88
+ # terminal; a v2 generated script never calls it.
89
+ # 75 approval required or not valid for a staged WRITE (EX_TEMPFAIL, ADR-264):
90
+ # no approval was presented, the receipt was expired / used / for another
91
+ # command, or the plan changed between the plan and the apply. Nothing was
92
+ # written. The marker's `reason=` and the sentence under it say what to do;
93
+ # the agent re-plans, re-issues the command, or stops and reports.
94
+ # 78 refusing to run under shell tracing while holding a live credential
95
+ # (#7797). Remedy: re-run without `-x`.
96
+ #
97
+ # Exit 1 has a FOURTH meaning under the caller's `set -e`: a `soleur_op_gh_*`
98
+ # helper that `return 1`s (write failed, verify failed, unsafe name) ends the
99
+ # caller with status 1 unless the call is guarded. The marker line above the
100
+ # exit says which.
101
+ #
102
+ # STDOUT MARKERS, not stderr. Agent runtimes surface stdout and swallow stderr,
103
+ # so a stderr-only refusal is invisible on the one surface that matters
104
+ # (provision-doppler.sh records the same reason at its own prologue). On a
105
+ # hosted agent surface (a cloud session, a CI step) these markers are the ONLY
106
+ # durable signal besides the ledger: grep for `SOLEUR_BOOTSTRAP_` in the step
107
+ # log. Every refusal is the marker PLUS one plain sentence for the founder.
108
+ # SOLEUR_BOOTSTRAP_INPUT_REQUIRED var=<NAME> tty=0 → exit 64
109
+ # SOLEUR_BOOTSTRAP_MISSING_BINARY bin=<bin> → exit 64
110
+ # SOLEUR_BOOTSTRAP_ABORTED stage=<barrier|ack> → operator declined, exit 1
111
+ # SOLEUR_BOOTSTRAP_BAD_ARG key=<KEY> [reason=<r>] → refused .env write, return 1
112
+ # helper=<h> reason=<r> (mktemp failed)
113
+ # SOLEUR_BOOTSTRAP_ENV_READ_FAILED path=<path> → grep exit 2 on the .env; no mv, return 1
114
+ # SOLEUR_BOOTSTRAP_ENV_KEYCOUNT_DROP before=<n> after=<m> path=<path>
115
+ # → filter lost keys; no mv, return 1
116
+ # SOLEUR_BOOTSTRAP_UNSAFE_VARIABLE name=<NAME> reason=secret-shaped-name-on-argv
117
+ # → refused argv write, return 1
118
+ # SOLEUR_BOOTSTRAP_SECRET_WRITE_FAILED name=<NAME> repo=<repo> → return 1
119
+ # SOLEUR_BOOTSTRAP_SECRET_VERIFY_FAILED name=<NAME> repo=<repo> → return 1 (write may have
120
+ # landed; also sets SOLEUR_OP_WRITE_MAY_HAVE_LANDED=1 for the trap)
121
+ # SOLEUR_BOOTSTRAP_VARIABLE_WRITE_FAILED name=<NAME> repo=<repo> → return 1
122
+ # SOLEUR_BOOTSTRAP_LEDGER_WRITE_FAILED path=<path> → non-fatal; the run
123
+ # continues, the record is lost
124
+ # SOLEUR_BOOTSTRAP_LIB_MISSING path=<last-rejected> → exit 64
125
+ # SOLEUR_BOOTSTRAP_LIB_INCOMPATIBLE need=1 got=<n> → exit 64
126
+ # SOLEUR_BOOTSTRAP_ENV_NOT_IGNORED path=<path> → exit 64 (template.sh)
127
+ #
128
+ # STAGED CONTRACT markers (ADR-264; every v2 generated script: --list, --stage
129
+ # <name>, --stage <name> --apply --plan-digest <d> [--rotate-token]). One line
130
+ # each, names and booleans only — never a value and never a hash of one:
131
+ # SOLEUR_BOOTSTRAP_STAGE_DECL stage=<s> class=<read|write> (--list)
132
+ # SOLEUR_BOOTSTRAP_PLAN stage=<s> operations=<n> digest=<hex>
133
+ # SOLEUR_BOOTSTRAP_PLAN_OP stage=<s> op=<name> target=<name>
134
+ # SOLEUR_BOOTSTRAP_IMPACT / _ROLLBACK stage=<s> text=<one plain sentence>
135
+ # SOLEUR_BOOTSTRAP_APPLY_COMMAND stage=<s> approval_digest=<hex> + the exact command
136
+ # SOLEUR_BOOTSTRAP_APPROVAL_REQUIRED stage=<s> approval_digest=<hex> surface=<s> → exit 75
137
+ # SOLEUR_BOOTSTRAP_APPROVAL_INVALID stage=<s> reason=<no-record|expired|digest-mismatch|
138
+ # consumed|algo|perms|format> → exit 75
139
+ # SOLEUR_BOOTSTRAP_PLAN_DRIFT stage=<s> → exit 75, receipt burned
140
+ # SOLEUR_BOOTSTRAP_PRECONDITION_FAILED stage=<s> need=<stage> → exit 1 (settles as refused;
141
+ # run the `need` stage, then this one again)
142
+ # SOLEUR_BOOTSTRAP_PATH_UNSUPPORTED stage=<s> → exit 64 (script path has a ')
143
+ # SOLEUR_BOOTSTRAP_STAGE_REQUIRED (no marker args) → exit 64 (no --stage: no run-all mode)
144
+ # SOLEUR_BOOTSTRAP_UNKNOWN_STAGE stage=<s> → exit 64 (not in the stage table)
145
+ # SOLEUR_BOOTSTRAP_PLAN_DIGEST_REQUIRED stage=<s> → exit 64 (--apply without the digest)
146
+ # SOLEUR_BOOTSTRAP_STAGE_OK stage=<s> changed=<0|1> [approval=<harness-receipt|tty-ack>]
147
+ # SOLEUR_BOOTSTRAP_STAGE_FAILED stage=<s> rc=<n>
148
+ # SOLEUR_BOOTSTRAP_LIB_INCOMPATIBLE need=stage-1 got=<n> → exit 64 (consumer)
149
+
150
+ # SOLEUR_BOOTSTRAP_LIB_MISSING, _LIB_INCOMPATIBLE and _ENV_NOT_IGNORED are
151
+ # emitted by the CONSUMER, not by this file — a library that is not there cannot
152
+ # announce itself. The canonical shape of that resolution (env override →
153
+ # CLAUDE_PLUGIN_ROOT → generation-time baked path → hard exit 64, never a stub;
154
+ # ADR-178 Context §1 records what fail-closed stubs did to `cleanup-merged`) is
155
+ # the "library resolution" section of
156
+ # plugins/soleur/skills/operator-bootstrap/template.sh.
157
+ #
158
+ # <!-- Inspired by mattpocock/skills/skills/engineering/wizard/ (MIT, Copyright (c) 2026 Matt Pocock). -->
159
+ # What is adopted is the SHAPE — a wizard that walks named stages, one journey
160
+ # per stage, with the operator told what will happen before it happens. The code
161
+ # is extracted from in-repo prior art, not ported: five of the six primitives
162
+ # already existed here (apps/cla-evidence/infra/bootstrap.sh for stage progress,
163
+ # preflight and the closing summary; community/scripts/{x,bsky,discord}-setup.sh
164
+ # for the exact-key `.env` upsert; operator-digest/scripts/
165
+ # provision-operator-digest-repo.sh for the stdin-only secret write and the
166
+ # separate argv variable write). Only cross-platform URL opening is new.
167
+
168
+ # Guard against double-source within a single shell.
169
+ if [[ "${_SOLEUR_OPERATOR_SCRIPT_LOADED:-}" == "1" ]]; then
170
+ return 0 2>/dev/null || true
171
+ fi
172
+ _SOLEUR_OPERATOR_SCRIPT_LOADED=1
173
+
174
+ # The class-2 ack's in-process result (#8486, ADR-249). Cleared at load so an
175
+ # inherited or exported value can never stand in for an ack that did not happen
176
+ # here; soleur_op_ack_or_die assigns it (never exports it) after a typed `yes`,
177
+ # and plugins/soleur/scripts/audit-flag-flip.sh refuses to append without it.
178
+ unset SOLEUR_OP_ACKED
179
+
180
+ # The API contract every consumer asserts after its `source` line (header §API).
181
+ export SOLEUR_OP_LIB_API=1
182
+
183
+ # The generated `.env` holds live credentials on the founder's own disk, and a
184
+ # default umask writes it 0644 into a directory that may sit under a cloud-sync
185
+ # client. 077 here, and `chmod 600` asserted after every upsert.
186
+ umask 077
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Approval receipts — the algorithm shared by the approval HOOK and this library
190
+ # ---------------------------------------------------------------------------
191
+ #
192
+ # The hook (plugins/soleur/hooks/operator-stage-approval.sh) SOURCES this file for
193
+ # `soleur_approval_*`, so MINT and VERIFY share ONE digest algorithm in ONE file
194
+ # and one diff moves both. (An earlier draft kept this in a sibling file; every
195
+ # harness that copies the library to a sandbox then lost it. One file is the
196
+ # distribution contract the rest of this header already states.) The independent
197
+ # anchor is the known-answer vector set in plugins/soleur/test/operator-script.test.sh,
198
+ # computed with python3 hashlib.
199
+ #
200
+ # WHAT A RECEIPT IS. When the human approves the exact command the agent is about
201
+ # to run, the harness hook has already minted a one-time record on the human's
202
+ # machine, and the nonce that unlocks it rides INSIDE the approved command line
203
+ # (an environment prefix the hook rewrote in). The script recomputes the digest of
204
+ # its own invocation, finds the record named sha256(nonce), and consumes it
205
+ # (rename) BEFORE the first write. An environment variable or a flag the agent sets
206
+ # by itself is therefore never an approval: the nonce is a LOOKUP KEY into a record
207
+ # the hook wrote, and any value with no matching record is exit 75.
208
+ #
209
+ # HONEST THRESHOLD (ADR-264, binding): the record is a file the same OS user can
210
+ # write. This resists an instruction-following agent that follows skill text and
211
+ # exit markers; it does NOT resist a hijacked agent that deliberately forges a
212
+ # record. Closing that needs a principal the agent cannot write as (credential
213
+ # custody, ADR-249 step 2). Never describe this mechanism as stronger.
214
+ #
215
+ # A failure in this section is a `return`, never an `exit`: the consumer owns the
216
+ # exit code (a library exit kills the caller).
217
+
218
+ # The record format version. A record or digest carrying any other value is
219
+ # refused with reason=algo, never as a generic failure (version skew is a
220
+ # distinct, reportable condition).
221
+ SOLEUR_APPROVAL_ALGO=1
222
+ # A record is valid for five minutes. Long enough for a human to read the prompt,
223
+ # short enough that a record the human denied is dead soon. A new mint for the same
224
+ # session also removes that session's earlier unconsumed records (soleur_approval_mint).
225
+ SOLEUR_APPROVAL_TTL_SECONDS=300
226
+
227
+ # The presented nonce is taken OUT of the environment the moment this file is loaded
228
+ # and held in a plain (never exported) shell variable. A child process the script
229
+ # starts later (doppler, gh, curl, a stage helper) therefore never inherits it, and
230
+ # the only function that ever reads it is soleur_approval_consume / _burn. The hook
231
+ # sources this file too and presents no nonce, so it holds the empty string there.
232
+ unset _SOLEUR_APPROVAL_NONCE_HELD
233
+ _SOLEUR_APPROVAL_NONCE_HELD="${SOLEUR_APPROVAL_NONCE:-}"
234
+ unset SOLEUR_APPROVAL_NONCE
235
+
236
+ # soleur_approval_sha256 — stdin to a lowercase hex digest on stdout.
237
+ soleur_approval_sha256() {
238
+ if command -v sha256sum >/dev/null 2>&1; then
239
+ sha256sum | cut -d' ' -f1
240
+ else
241
+ shasum -a 256 | cut -d' ' -f1
242
+ fi
243
+ }
244
+
245
+ # soleur_approval_dir — where records live: outside the worktree, per user.
246
+ soleur_approval_dir() {
247
+ printf '%s' "${XDG_STATE_HOME:-${HOME:-/nonexistent}/.local/state}/soleur/approvals"
248
+ }
249
+
250
+ # soleur_approval_realpath <path> — the resolved absolute path of an existing
251
+ # file, falling back to the unresolved absolute path (readlink -f may be absent
252
+ # on older macOS).
253
+ soleur_approval_realpath() {
254
+ local p="$1" d
255
+ readlink -f -- "$p" 2>/dev/null && return 0
256
+ d="$(cd "$(dirname "$p")" 2>/dev/null && pwd -P)" || { printf '%s' "$p"; return 0; }
257
+ printf '%s/%s' "$d" "$(basename "$p")"
258
+ }
259
+
260
+ # soleur_approval_content_hash <file> — sha256 of the file's bytes, or the fixed
261
+ # word `unreadable`. A receipt is bound to the script's CONTENT as well as its
262
+ # path: a script edited between the human's approval and the run no longer matches.
263
+ soleur_approval_content_hash() {
264
+ local h
265
+ h="$(soleur_approval_sha256 < "$1" 2>/dev/null)" || h=""
266
+ [[ "$h" =~ ^[0-9a-f]{64}$ ]] && printf '%s' "$h" || printf 'unreadable'
267
+ }
268
+
269
+ # soleur_approval_binding_digest <script-realpath> <stage> <arg>...
270
+ # The digest an approval is bound to: the resolved script, its content hash, the
271
+ # stage and every argument EXACTLY as the shell handed it over. Every variable
272
+ # field is written as `<byte-length>:<value>` so no value, whatever characters it
273
+ # carries (a newline included), can read as the next field. The hook computes it
274
+ # from the command string it parsed; the script recomputes it from "$@". The two
275
+ # are equal only for the same invocation of the same script bytes.
276
+ soleur_approval_binding_digest() {
277
+ local real="$1" stage="$2" a content
278
+ shift 2
279
+ content="$(soleur_approval_content_hash "$real")"
280
+ {
281
+ local LC_ALL=C
282
+ printf 'algo=%s\n' "$SOLEUR_APPROVAL_ALGO"
283
+ printf 'script=%d:%s\n' "${#real}" "$real"
284
+ printf 'content=%s\n' "$content"
285
+ printf 'stage=%d:%s\n' "${#stage}" "$stage"
286
+ for a in "$@"; do printf 'arg=%d:%s\n' "${#a}" "$a"; done
287
+ } | soleur_approval_sha256
288
+ }
289
+
290
+ # soleur_approval_nonce_ok <value> — a 32-hex-digit (128-bit) nonce.
291
+ soleur_approval_nonce_ok() {
292
+ [[ "${1:-}" =~ ^[0-9a-f]{32}$ ]]
293
+ }
294
+
295
+ # soleur_approval_new_nonce — 128 random bits as 32 hex digits.
296
+ soleur_approval_new_nonce() {
297
+ od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n'
298
+ }
299
+
300
+ # _soleur_approval_stat <path> — "<uid> <octal-mode>" or empty.
301
+ _soleur_approval_stat() {
302
+ stat -c '%u %a' -- "$1" 2>/dev/null || stat -f '%u %Lp' -- "$1" 2>/dev/null || true
303
+ }
304
+
305
+ # _soleur_approval_dir_ok <dir> — a real directory (never a symlink), owned by
306
+ # this euid, mode 0700.
307
+ _soleur_approval_dir_ok() {
308
+ local d="$1" st
309
+ [[ -d "$d" && ! -L "$d" ]] || return 1
310
+ st="$(_soleur_approval_stat "$d")"
311
+ [[ "$st" == "$(id -u) 700" ]]
312
+ }
313
+
314
+ # _soleur_approval_file_ok <file> — a regular file (never a symlink), owned by
315
+ # this euid, mode 0600.
316
+ _soleur_approval_file_ok() {
317
+ local f="$1" st
318
+ [[ -f "$f" && ! -L "$f" ]] || return 1
319
+ st="$(_soleur_approval_stat "$f")"
320
+ [[ "$st" == "$(id -u) 600" ]]
321
+ }
322
+
323
+ # _soleur_approval_housekeeping <dir> <session>
324
+ # Runs on every mint. (1) Removes every record, consumed or not, older than an
325
+ # hour: the directory never grows without bound and a stale record is not left
326
+ # for a later session to find. (2) Removes this session's earlier UNCONSUMED
327
+ # records: only the newest receipt of a session is live, so a prompt the person
328
+ # declined cannot be revived by a later one. Best effort; a failure here never
329
+ # blocks a mint.
330
+ _soleur_approval_housekeeping() {
331
+ local dir="$1" session="$2"
332
+ # find, not a glob: the approval hook runs with pathname expansion off (`set -f`).
333
+ find "$dir" -maxdepth 1 -type f -mmin +60 -delete 2>/dev/null || true
334
+ find "$dir" -maxdepth 1 -type f ! -name '*.consumed' ! -name '.mint.*' \
335
+ -exec grep -aqx "session=${session}" {} \; -delete 2>/dev/null || true
336
+ return 0
337
+ }
338
+
339
+ # soleur_approval_mint <binding-digest> <session-id>
340
+ # Writes one record and prints the nonce on stdout (the ONLY place it is ever
341
+ # printed; the caller embeds it in the rewritten command and nowhere else).
342
+ # The record is named sha256(nonce) so a directory listing never reveals a live
343
+ # nonce. Returns 1 (prints nothing) if the directory cannot be made safe.
344
+ soleur_approval_mint() {
345
+ local digest="$1" session="${2:-unknown}" dir nonce name tmp expires
346
+ dir="$(soleur_approval_dir)"
347
+ if [[ ! -e "$dir" ]]; then
348
+ (umask 077; mkdir -p "$dir") 2>/dev/null || return 1
349
+ chmod 700 "$dir" 2>/dev/null || return 1
350
+ fi
351
+ _soleur_approval_dir_ok "$dir" || return 1
352
+ nonce="$(soleur_approval_new_nonce)"
353
+ soleur_approval_nonce_ok "$nonce" || return 1
354
+ name="$(printf '%s' "$nonce" | soleur_approval_sha256)"
355
+ expires=$(( $(date +%s) + SOLEUR_APPROVAL_TTL_SECONDS ))
356
+ _soleur_approval_housekeeping "$dir" "${session//[^A-Za-z0-9._-]/_}"
357
+ tmp="${dir}/.mint.$$.${name:0:12}"
358
+ (
359
+ umask 077
360
+ printf 'algo=%s\ndigest=%s\nexpires=%s\nsession=%s\n' \
361
+ "$SOLEUR_APPROVAL_ALGO" "$digest" "$expires" "${session//[^A-Za-z0-9._-]/_}" > "$tmp"
362
+ ) 2>/dev/null || { rm -f "$tmp"; return 1; }
363
+ chmod 600 "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
364
+ mv -- "$tmp" "${dir}/${name}" 2>/dev/null || { rm -f "$tmp"; return 1; }
365
+ printf '%s' "$nonce"
366
+ }
367
+
368
+ # SOLEUR_APPROVAL_REASON
369
+ # The consume functions report WHY through this variable (never stdout: stdout
370
+ # of a library function is often captured). One of: no-record expired
371
+ # digest-mismatch consumed algo perms format. Empty means "no nonce was
372
+ # presented" (a different outcome: approval REQUIRED, not INVALID).
373
+ SOLEUR_APPROVAL_REASON=""
374
+ SOLEUR_APPROVAL_NONCE_SHA12=""
375
+
376
+ # soleur_approval_consume <binding-digest>
377
+ # 0 — a valid unexpired record for exactly this digest existed and has been
378
+ # renamed to <name>.consumed (single use; the rename IS the consumption and
379
+ # happens BEFORE the caller's first write).
380
+ # 1 — nothing was consumed; SOLEUR_APPROVAL_REASON says why ("" = no nonce).
381
+ soleur_approval_consume() {
382
+ local digest="$1" nonce dir name rec algo rdigest expires now
383
+ SOLEUR_APPROVAL_REASON=""
384
+ SOLEUR_APPROVAL_NONCE_SHA12=""
385
+ nonce="${_SOLEUR_APPROVAL_NONCE_HELD:-}"
386
+ [[ -n "$nonce" ]] || return 1
387
+ if ! soleur_approval_nonce_ok "$nonce"; then SOLEUR_APPROVAL_REASON=format; return 1; fi
388
+ dir="$(soleur_approval_dir)"
389
+ # No directory at all means the hook never minted anything here: no record. A
390
+ # directory that exists but is a symlink, foreign-owned or not 0700 is a tamper
391
+ # class: perms.
392
+ [[ -e "$dir" || -L "$dir" ]] || { SOLEUR_APPROVAL_REASON=no-record; return 1; }
393
+ _soleur_approval_dir_ok "$dir" || { SOLEUR_APPROVAL_REASON=perms; return 1; }
394
+ name="$(printf '%s' "$nonce" | soleur_approval_sha256)"
395
+ SOLEUR_APPROVAL_NONCE_SHA12="${name:0:12}"
396
+ rec="${dir}/${name}"
397
+ if [[ ! -e "$rec" && ! -L "$rec" ]]; then
398
+ if [[ -e "${rec}.consumed" ]]; then SOLEUR_APPROVAL_REASON=consumed; else SOLEUR_APPROVAL_REASON=no-record; fi
399
+ return 1
400
+ fi
401
+ _soleur_approval_file_ok "$rec" || { SOLEUR_APPROVAL_REASON=perms; return 1; }
402
+ # Field extraction without a `read` loop (the prompt census in Guard 4 treats every
403
+ # `read` in this file as a prompt, and these are not).
404
+ algo="$(grep -a '^algo=' "$rec" | head -n 1 | cut -d= -f2-)"
405
+ rdigest="$(grep -a '^digest=' "$rec" | head -n 1 | cut -d= -f2-)"
406
+ expires="$(grep -a '^expires=' "$rec" | head -n 1 | cut -d= -f2-)"
407
+ [[ "$algo" == "$SOLEUR_APPROVAL_ALGO" ]] || { SOLEUR_APPROVAL_REASON=algo; return 1; }
408
+ [[ "$rdigest" =~ ^[0-9a-f]{64}$ && "$expires" =~ ^[0-9]+$ ]] || { SOLEUR_APPROVAL_REASON=format; return 1; }
409
+ [[ "$rdigest" == "$digest" ]] || { SOLEUR_APPROVAL_REASON=digest-mismatch; return 1; }
410
+ now="$(date +%s)"
411
+ (( expires > now )) || { SOLEUR_APPROVAL_REASON=expired; return 1; }
412
+ # Single use. A lost rename race (two applies, one record) fails here and the
413
+ # loser writes nothing.
414
+ if ! mv -- "$rec" "${rec}.consumed" 2>/dev/null; then
415
+ # The record is gone: another run won the rename. Still there: the rename itself
416
+ # failed (permissions, a full disk) and that is a different, reportable condition.
417
+ if [[ -e "$rec" ]]; then SOLEUR_APPROVAL_REASON=perms; else SOLEUR_APPROVAL_REASON=consumed; fi
418
+ return 1
419
+ fi
420
+ _SOLEUR_APPROVAL_NONCE_HELD=""
421
+ return 0
422
+ }
423
+
424
+ # soleur_approval_burn
425
+ # Consume whatever record the presented nonce points at, valid or not: a plan
426
+ # that drifted between plan and apply must not leave a live receipt behind.
427
+ soleur_approval_burn() {
428
+ local nonce dir name rec
429
+ nonce="${_SOLEUR_APPROVAL_NONCE_HELD:-}"
430
+ soleur_approval_nonce_ok "$nonce" || return 0
431
+ dir="$(soleur_approval_dir)"
432
+ _soleur_approval_dir_ok "$dir" || return 0
433
+ name="$(printf '%s' "$nonce" | soleur_approval_sha256)"
434
+ rec="${dir}/${name}"
435
+ [[ -f "$rec" && ! -L "$rec" ]] || return 0
436
+ mv -- "$rec" "${rec}.consumed" 2>/dev/null || true
437
+ _SOLEUR_APPROVAL_NONCE_HELD=""
438
+ }
439
+
440
+ # ---------------------------------------------------------------------------
441
+ # Output helpers (shape from apps/cla-evidence/infra/bootstrap.sh)
442
+ # ---------------------------------------------------------------------------
443
+
444
+ SOLEUR_OP_GREEN='\033[32m'
445
+ SOLEUR_OP_RED='\033[31m'
446
+ SOLEUR_OP_YELLOW='\033[33m'
447
+ SOLEUR_OP_NC='\033[0m'
448
+
449
+ soleur_op_red() { printf '%b%s%b\n' "$SOLEUR_OP_RED" "$*" "$SOLEUR_OP_NC" >&2; }
450
+ soleur_op_green() { printf '%b%s%b\n' "$SOLEUR_OP_GREEN" "$*" "$SOLEUR_OP_NC"; }
451
+ soleur_op_yellow() { printf '%b%s%b\n' "$SOLEUR_OP_YELLOW" "$*" "$SOLEUR_OP_NC"; }
452
+
453
+ # ---------------------------------------------------------------------------
454
+ # Preflight
455
+ # ---------------------------------------------------------------------------
456
+
457
+ # soleur_op_require_bins <bin>...
458
+ # Exit 64 naming the first missing binary. The ladder (environment → Doppler →
459
+ # MCP/CLI/REST) runs BEFORE any prompt; a value outside the two interactive
460
+ # carve-outs that is missing is a hard failure with a named remedy, never a
461
+ # prompt.
462
+ # STDOUT marker plus one plain sentence: agent runtimes surface stdout and
463
+ # swallow stderr, and a founder reads the sentence, not the marker.
464
+ soleur_op_require_bins() {
465
+ local bin
466
+ for bin in "$@"; do
467
+ command -v "$bin" >/dev/null 2>&1 || {
468
+ printf 'SOLEUR_BOOTSTRAP_MISSING_BINARY bin=%s\n' "$bin"
469
+ printf 'Install %s first, then run again.\n' "$bin"
470
+ exit 64
471
+ }
472
+ done
473
+ }
474
+
475
+ # ---------------------------------------------------------------------------
476
+ # Run ledger — one JSON line per stage, BEFORE and AFTER. Names, never values.
477
+ # ---------------------------------------------------------------------------
478
+ #
479
+ # Layer 7 (cli-stdout-artifact): stdout alone is an explicit P1 rejection because
480
+ # it does not survive the session, so the synchronous markers above are paired
481
+ # with this durable artifact. ONE convention: the ledger sits BESIDE the script
482
+ # that writes it, as `bootstrap-runs.jsonl` — for a generated script that is
483
+ # `knowledge-base/project/specs/feat-<name>/`, tracked in the FOUNDER's
484
+ # repository, which is where the layer's "committed to the customer's own
485
+ # repository" condition is met. Nothing is transmitted and no alert target
486
+ # exists — the surface is the founder's own machine, and routing it to Soleur
487
+ # infrastructure would be a data-controller event, not an observability
488
+ # improvement.
489
+ #
490
+ # A write that fails (disk full, unwritable directory) is NON-FATAL — a
491
+ # provisioning stage must not abort because its audit line could not be
492
+ # appended — but it is never silent: `SOLEUR_BOOTSTRAP_LEDGER_WRITE_FAILED
493
+ # path=<path>` goes to stdout so the founder knows the artifact is incomplete.
494
+
495
+ # `$$` until soleur_op_ledger_init assigns the timestamped id: a helper that
496
+ # writes a ledger line before init (the `--reset` path does) must still carry a
497
+ # non-empty run_id, or the artifact has lines no run can be attributed to.
498
+ SOLEUR_OP_RUN_ID="$$"
499
+ SOLEUR_OP_TOTAL_STAGES=0
500
+ # Resolved ONCE (first use or soleur_op_ledger_init) and cached: the default is
501
+ # derived from the bottom of BASH_SOURCE, which may be relative, and a `cd` inside a stage
502
+ # would otherwise split one run's lines across two files.
503
+ SOLEUR_OP_LEDGER_FILE=""
504
+
505
+ # Default: beside the MAIN script (the bottom of the source stack), never a
506
+ # cwd-relative dotdir. `SOLEUR_BOOTSTRAP_LEDGER` overrides.
507
+ soleur_op_ledger_path() {
508
+ local main_script main_dir
509
+ if [[ -n "$SOLEUR_OP_LEDGER_FILE" ]]; then
510
+ printf '%s' "$SOLEUR_OP_LEDGER_FILE"
511
+ return 0
512
+ fi
513
+ if [[ -n "${SOLEUR_BOOTSTRAP_LEDGER:-}" ]]; then
514
+ SOLEUR_OP_LEDGER_FILE="$SOLEUR_BOOTSTRAP_LEDGER"
515
+ else
516
+ # `${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}`, not `[-1]`: negative subscripts
517
+ # are bash 4.3+ and the 3.2 that macOS ships dies here with "bad array
518
+ # subscript" — fatal under set -e, stderr only, no marker.
519
+ main_script="${BASH_SOURCE[${#BASH_SOURCE[@]}-1]:-}"
520
+ # Sourced with no main script (an interactive shell, `bash -c`): the bottom of
521
+ # the stack is this file, and "beside the library" is not a ledger home.
522
+ if [[ -z "$main_script" || "$main_script" == "${BASH_SOURCE[0]}" ]]; then
523
+ SOLEUR_OP_LEDGER_FILE="$(pwd)/bootstrap-runs.jsonl"
524
+ else
525
+ main_dir="$(cd "$(dirname "$main_script")" 2>/dev/null && pwd)" || main_dir="$(dirname "$main_script")"
526
+ SOLEUR_OP_LEDGER_FILE="${main_dir}/bootstrap-runs.jsonl"
527
+ fi
528
+ fi
529
+ printf '%s' "$SOLEUR_OP_LEDGER_FILE"
530
+ }
531
+
532
+ soleur_op_now() { date -u '+%Y-%m-%dT%H:%M:%SZ'; }
533
+
534
+ # JSON string escaping for the field set below (names and small integers by
535
+ # construction): newline and carriage return become a space FIRST, because the
536
+ # sed that follows is line-oriented and would otherwise pass a second line
537
+ # through unescaped; U+2028/U+2029 are stripped (JSON allows them raw, but a
538
+ # JS-hosted reader treats them as line terminators); backslash and double quote
539
+ # are escaped; remaining control characters collapse to a space.
540
+ soleur_op_json_escape() {
541
+ local s="$1"
542
+ s="${s//$'\n'/ }"
543
+ s="${s//$'\r'/ }"
544
+ s="${s//$'\xe2\x80\xa8'/}"
545
+ s="${s//$'\xe2\x80\xa9'/}"
546
+ printf '%s' "$s" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/[[:cntrl:]]/ /g'
547
+ }
548
+
549
+ # soleur_op_json_int <value>
550
+ # A bare JSON number, or 0 when the argument is not an integer — an unquoted
551
+ # non-number would make the whole line unparseable.
552
+ soleur_op_json_int() {
553
+ if [[ "${1:-}" =~ ^-?[0-9]+$ ]]; then printf '%s' "$1"; else printf '0'; fi
554
+ }
555
+
556
+ soleur_op_ledger_write() {
557
+ local path line
558
+ path="$(soleur_op_ledger_path)"
559
+ line="$1"
560
+ mkdir -p "$(dirname "$path")" 2>/dev/null || true
561
+ if ! printf '%s\n' "$line" >> "$path" 2>/dev/null; then
562
+ printf 'SOLEUR_BOOTSTRAP_LEDGER_WRITE_FAILED path=%s\n' "$path"
563
+ fi
564
+ }
565
+
566
+ # soleur_op_ledger_init <total-stages> <script-name>
567
+ soleur_op_ledger_init() {
568
+ SOLEUR_OP_TOTAL_STAGES="$(soleur_op_json_int "${1:-}")"
569
+ SOLEUR_OP_RUN_ID="$(date -u '+%Y%m%dT%H%M%SZ')-$$"
570
+ soleur_op_ledger_path >/dev/null
571
+ soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"run_begin","total_stages":%s,"script":"%s"}' \
572
+ "$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$SOLEUR_OP_TOTAL_STAGES" "$(soleur_op_json_escape "${2:-unknown}")")"
573
+ }
574
+
575
+ # soleur_op_ledger_note <kind> <name> <detail>
576
+ # `name` is a KEY NAME or a SECRET NAME. `detail` is metadata — never a value.
577
+ # Guard 3 drives a mutation that swaps a value into `detail` and asserts the
578
+ # suite reddens, because "names never values" is otherwise a comment.
579
+ soleur_op_ledger_note() {
580
+ soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","kind":"%s","name":"%s","detail":"%s"}' \
581
+ "$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" \
582
+ "$(soleur_op_json_escape "$1")" "$(soleur_op_json_escape "$2")" "$(soleur_op_json_escape "${3:-}")")"
583
+ }
584
+
585
+ # soleur_op_stage_begin <index> <name>
586
+ soleur_op_stage_begin() {
587
+ printf '\n→ [%s/%s] %s\n' "$1" "$SOLEUR_OP_TOTAL_STAGES" "$2"
588
+ soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"stage","phase":"begin","stage_index":%s,"total_stages":%s,"stage_name":"%s","outcome":"pending","exit_code":null}' \
589
+ "$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$(soleur_op_json_int "$1")" "$(soleur_op_json_int "$SOLEUR_OP_TOTAL_STAGES")" "$(soleur_op_json_escape "$2")")"
590
+ }
591
+
592
+ # soleur_op_stage_end <index> <name> <outcome> <exit-code>
593
+ soleur_op_stage_end() {
594
+ soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"stage","phase":"settle","stage_index":%s,"total_stages":%s,"stage_name":"%s","outcome":"%s","exit_code":%s}' \
595
+ "$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$(soleur_op_json_int "$1")" "$(soleur_op_json_int "$SOLEUR_OP_TOTAL_STAGES")" \
596
+ "$(soleur_op_json_escape "$2")" "$(soleur_op_json_escape "$3")" "$(soleur_op_json_int "${4:-0}")")"
597
+ }
598
+
599
+ # There is deliberately NO resume index. Every stage opens with an "already
600
+ # satisfied?" precondition (operator-bootstrap/SKILL.md §2), so re-running from
601
+ # stage 1 IS resume; a start-stage variable would be a second mechanism for the
602
+ # same property, and one the precondition already makes redundant.
603
+
604
+ # ---------------------------------------------------------------------------
605
+ # Prompts — R8's THREE carve-out classes
606
+ # ---------------------------------------------------------------------------
607
+ #
608
+ # class 1 NON-SECRET ladder value (a region, an account id, a repo slug)
609
+ # → named skip variable; no TTY + unset ⇒ exit 64 naming it.
610
+ # Credential ENTRY stays in the CALLER with its own `read -rs`
611
+ # behind the same gate — see provision-hetzner.sh. This helper
612
+ # echoes its input and must never take a secret.
613
+ # class 2 per-command destructive-write acknowledgement
614
+ # → NO SKIP VARIABLE AT ALL. No TTY ⇒ exit 64 unconditionally.
615
+ # An environment variable set once is exactly the "prior approval
616
+ # extending to a new command" hr-menu-option-ack-not-prod-write-auth
617
+ # forbids, so automation must not be able to supply this.
618
+ # class 3 out-of-band completion barrier ("Token created? Type 'yes'")
619
+ # → named skip variable, AND the caller MUST follow it with an
620
+ # independent verification of the thing attested. A barrier that is
621
+ # skippable and unverified attests nothing.
622
+ #
623
+ # A fourth class requires an ADR amendment (ADR-228).
624
+
625
+ # --- MUTATION ANCHOR: start of prompt helpers ---
626
+
627
+ # soleur_op_skip_value <VAR-NAME>
628
+ # `printenv`, deliberately, NOT `${!name}`: indirect expansion would put this
629
+ # file into the credential linter's scope (library invariant, above).
630
+ soleur_op_skip_value() {
631
+ printenv "$1" 2>/dev/null || true
632
+ }
633
+
634
+ # soleur_op_run_halt <reason> <name>
635
+ # The ledger's TERMINAL line for a run that stops before its stages settle:
636
+ # `reason` is `input_required` or `aborted`, `name` the variable or the stage
637
+ # kind. Without it the ledger could only ever say "ok" (review P2-12).
638
+ soleur_op_run_halt() {
639
+ soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"run_halt","reason":"%s","var":"%s"}' \
640
+ "$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$(soleur_op_json_escape "$1")" "$(soleur_op_json_escape "$2")")"
641
+ }
642
+
643
+ # soleur_op_input_required <VAR-NAME-or-reason> [ack]
644
+ # The no-TTY refusal. Emitted BEFORE any read, never after. Today's behaviour
645
+ # in provision-hetzner.sh is fail-closed but MUTE — EOF read into an empty ACK
646
+ # and `exit 1` with "Aborted." — the right outcome with an unattributed cause.
647
+ # The marker is for the agent; the sentence after it is for the founder. The
648
+ # second argument `ack` selects the class-2 sentence: there is no variable to
649
+ # set, so the only remedy is a person at a terminal.
650
+ soleur_op_input_required() {
651
+ printf 'SOLEUR_BOOTSTRAP_INPUT_REQUIRED var=%s tty=0\n' "$1"
652
+ if [[ "${2:-}" == "ack" ]]; then
653
+ # Legacy class-2 ack only (a script not yet moved to the staged contract):
654
+ # the approval is a typed yes at a terminal and nothing set by an agent can
655
+ # stand in for it. A v2 generated script never reaches this line.
656
+ printf 'This older script can only be approved by a person typing yes at a terminal; no setting can answer it for you. Regenerate it with the operator-bootstrap skill to get the staged form, where the approval happens at the Claude Code prompt on the exact command.\n'
657
+ else
658
+ printf 'This step needs one value. Set %s and run again; the agent can set it for you if you give it the value (a non-secret one) or point it at where the value already lives.\n' "$1"
659
+ fi
660
+ soleur_op_run_halt input_required "$1"
661
+ exit 64
662
+ }
663
+
664
+ # soleur_op_aborted <kind>
665
+ # The operator declined. Marker, plain sentence, ledger line, exit 1.
666
+ soleur_op_aborted() {
667
+ printf 'SOLEUR_BOOTSTRAP_ABORTED stage=%s\n' "$1"
668
+ printf 'Stopped. Nothing was created.\n'
669
+ soleur_op_run_halt aborted "$1"
670
+ exit 1
671
+ }
672
+
673
+ # soleur_op_value <SKIP-VAR> <prompt> <out-var-name> [class 1]
674
+ # Writes the answer into the named variable with `printf -v` rather than to
675
+ # stdout. Deliberate: a `$(...)` capture would run the helper in a SUBSHELL,
676
+ # where `exit 64` kills only the subshell and the script sails on past a
677
+ # prompt it never answered.
678
+ soleur_op_value() {
679
+ local var_name="$1" prompt_text="$2" out_name="$3" skip reply
680
+ skip="$(soleur_op_skip_value "$var_name")"
681
+ if [[ -n "$skip" ]]; then
682
+ printf -v "$out_name" '%s' "$skip"
683
+ return 0
684
+ fi
685
+ [[ -t 0 ]] || soleur_op_input_required "$var_name"
686
+ read -r -p "$prompt_text" reply
687
+ printf -v "$out_name" '%s' "$reply"
688
+ }
689
+
690
+ # soleur_op_barrier <SKIP-VAR> <prompt> [class 3]
691
+ # MUST be followed by an independent verification of the thing attested.
692
+ soleur_op_barrier() {
693
+ local var_name="$1" prompt_text="$2" skip reply
694
+ skip="$(soleur_op_skip_value "$var_name")"
695
+ if [[ -n "$skip" ]]; then
696
+ printf ' [skip] %s (%s is set; the caller still runs its own verification)\n' "$prompt_text" "$var_name"
697
+ return 0
698
+ fi
699
+ [[ -t 0 ]] || soleur_op_input_required "$var_name"
700
+ read -r -p "$prompt_text" reply
701
+ [[ "$reply" == "yes" ]] || soleur_op_aborted barrier
702
+ }
703
+
704
+ # soleur_op_in_agent_harness
705
+ # True when this process runs under an agent harness (Claude Code sets CLAUDECODE
706
+ # and CLAUDE_CODE_ENTRYPOINT in every tool call's environment). A typed `yes` on a
707
+ # pseudo-terminal is accepted only when this is false. HONEST LIMIT: an agent that
708
+ # deliberately unsets these names and opens a pty defeats it; that is the
709
+ # hijacked-agent case the receipt threshold already excludes (ADR-264).
710
+ soleur_op_in_agent_harness() {
711
+ [[ -n "${CLAUDECODE:-}" || -n "${CLAUDE_CODE_ENTRYPOINT:-}" ]]
712
+ }
713
+
714
+ # soleur_op_ack_or_die <prompt> [class 2]
715
+ # NO skip variable, by design and by rule. Do not add one.
716
+ soleur_op_ack_or_die() {
717
+ local prompt_text="$1" reply
718
+ if soleur_op_in_agent_harness; then
719
+ soleur_op_input_required "destructive-write-ack(agent-harness-no-typed-ack)" ack
720
+ fi
721
+ [[ -t 0 ]] || soleur_op_input_required "destructive-write-ack(no-skip-variable-by-design)" ack
722
+ read -r -p "$prompt_text" reply
723
+ [[ "$reply" == "yes" ]] || soleur_op_aborted ack
724
+ SOLEUR_OP_ACKED=tty-ack
725
+ }
726
+
727
+ # --- MUTATION ANCHOR: end of prompt helpers ---
728
+
729
+ # ---------------------------------------------------------------------------
730
+ # Staged contract — agent-run stages, harness-approved writes (ADR-264)
731
+ # ---------------------------------------------------------------------------
732
+ #
733
+ # A v2 generated script is driven ONE STAGE AT A TIME by an agent with no TTY:
734
+ #
735
+ # bash bootstrap.sh --list
736
+ # bash bootstrap.sh --stage <name> read stage: runs; write stage: prints the PLAN
737
+ # bash bootstrap.sh --stage <name> --apply --plan-digest <d> write stage: performs the write
738
+ #
739
+ # Every write needs a human acknowledgement of the EXACT command. It moves out of
740
+ # the terminal into the harness approval channel: the approval hook minted a
741
+ # one-time receipt when the human approved the command, and the nonce for it rode
742
+ # inside that command (the "Approval receipts" section above). This library never trusts a value
743
+ # the caller supplies about the approval: the plan is RECOMPUTED, the digest is
744
+ # RECOMPUTED, the nonce is only a lookup key into a record the hook wrote.
745
+ #
746
+ # THE ORDER INSIDE soleur_op_stage_gate IS THE CONTRACT (each step is a mutation
747
+ # row of Guard 11): recompute the plan and compare the digest (drift burns the
748
+ # receipt) -> validate the nonce against its record -> CONSUME the record (rename)
749
+ # -> unset the nonce so no child inherits it -> only then may the caller write.
750
+ # Otherwise a real TTY typed `yes` is the second valid source; otherwise exit 75.
751
+ #
752
+ # NO MARKER INSIDE A `$(...)` CAPTURE: a marker printed by a function whose stdout
753
+ # is captured becomes the captured value. Every function below that prints is
754
+ # called directly, never as `x=$(fn)`.
755
+
756
+ SOLEUR_OP_PLAN_OPS=()
757
+ SOLEUR_OP_APPROVAL=""
758
+ # Set by soleur_op_stage_gate immediately before it exits 75. The EXIT trap of a
759
+ # generated script settles the stage as `refused` on THIS flag, never on the bare
760
+ # exit code: a vendor CLI that happens to exit 75 mid-write must not be reported as
761
+ # a clean refusal.
762
+ SOLEUR_OP_REFUSED=""
763
+
764
+ # soleur_op_shquote <word> — the word as one shell-safe token (raw when it is
765
+ # plain, single-quoted otherwise). Returns 1 and prints nothing for a word that
766
+ # contains a single quote: no quoting of it survives the approval hook's parser, so
767
+ # the caller refuses (soleur_op_plan_emit) instead of printing a command the hook
768
+ # cannot ask about.
769
+ soleur_op_shquote() {
770
+ if [[ "$1" =~ ^[A-Za-z0-9_./:=@+,-]+$ ]]; then
771
+ printf '%s' "$1"
772
+ elif [[ "$1" == *"'"* ]]; then
773
+ return 1
774
+ else
775
+ printf "'%s'" "$1"
776
+ fi
777
+ }
778
+
779
+ # soleur_op_stage_lines <script>
780
+ # The script's own declarations, one per line: name|class|impact|rollback. The
781
+ # SAME lines are read by the approval hook, so there is one source of truth.
782
+ soleur_op_stage_lines() {
783
+ grep -a '^# SOLEUR-STAGE ' "$1" 2>/dev/null | sed 's/^# SOLEUR-STAGE //'
784
+ }
785
+
786
+ # soleur_op_stage_field <script> <stage> <1=name 2=class 3=impact 4=rollback>
787
+ # Empty (return 1) when the stage is not declared.
788
+ soleur_op_stage_field() {
789
+ soleur_op_stage_lines "$1" | awk -F'|' -v s="$2" -v f="$3" '$1 == s && !found { print $f; found = 1 } END { exit !found }'
790
+ }
791
+
792
+ # soleur_op_list <script> — the machine-readable stage table.
793
+ soleur_op_list() {
794
+ soleur_op_stage_lines "$1" | awk -F'|' '{ printf "SOLEUR_BOOTSTRAP_STAGE_DECL stage=%s class=%s\n", $1, $2 }'
795
+ }
796
+
797
+ # soleur_op_plan_begin / soleur_op_plan_op <op-name> <target-name>
798
+ # A plan is a list of operation NAMES and the container/slug/secret NAMES they
799
+ # touch — never a value. The digest covers exactly this list.
800
+ soleur_op_plan_begin() { SOLEUR_OP_PLAN_OPS=(); }
801
+ soleur_op_plan_op() { SOLEUR_OP_PLAN_OPS[${#SOLEUR_OP_PLAN_OPS[@]}]="$1|${2:-}"; }
802
+ soleur_op_plan_count() { printf '%s' "${#SOLEUR_OP_PLAN_OPS[@]}"; }
803
+
804
+ # soleur_op_plan_digest <script-real> <stage> <flags> <impact> <rollback>
805
+ # Deterministic digest of what an apply will do and what the human was told.
806
+ # The impact and rollback TEXT is inside it, so the words the human read are
807
+ # the words that were approved.
808
+ soleur_op_plan_digest() {
809
+ local op content
810
+ content="$(soleur_approval_content_hash "$1")"
811
+ {
812
+ local LC_ALL=C
813
+ printf 'algo=%s\n' "$SOLEUR_APPROVAL_ALGO"
814
+ printf 'script=%d:%s\ncontent=%s\n' "${#1}" "$1" "$content"
815
+ printf 'stage=%d:%s\nflags=%d:%s\n' "${#2}" "$2" "${#3}" "$3"
816
+ printf 'impact=%d:%s\nrollback=%d:%s\n' "${#4}" "$4" "${#5}" "$5"
817
+ for op in ${SOLEUR_OP_PLAN_OPS[@]+"${SOLEUR_OP_PLAN_OPS[@]}"}; do printf 'op=%d:%s\n' "${#op}" "$op"; done
818
+ } | soleur_approval_sha256
819
+ }
820
+
821
+ # soleur_op_plan_emit <script-real> <stage> <rotate 0|1>
822
+ # Prints the plan a human is asked to approve and the exact command that
823
+ # applies it. Declared impact and rollback text come from the script's own
824
+ # stage line; the agent relays them verbatim and composes none of its own.
825
+ soleur_op_plan_emit() {
826
+ local real="$1" stage="$2" rotate="${3:-0}" impact rollback flags="" digest op n apply_cmd approval quoted
827
+ impact="$(soleur_op_stage_field "$real" "$stage" 3)"
828
+ rollback="$(soleur_op_stage_field "$real" "$stage" 4)"
829
+ [[ "$rotate" == "1" ]] && flags="rotate-token"
830
+ digest="$(soleur_op_plan_digest "$real" "$stage" "$flags" "$impact" "$rollback")"
831
+ n="$(soleur_op_plan_count)"
832
+ printf 'SOLEUR_BOOTSTRAP_PLAN stage=%s operations=%s digest=%s\n' "$stage" "$n" "$digest"
833
+ for op in ${SOLEUR_OP_PLAN_OPS[@]+"${SOLEUR_OP_PLAN_OPS[@]}"}; do
834
+ printf 'SOLEUR_BOOTSTRAP_PLAN_OP stage=%s op=%s target=%s\n' "$stage" "${op%%|*}" "${op#*|}"
835
+ done
836
+ printf 'SOLEUR_BOOTSTRAP_IMPACT stage=%s text=%s\n' "$stage" "$impact"
837
+ printf 'SOLEUR_BOOTSTRAP_ROLLBACK stage=%s text=%s\n' "$stage" "$rollback"
838
+ if (( n == 0 )); then
839
+ printf 'Nothing to change for this stage: do NOT run --apply, there is nothing to approve.\n'
840
+ SOLEUR_OP_PLAN_DIGEST="$digest"
841
+ return 0
842
+ fi
843
+ if ! quoted="$(soleur_op_shquote "$real")"; then
844
+ printf 'SOLEUR_BOOTSTRAP_PATH_UNSUPPORTED stage=%s\n' "$stage"
845
+ printf 'This script lives at a path containing a single quote, which the approval prompt cannot carry. Move the script to a path without one and run the stage again; nothing was changed.\n'
846
+ exit 64
847
+ fi
848
+ apply_cmd="bash ${quoted} --stage ${stage} --apply --plan-digest ${digest}"
849
+ [[ "$rotate" == "1" ]] && apply_cmd="${apply_cmd} --rotate-token"
850
+ # approval_digest names the exact command printed below (script bytes included). It is an
851
+ # identifier for the receipt, never an approval: no variable or flag turns it into one.
852
+ if [[ "$rotate" == "1" ]]; then
853
+ approval="$(soleur_approval_binding_digest "$real" "$stage" --stage "$stage" --apply --plan-digest "$digest" --rotate-token)"
854
+ else
855
+ approval="$(soleur_approval_binding_digest "$real" "$stage" --stage "$stage" --apply --plan-digest "$digest")"
856
+ fi
857
+ printf 'SOLEUR_BOOTSTRAP_APPLY_COMMAND stage=%s approval_digest=%s\n' "$stage" "$approval"
858
+ printf ' %s\n' "$apply_cmd"
859
+ printf 'Tell the person the impact and rollback lines above in plain words, then run the command above exactly as printed (one simple command) and let the approval prompt carry their decision.\n'
860
+ SOLEUR_OP_PLAN_DIGEST="$digest"
861
+ }
862
+ SOLEUR_OP_PLAN_DIGEST=""
863
+
864
+ # soleur_op_stage_ok <stage> <changed 0|1> [approval]
865
+ soleur_op_stage_ok() {
866
+ if [[ -n "${3:-}" ]]; then
867
+ printf 'SOLEUR_BOOTSTRAP_STAGE_OK stage=%s changed=%s approval=%s\n' "$1" "$2" "$3"
868
+ else
869
+ printf 'SOLEUR_BOOTSTRAP_STAGE_OK stage=%s changed=%s\n' "$1" "$2"
870
+ fi
871
+ }
872
+
873
+ # soleur_op_precondition_failed <stage> <need-stage> <sentence>
874
+ # Stages are separately addressable, so a later stage re-proves what it depends
875
+ # on instead of trusting that an earlier one ran. Prints no plan digest.
876
+ soleur_op_precondition_failed() {
877
+ printf 'SOLEUR_BOOTSTRAP_PRECONDITION_FAILED stage=%s need=%s\n' "$1" "$2"
878
+ printf '%s\n' "$3"
879
+ # A precondition is a refusal with a named remedy, not a failure of the stage: the
880
+ # EXIT trap settles it as `refused` and prints no "run the stage again" banner.
881
+ SOLEUR_OP_REFUSED=1
882
+ exit 1
883
+ }
884
+
885
+ # soleur_op_ledger_approval <stage> <approval> <nonce-sha12> <plan-digest> <surface>
886
+ # One ledger line per approved write: names and hashes of PLAN data only. The
887
+ # nonce itself is never written anywhere; `nonce_sha12` is the first twelve hex
888
+ # digits of sha256(nonce) — the record's file-name prefix — enough to tie a line
889
+ # to a record and no use as a credential.
890
+ soleur_op_ledger_approval() {
891
+ soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"approval","stage":"%s","approval":"%s","nonce_sha12":"%s","plan":"%s","surface":"%s"}' \
892
+ "$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$(soleur_op_json_escape "$1")" "$(soleur_op_json_escape "$2")" \
893
+ "$(soleur_op_json_escape "$3")" "$(soleur_op_json_escape "$4")" "$(soleur_op_json_escape "$5")")"
894
+ }
895
+
896
+ # --- MUTATION ANCHOR: start of stage gate ---
897
+
898
+ # soleur_op_stage_gate <script-real> <stage> <computed-plan-digest> <supplied-digest> <impact> <rollback> <argv...>
899
+ # Returns 0 ONLY when this stage's write is approved; every other outcome exits
900
+ # 75 (or 1 when a person typed anything but yes) with nothing written.
901
+ # <argv...> is the invocation exactly as the shell handed it over ("$@").
902
+ # A caller MUST call this before the first mutating command of the stage and
903
+ # MUST NOT run any child before it returns.
904
+ soleur_op_stage_gate() {
905
+ local real="$1" stage="$2" computed="$3" supplied="$4" impact="$5" rollback="$6" binding surface
906
+ shift 6
907
+ surface="${CLAUDE_CODE_ENTRYPOINT:-unknown}"
908
+ binding="$(soleur_approval_binding_digest "$real" "$stage" "$@")"
909
+
910
+ # 1. The plan the human approved is the plan about to run, or nothing runs. The
911
+ # supplied digest is only ever COMPARED, never trusted.
912
+ if [[ "$supplied" != "$computed" ]]; then
913
+ soleur_approval_burn
914
+ printf 'SOLEUR_BOOTSTRAP_PLAN_DRIFT stage=%s\n' "$stage"
915
+ printf 'The digest does not match the plan this script computes now (the state changed since the plan was shown, or the digest was not the one printed), so nothing was changed. Run --stage %s again (without --apply), use the digest it prints, tell the person what is different if anything is, and ask for approval again.\n' "$stage"
916
+ soleur_op_run_halt plan_drift "$stage"
917
+ SOLEUR_OP_REFUSED=1
918
+ exit 75
919
+ fi
920
+
921
+ # 2. A presented nonce must match a live record for exactly this invocation.
922
+ if soleur_approval_consume "$binding"; then
923
+ SOLEUR_OP_APPROVAL="harness-receipt"
924
+ soleur_op_ledger_approval "$stage" harness-receipt "$SOLEUR_APPROVAL_NONCE_SHA12" "$computed" "$surface"
925
+ return 0
926
+ fi
927
+ if [[ -n "$SOLEUR_APPROVAL_REASON" ]]; then
928
+ _SOLEUR_APPROVAL_NONCE_HELD=""
929
+ printf 'SOLEUR_BOOTSTRAP_APPROVAL_INVALID stage=%s reason=%s\n' "$stage" "$SOLEUR_APPROVAL_REASON"
930
+ case "$SOLEUR_APPROVAL_REASON" in
931
+ no-record|expired|digest-mismatch|consumed)
932
+ printf 'That approval is not usable for this command (%s), so nothing was changed. Issue the apply command once more exactly as printed by the plan, and the person will be asked again.\n' "$SOLEUR_APPROVAL_REASON" ;;
933
+ *)
934
+ printf 'A safety check on the approval failed (%s) and nothing was changed. Stop and report this to the person; do not retry.\n' "$SOLEUR_APPROVAL_REASON" ;;
935
+ esac
936
+ soleur_op_run_halt approval_invalid "$stage"
937
+ SOLEUR_OP_REFUSED=1
938
+ exit 75
939
+ fi
940
+
941
+ # 3. The second valid source: a real person typing yes at a real terminal, and
942
+ # ONLY outside an agent harness. A pseudo-terminal an agent opens (script, a
943
+ # python pty, expect) satisfies `-t 0`, so a bare TTY test would let the agent
944
+ # type its own `yes`; inside a harness the terminal is not evidence of a person.
945
+ if [[ -t 0 && -t 1 ]] && ! soleur_op_in_agent_harness; then
946
+ printf ' If this fails: %s\n Rollback: %s\n' "$impact" "$rollback"
947
+ soleur_op_ack_or_die " Apply stage ${stage}? Type 'yes': "
948
+ SOLEUR_OP_APPROVAL="tty-ack"
949
+ soleur_op_ledger_approval "$stage" tty-ack "" "$computed" "$surface"
950
+ return 0
951
+ fi
952
+
953
+ # 4. No approval and no terminal: refuse. The agent re-issues the command as one
954
+ # simple command so the approval hook can ask the person, or reports that this
955
+ # surface cannot make production changes yet.
956
+ printf 'SOLEUR_BOOTSTRAP_APPROVAL_REQUIRED stage=%s approval_digest=%s surface=%s\n' "$stage" "$binding" "$surface"
957
+ printf 'This change needs the person to approve this exact command, and nothing approved it. In Claude Code with the Soleur plugin, issue the apply command as ONE simple command (no pipe, no semicolon, no &&, no substitution) and the person will be asked at the prompt. On a surface without that prompt I cannot make this change from here yet: nothing was changed, the plan is saved above, and it needs Claude Code with the Soleur plugin on the person'"'"'s computer (tracked in #9388). Tell the person exactly that; do not ask them to open a terminal.\n'
958
+ soleur_op_run_halt approval_required "$stage"
959
+ SOLEUR_OP_REFUSED=1
960
+ exit 75
961
+ }
962
+
963
+ # --- MUTATION ANCHOR: end of stage gate ---
964
+
965
+ # ---------------------------------------------------------------------------
966
+ # .env upsert — EXACT-KEY, and the mechanism is the trailing `=`
967
+ # ---------------------------------------------------------------------------
968
+ #
969
+ # Revision R41: exact-key is the MAJORITY precedent, not a correction to one.
970
+ # The four community setup scripts now write `.env` through `_wenv_write <env>
971
+ # <remove-ERE> <KEY=VALUE>...` with an exact-key ERE (`^(KEY_A|KEY_B)=`); a PREFIX
972
+ # ERE such as `^LINKEDIN_` is correct only for a fixed block of keys sharing that
973
+ # prefix and also removes unrelated keys. Drop the `=` here and upserting
974
+ # `X_API_KEY` silently removes `X_API_KEY_SECRET`. This library takes no lock
975
+ # (tracked in #10204); the community helper's lock is a separate mechanism.
976
+ #
977
+ # No precedent fsyncs, and none handles a concurrent writer. This library
978
+ # inherits that limitation: acceptable for a single-operator script on the
979
+ # founder's own machine, and stated rather than assumed.
980
+
981
+ # soleur_op_validate_key <KEY>
982
+ # ONE validator for BOTH .env writers. The key is interpolated into a BRE
983
+ # (`grep -v "^${key}="`), so anything outside the identifier alphabet is either
984
+ # a regex metacharacter (`.` matches every key; `[` is a grep exit 2) or a
985
+ # newline that splits the file. Refused, never escaped.
986
+ soleur_op_validate_key() {
987
+ local key="$1"
988
+ if [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
989
+ printf 'SOLEUR_BOOTSTRAP_BAD_ARG key=%s\n' "${key//[$'\n\r']/ }"
990
+ return 1
991
+ fi
992
+ }
993
+
994
+ # soleur_op_env_key_count <file>
995
+ # `-a` on EVERY grep over the .env: without it grep's binary-file heuristic
996
+ # applies, and (measured) one cp1252 byte in a comment under a UTF-8 locale
997
+ # silently drops that line with rc 0, while a NUL byte anywhere makes `grep -v`
998
+ # print nothing at all — the mv then installs an .env holding only the new key.
999
+ soleur_op_env_key_count() {
1000
+ grep -a -c '^[A-Za-z_][A-Za-z0-9_]*=' "$1" 2>/dev/null || true
1001
+ }
1002
+
1003
+ # soleur_op_env_filter_out <env-file> <KEY> <tmp>
1004
+ # Every line but `KEY=...` into <tmp>. grep exit 1 (nothing left) is a normal
1005
+ # outcome; exit 2 (unreadable file, a bad pattern) is NOT — the old `|| true`
1006
+ # turned it into an empty <tmp> that the mv then installed as the .env,
1007
+ # wiping every key and printing green. Shared by upsert and reset so the two
1008
+ # cannot drift.
1009
+ soleur_op_env_filter_out() {
1010
+ local env_file="$1" key="$2" tmp="$3" rc
1011
+ grep -a -v "^${key}=" "$env_file" > "$tmp" && rc=0 || rc=$?
1012
+ if (( rc > 1 )); then
1013
+ printf 'SOLEUR_BOOTSTRAP_ENV_READ_FAILED path=%s\n' "$env_file"
1014
+ rm -f "$tmp"
1015
+ return 1
1016
+ fi
1017
+ }
1018
+
1019
+ # soleur_op_env_commit <env-file> <tmp> <keys-before> <min-keys-after>
1020
+ # The mv is REFUSED when the file about to be installed holds fewer keys than
1021
+ # the writer can legitimately produce: an upsert only adds or replaces (never
1022
+ # shrinks), a reset shrinks by at most one. Anything else is the filter having
1023
+ # lost lines — the failure both greps above were made loud for — caught on the
1024
+ # artefact rather than on the mechanism.
1025
+ soleur_op_env_commit() {
1026
+ local env_file="$1" tmp="$2" before="$3" min_after="$4" after
1027
+ after="$(soleur_op_env_key_count "$tmp")"
1028
+ if (( after < min_after )); then
1029
+ printf 'SOLEUR_BOOTSTRAP_ENV_KEYCOUNT_DROP before=%s after=%s path=%s\n' "$before" "$after" "$env_file"
1030
+ rm -f "$tmp"
1031
+ return 1
1032
+ fi
1033
+ mv "$tmp" "$env_file"
1034
+ chmod 600 "$env_file"
1035
+ }
1036
+
1037
+ # soleur_op_has_control_char <value>
1038
+ # Success when the value holds ANY control character: the C0 set (tab, ESC,
1039
+ # the bell ...), DEL, and — the two the line-break check below already names —
1040
+ # CR and LF. The class is evaluated under `local LC_ALL=C` so the verdict is
1041
+ # BYTE-based and does not depend on the caller's locale (a bare `LC_ALL=C`
1042
+ # assignment would leak into the sourcing caller; `local` cannot). Under C only
1043
+ # the 0x00-0x1f and 0x7f bytes are control characters, so a UTF-8 credential is
1044
+ # judged the same in every locale.
1045
+ #
1046
+ # U+0085 (NEL), U+2028 and U+2029 are deliberately NOT refused, and that is
1047
+ # stated rather than assumed: they are the Unicode "line" separators, but the
1048
+ # sink here is a file read by `grep -a '^KEY='` and by the generated scripts'
1049
+ # byte-oriented `read`/`source`, none of which treats them as a line end — only a
1050
+ # consumer that Unicode-splits lines (a Python `splitlines`, a JS regexp `$`
1051
+ # with `m`) would, and no consumer of this file does. Refusing them would also
1052
+ # reject legitimate non-ASCII credentials. The ledger, a JSON sink that DOES care,
1053
+ # strips them in its own writer.
1054
+ soleur_op_has_control_char() {
1055
+ local LC_ALL=C
1056
+ [[ "$1" == *[[:cntrl:]]* ]]
1057
+ }
1058
+
1059
+ # soleur_op_env_upsert <env-file> <KEY> <value>
1060
+ # ATOMIC: the new `KEY=value` line is appended to the temp file BEFORE the
1061
+ # `mv`, so there is no instant at which the key is absent from the .env (the
1062
+ # old shape had mv → chmod → separate append; a Ctrl-C in between lost the
1063
+ # key). The .env is resolved through any symlink first, so a linked .env is
1064
+ # rewritten in place rather than replaced by a regular file.
1065
+ soleur_op_env_upsert() {
1066
+ local env_file="$1" key="$2" value="$3" tmp before_count
1067
+ soleur_op_validate_key "$key" || return 1
1068
+ if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
1069
+ printf 'SOLEUR_BOOTSTRAP_BAD_ARG key=%s reason=value-contains-line-break\n' "$key"
1070
+ return 1
1071
+ fi
1072
+ # Any OTHER control character (tab, ESC, DEL ...): a value written raw as `KEY=value`
1073
+ # must stay one printable line. The marker names the KEY (already validated to the
1074
+ # identifier alphabet) and never the value.
1075
+ if soleur_op_has_control_char "$value"; then
1076
+ printf 'SOLEUR_BOOTSTRAP_BAD_ARG key=%s reason=value-contains-control-char\n' "$key"
1077
+ return 1
1078
+ fi
1079
+ env_file="$(readlink -f -- "$env_file" 2>/dev/null || printf '%s' "$env_file")"
1080
+ [[ -f "$env_file" ]] || : > "$env_file"
1081
+ before_count="$(soleur_op_env_key_count "$env_file")"
1082
+ # `.tmp.` in the name so a `.env*` ignore pattern covers the sibling too; the
1083
+ # template's not-ignored check probes this exact shape.
1084
+ #
1085
+ # A SOURCED library must not install an owning `trap ... EXIT`: it would REPLACE
1086
+ # the consumer's — provision-hetzner.sh installs `cleanup`, the generated template
1087
+ # installs `on_exit` — and `on_exit` is the terminal-outcome mechanism this whole
1088
+ # feature rests on. Clobbering it to tidy a tempfile trades a bounded leak for the
1089
+ # silent loss of the founder's stopped-at-stage report. So every path out of these
1090
+ # two helpers removes its own temp explicitly; the temp is a full copy of the
1091
+ # credentials file, which is why this is not merely tidiness.
1092
+ # lint-trap-ownership: ok sourced library; an EXIT trap here would clobber the consumer's on_exit/cleanup, and every path below removes this temp explicitly
1093
+ tmp="$(mktemp "${env_file}.tmp.XXXXXX")" || {
1094
+ printf 'SOLEUR_BOOTSTRAP_BAD_ARG helper=env_upsert reason=mktemp-failed\n'
1095
+ return 1
1096
+ }
1097
+ soleur_op_env_filter_out "$env_file" "$key" "$tmp" || { rm -f "$tmp"; return 1; }
1098
+ printf '%s=%s\n' "$key" "$value" >> "$tmp" || { rm -f "$tmp"; return 1; }
1099
+ # On success env_commit `mv`s the temp into place, so there is nothing to remove;
1100
+ # on failure it leaves it, and this is the only frame that still holds the name.
1101
+ soleur_op_env_commit "$env_file" "$tmp" "$before_count" "$before_count" || { rm -f "$tmp"; return 1; }
1102
+ soleur_op_ledger_note env_upsert "$key" "keys_before=${before_count}"
1103
+ }
1104
+
1105
+ # soleur_op_env_reset <env-file> <KEY>
1106
+ # Revision R21a: a WRONG credential is otherwise permanent. The value is
1107
+ # persisted before it can be validated, and the environment-first ladder means
1108
+ # a re-run never re-prompts — it fails identically forever. This is the
1109
+ # `--reset <KEY>` path every generated script exposes.
1110
+ soleur_op_env_reset() {
1111
+ local env_file="$1" key="$2" tmp before_count
1112
+ soleur_op_validate_key "$key" || return 1
1113
+ env_file="$(readlink -f -- "$env_file" 2>/dev/null || printf '%s' "$env_file")"
1114
+ [[ -f "$env_file" ]] || return 0
1115
+ before_count="$(soleur_op_env_key_count "$env_file")"
1116
+ # lint-trap-ownership: ok sourced library; same reason as env_upsert above — an EXIT trap here would clobber the consumer's, and every path below removes this temp explicitly
1117
+ tmp="$(mktemp "${env_file}.tmp.XXXXXX")" || return 1
1118
+ soleur_op_env_filter_out "$env_file" "$key" "$tmp" || { rm -f "$tmp"; return 1; }
1119
+ soleur_op_env_commit "$env_file" "$tmp" "$before_count" "$(( before_count > 0 ? before_count - 1 : 0 ))" || { rm -f "$tmp"; return 1; }
1120
+ soleur_op_ledger_note env_reset "$key" ""
1121
+ }
1122
+
1123
+ # ---------------------------------------------------------------------------
1124
+ # GitHub writes — two helpers, deliberately NOT one
1125
+ # ---------------------------------------------------------------------------
1126
+ #
1127
+ # Argv is CORRECT for a login and WRONG for a credential. Merging the two "for
1128
+ # symmetry" is the mutation Guard 3 drives, because the merged helper would let
1129
+ # a secret take the argv path — visible in /proc/<pid>/cmdline to every process
1130
+ # on the machine, and in shell history when re-run by hand.
1131
+
1132
+ # soleur_op_gh_secret_set <repo> <SECRET-NAME> <value>
1133
+ # STDIN ONLY. The write's own output is redirected — a third-party CLI can dump
1134
+ # unrelated secrets as a side effect of a write (the case
1135
+ # .claude/hooks/doppler-secrets-delete-redirect.sh already blocks) — and the
1136
+ # result is confirmed by a SEPARATE read.
1137
+ soleur_op_gh_secret_set() {
1138
+ local repo="$1" sec_name="$2" value="$3" listed
1139
+ printf '%s' "$value" | gh secret set "$sec_name" -R "$repo" >/dev/null 2>&1 || {
1140
+ printf 'SOLEUR_BOOTSTRAP_SECRET_WRITE_FAILED name=%s repo=%s\n' "$sec_name" "$repo"
1141
+ return 1
1142
+ }
1143
+ listed="$(gh secret list -R "$repo" 2>/dev/null || true)"
1144
+ if ! grep -qE "^${sec_name}[[:space:]]" <<<"$listed"; then
1145
+ # The write returned 0 and the read-back did not confirm it, so the secret
1146
+ # MAY be live. The terminal-outcome trap must not tell the founder nothing
1147
+ # changed — that is the one sentence that stops them revoking. Exported so
1148
+ # a trap in the calling script can see it.
1149
+ SOLEUR_OP_WRITE_MAY_HAVE_LANDED=1
1150
+ export SOLEUR_OP_WRITE_MAY_HAVE_LANDED
1151
+ printf 'SOLEUR_BOOTSTRAP_SECRET_VERIFY_FAILED name=%s repo=%s\n' "$sec_name" "$repo"
1152
+ return 1
1153
+ fi
1154
+ soleur_op_ledger_note gh_secret "$sec_name" "repo=${repo}"
1155
+ }
1156
+
1157
+ # soleur_op_gh_variable_set <repo> <VARIABLE-NAME> <value>
1158
+ # Argv, because a repo VARIABLE is not confidential and masking it would make a
1159
+ # delivery failure harder to diagnose (the reason
1160
+ # provision-operator-digest-repo.sh writes OPERATOR_GH_LOGIN this way).
1161
+ #
1162
+ # The refusal below is NOVEL — no in-repo precedent has it. It is what stops
1163
+ # this helper from quietly becoming a secret path.
1164
+ # Case-insensitive and CONTAINS-form (review P2-10): the old suffix-only,
1165
+ # case-sensitive `^(.*_)?(TOKEN|KEY|SECRET|PASSWORD|PAT)$` admitted
1166
+ # HCLOUD_TOKEN_PRD, hcloud_token, SECRET_KEY_BASE, API_KEYS, PRIVATE_KEY_PEM,
1167
+ # CREDENTIALS, DB_PASSWD and SENTRY_DSN onto argv. `tr`, not `${var^^}`: the
1168
+ # latter is bash 4.0+ and this file's floor is 3.2.
1169
+ soleur_op_gh_variable_set() {
1170
+ local repo="$1" var_name="$2" value="$3" upper
1171
+ upper="$(printf '%s' "$var_name" | tr '[:lower:]' '[:upper:]')"
1172
+ if [[ "$upper" =~ (^|_)(TOKEN|KEY|SECRET|PASSWORD|PASSWD|PASSPHRASE|PAT|CREDENTIALS?|DSN|PRIVATE)(S?$|_) ]]; then
1173
+ printf 'SOLEUR_BOOTSTRAP_UNSAFE_VARIABLE name=%s reason=secret-shaped-name-on-argv\n' "$var_name"
1174
+ return 1
1175
+ fi
1176
+ gh variable set "$var_name" -R "$repo" --body "$value" >/dev/null 2>&1 || {
1177
+ printf 'SOLEUR_BOOTSTRAP_VARIABLE_WRITE_FAILED name=%s repo=%s\n' "$var_name" "$repo"
1178
+ return 1
1179
+ }
1180
+ soleur_op_ledger_note gh_variable "$var_name" "repo=${repo}"
1181
+ }
1182
+
1183
+ # ---------------------------------------------------------------------------
1184
+ # Cross-platform URL opening — the one genuinely new primitive
1185
+ # ---------------------------------------------------------------------------
1186
+ #
1187
+ # ADDITIVE ONLY. The URL is printed FIRST and the opener's exit code is NEVER
1188
+ # branched on, so a headless box, a locked-down desktop or a broken handler
1189
+ # degrades to "here is the URL" rather than to a failed stage. In-repo prior art
1190
+ # (community/scripts/linkedin-setup.sh) is `xdg-open`/`open` only; the WSL arm is
1191
+ # new — `wslview`, `explorer.exe` and `$WSL_DISTRO_NAME` detection appear nowhere
1192
+ # else in this repository.
1193
+ #
1194
+ # Each opener runs in the BACKGROUND with stdin detached (review P2-11):
1195
+ # `xdg-open` on a box with no DISPLAY hands the URL to a terminal browser
1196
+ # (w3m, lynx) that seizes the tty and stdin, and the script looks hung one line
1197
+ # before its next prompt. The URL is already printed, so nothing is lost if the
1198
+ # opener never returns. It may carry a query string — a `?token=` would be
1199
+ # printed and passed on argv; do not put a secret in a URL you open this way.
1200
+ #
1201
+ # SOLEUR_OP_NO_OPEN=1 prints the URL and skips every opener — for a test
1202
+ # harness, a CI runner or a founder who does not want a tab stolen. It is the
1203
+ # only environment variable this primitive reads.
1204
+ soleur_op_open_url() {
1205
+ local url="$1"
1206
+ printf ' %s\n' "$url"
1207
+ if [[ "${SOLEUR_OP_NO_OPEN:-}" == "1" ]]; then
1208
+ return 0
1209
+ fi
1210
+ if [[ -n "${WSL_DISTRO_NAME:-}${WSL_INTEROP:-}" ]]; then
1211
+ if command -v wslview >/dev/null 2>&1; then
1212
+ (wslview "$url" || true) </dev/null >/dev/null 2>&1 &
1213
+ elif command -v explorer.exe >/dev/null 2>&1; then
1214
+ (explorer.exe "$url" || true) </dev/null >/dev/null 2>&1 &
1215
+ fi
1216
+ elif command -v xdg-open >/dev/null 2>&1; then
1217
+ (xdg-open "$url" || true) </dev/null >/dev/null 2>&1 &
1218
+ elif command -v open >/dev/null 2>&1; then
1219
+ (open "$url" || true) </dev/null >/dev/null 2>&1 &
1220
+ fi
1221
+ return 0
1222
+ }
1223
+
1224
+ # ---------------------------------------------------------------------------
1225
+ # Closing summary
1226
+ # ---------------------------------------------------------------------------
1227
+
1228
+ soleur_op_summary_begin() {
1229
+ printf '\n'
1230
+ soleur_op_green "$1"
1231
+ soleur_op_green "Operator follow-ups:"
1232
+ }
1233
+
1234
+ soleur_op_summary_line() {
1235
+ soleur_op_green " $1"
1236
+ }