@soleur/opencode 0.0.0-stage → 1.0.0

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 (812) 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 +659 -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 +108 -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 +342 -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 +152 -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 +493 -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 +363 -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 +520 -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 +113 -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 +178 -0
  247. package/scripts/sync-opencode-adapter.ts +479 -0
  248. package/scripts/sync-pr-behind.sh +534 -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 +65 -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 +142 -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 +118 -0
  493. package/skills/fix-issue/references/exclude-label-jq-snippet.md +96 -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 +668 -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 +325 -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 +138 -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 +1621 -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-todo-structure.md +165 -0
  674. package/skills/review/references/risk-tier-and-fix-rounds.md +95 -0
  675. package/skills/review/references/semgrep-custom-rules.yaml +174 -0
  676. package/skills/review/references/ultrathink-phases.md +98 -0
  677. package/skills/review/references/wfs.md +6 -0
  678. package/skills/review/scripts/emit-review-trailer.sh +499 -0
  679. package/skills/review/scripts/ensure-semgrep.sh +99 -0
  680. package/skills/review/scripts/fix-round-seats.sh +197 -0
  681. package/skills/review/test/emit-review-trailer.test.sh +387 -0
  682. package/skills/review/test/ensure-semgrep.test.sh +111 -0
  683. package/skills/review/test/fix-round-seats.test.sh +237 -0
  684. package/skills/review/workflows/README.md +147 -0
  685. package/skills/review/workflows/review.workflow.js +667 -0
  686. package/skills/schedule/SKILL.md +796 -0
  687. package/skills/seo-aeo/SKILL.md +139 -0
  688. package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
  689. package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
  690. package/skills/ship/SKILL.md +3022 -0
  691. package/skills/ship/references/adr-ordinal-collision.md +47 -0
  692. package/skills/ship/references/affected-gate-contract.md +115 -0
  693. package/skills/ship/references/ci-run-diagnosis.md +29 -0
  694. package/skills/ship/references/ci-workflow-authoring.md +68 -0
  695. package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
  696. package/skills/ship/references/cto-ruling-armed-push.md +5 -0
  697. package/skills/ship/references/decision-challenges-render.md +71 -0
  698. package/skills/ship/references/detached-battery-verdict.md +13 -0
  699. package/skills/ship/references/feature-tweet-draft.md +42 -0
  700. package/skills/ship/references/followthrough-issue-body-template.md +47 -0
  701. package/skills/ship/references/followthrough-stub-template.sh +92 -0
  702. package/skills/ship/references/merge-queue-dequeue.md +39 -0
  703. package/skills/ship/references/operator-holds.md +149 -0
  704. package/skills/ship/references/pipeline-tally-render.md +43 -0
  705. package/skills/ship/references/pr-overlap.md +153 -0
  706. package/skills/ship/references/queue-mode.md +7 -0
  707. package/skills/ship/references/ready-run-wait.md +36 -0
  708. package/skills/ship/references/red-on-main-quarantine.md +110 -0
  709. package/skills/ship/references/register-update-pr-pattern.md +88 -0
  710. package/skills/ship/references/settle-then-admin-merge.md +130 -0
  711. package/skills/ship/references/supabase-column-probe.md +13 -0
  712. package/skills/ship/scripts/auto-close-scan.sh +76 -0
  713. package/skills/ship/scripts/battery-owed.sh +403 -0
  714. package/skills/ship/scripts/hold-gate-eval.sh +410 -0
  715. package/skills/ship/scripts/net-issue-flow.sh +665 -0
  716. package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
  717. package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
  718. package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
  719. package/skills/skill-creator/SKILL.md +299 -0
  720. package/skills/skill-creator/references/api-security.md +248 -0
  721. package/skills/skill-creator/references/authoring-levers.md +110 -0
  722. package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
  723. package/skills/skill-creator/references/best-practices.md +431 -0
  724. package/skills/skill-creator/references/common-patterns.md +639 -0
  725. package/skills/skill-creator/references/core-principles.md +193 -0
  726. package/skills/skill-creator/references/executable-code.md +188 -0
  727. package/skills/skill-creator/references/iteration-and-testing.md +509 -0
  728. package/skills/skill-creator/references/official-spec.md +193 -0
  729. package/skills/skill-creator/references/recommended-structure.md +180 -0
  730. package/skills/skill-creator/references/skill-structure.md +396 -0
  731. package/skills/skill-creator/references/using-scripts.md +122 -0
  732. package/skills/skill-creator/references/using-templates.md +120 -0
  733. package/skills/skill-creator/references/workflows-and-validation.md +541 -0
  734. package/skills/skill-creator/scripts/init_skill.py +322 -0
  735. package/skills/skill-creator/scripts/package_skill.py +110 -0
  736. package/skills/skill-creator/scripts/quick_validate.py +64 -0
  737. package/skills/skill-creator/templates/router-skill.md +73 -0
  738. package/skills/skill-creator/templates/simple-skill.md +34 -0
  739. package/skills/skill-creator/workflows/add-reference.md +96 -0
  740. package/skills/skill-creator/workflows/add-script.md +104 -0
  741. package/skills/skill-creator/workflows/add-template.md +83 -0
  742. package/skills/skill-creator/workflows/add-workflow.md +128 -0
  743. package/skills/skill-creator/workflows/audit-skill.md +159 -0
  744. package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
  745. package/skills/skill-creator/workflows/create-new-skill.md +207 -0
  746. package/skills/skill-creator/workflows/get-guidance.md +134 -0
  747. package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
  748. package/skills/skill-creator/workflows/verify-skill.md +226 -0
  749. package/skills/skill-security-scan/SKILL.md +145 -0
  750. package/skills/skill-security-scan/references/disclaimer.md +21 -0
  751. package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
  752. package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
  753. package/skills/skill-security-scan/references/override-mechanism.md +171 -0
  754. package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
  755. package/skills/skill-security-scan/references/regex-patterns.md +136 -0
  756. package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
  757. package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
  758. package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
  759. package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
  760. package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
  761. package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
  762. package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
  763. package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
  764. package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
  765. package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
  766. package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
  767. package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
  768. package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
  769. package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
  770. package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
  771. package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
  772. package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
  773. package/skills/skill-security-scan/scripts/lib.sh +197 -0
  774. package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
  775. package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
  776. package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
  777. package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
  778. package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
  779. package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
  780. package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
  781. package/skills/social-distribute/SKILL.md +485 -0
  782. package/skills/spec-templates/SKILL.md +274 -0
  783. package/skills/sync/SKILL.md +49 -0
  784. package/skills/test-browser/SKILL.md +464 -0
  785. package/skills/test-browser/references/feature-map.md +161 -0
  786. package/skills/test-fix-loop/SKILL.md +159 -0
  787. package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
  788. package/skills/triage/SKILL.md +402 -0
  789. package/skills/trigger-cron/SKILL.md +136 -0
  790. package/skills/trigger-cron/scripts/trigger.sh +198 -0
  791. package/skills/user-set-role/SKILL.md +147 -0
  792. package/skills/user-set-role/scripts/set-role.sh +218 -0
  793. package/skills/user-story-writer/SKILL.md +77 -0
  794. package/skills/ux-audit/SKILL.md +251 -0
  795. package/skills/ux-audit/references/finding.schema.json +62 -0
  796. package/skills/ux-audit/references/route-list.yaml +95 -0
  797. package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
  798. package/skills/ux-audit/scripts/bot-signin.ts +135 -0
  799. package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
  800. package/skills/work/SKILL.md +1479 -0
  801. package/skills/work/references/end-of-work-resume.md +7 -0
  802. package/skills/work/references/hook-test-main-cwd.md +24 -0
  803. package/skills/work/references/key-principles.md +83 -0
  804. package/skills/work/references/property-testing.md +122 -0
  805. package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
  806. package/skills/work/references/work-agent-teams.md +97 -0
  807. package/skills/work/references/work-lifecycle-parallel.md +147 -0
  808. package/skills/work/references/work-lockfile-bumps.md +97 -0
  809. package/skills/work/references/work-scratch-sandboxes.md +212 -0
  810. package/skills/work/references/work-subagent-fanout.md +61 -0
  811. package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
  812. package/skills/xcode-test/SKILL.md +360 -0
@@ -0,0 +1,1114 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook — bounds an agent session's memory via a systemd transient
3
+ # scope (ADR-161, #7166).
4
+ #
5
+ # WHAT HAPPENED. On 2026-08-01 a one-line regex search against a single 21 kB
6
+ # markdown file reached 9.5 GB RSS in 171 s at 99 % CPU and was still climbing,
7
+ # driving a 31 GB box to 691 MB free with swap at 88.5 %. The terminal crashed,
8
+ # killing six concurrent agent sessions and their in-flight work.
9
+ #
10
+ # WHAT THIS DOES. Adopts this session's whole process tree into
11
+ # `soleur-agent-<pid>.scope` under a shared `soleur-agents.slice`, with a
12
+ # per-session cap and a fleet-wide cap on BOTH memory and task count, then
13
+ # sweeps any OTHER unadopted agent session (claude, devin, codex) loose in a
14
+ # user scope into its own capped scope — codex fires no .claude hooks, and a
15
+ # pane-restore burst of simultaneous SessionStarts starves the apply flock, so
16
+ # self-adoption alone leaves most panes uncovered. systemd is the SINGLE
17
+ # WRITER to the cgroup hierarchy: this hook never creates a cgroup directory
18
+ # itself.
19
+ #
20
+ # WHY TASKS TOO. On 2026-09-29 a herdr workspace died wholesale: ~20 agent
21
+ # panes shared the terminal scope's pids.max (37984), concurrent pre-commit
22
+ # test runs pushed it over, and the kernel started rejecting forks. Two claude
23
+ # sessions aborted and the herdr server — the pane multiplexer itself — died,
24
+ # taking every session's in-flight work with it. No OOM: the binding limit was
25
+ # task count, which the memory caps below do not touch. A per-scope TasksMax
26
+ # makes the same runaway die ALONE (fork() fails inside its own scope) instead
27
+ # of starving the shared scope.
28
+ #
29
+ # WHAT THIS DOES NOT DO, stated up front: under a cap that runaway dies by
30
+ # SIGSEGV after ~45 s at 2.7 GB (measured at a 3 GB cap) — ugrep does not handle
31
+ # allocation failure gracefully. A cap BOUNDS DAMAGE; it does not prevent ~45 s
32
+ # of 100 % CPU on one core. What it does prevent is the memory-and-swap
33
+ # exhaustion that froze the desktop and killed six sessions.
34
+ #
35
+ # WHY THE SANCTIONED API. The previous attempt (#7151, closed unmerged) wrote a
36
+ # RAW cgroup v2 directory and moved the live process into it. That violated
37
+ # systemd's single-writer invariant, leaked a cgroup per session, and — because
38
+ # the new cgroup was a SIBLING of the terminal's scope — the process stopped
39
+ # being a scope member, so closing the terminal no longer reaped a runaway. For
40
+ # a non-technical operator that is the only stop mechanism there is.
41
+ # `StartTransientUnit` gives the identical kernel effect with systemd as the
42
+ # single writer, the cap visible to `systemctl --user status`, and the scope
43
+ # self-cleaning when its last process exits.
44
+ #
45
+ # NO TEST SEAMS. #7151 read four unvalidated test-injection env vars in
46
+ # production; one of them skipped the identity check entirely and another
47
+ # (`_BYTES=0`) was a one-token session kill. This file is split into functions
48
+ # plus `main`, guarded by the standard `BASH_SOURCE == $0` idiom: tests SOURCE it
49
+ # and call functions with ordinary arguments, production EXECS it. Those defects
50
+ # are not defended against here — they are unrepresentable. The one environment
51
+ # variable this hook honours is the documented kill switch
52
+ # SOLEUR_DISABLE_MEMORY_BACKSTOP, matching four sibling hooks. A second,
53
+ # SOLEUR_BACKSTOP_RESOLVED_FROM, is exported by the resolver shim
54
+ # (memory-backstop-resolve.sh, #9239) and read ONLY into the ledger's
55
+ # resolved_from field — attribution, never behaviour; empty on direct exec.
56
+ #
57
+ # NEVER BLOCKS. `exit 0` on every path, `set -e` deliberately NOT used (the
58
+ # duplicate-unit branch returns rc=1 by design). It can decline to protect; it
59
+ # cannot block a session. Every non-applied outcome is logged with a
60
+ # machine-readable reason.
61
+ #
62
+ # OUTPUT CHANNEL. `systemMessage` on STDOUT is the operator-visible channel for
63
+ # an exit-0 hook; plain stderr is DISCARDED (.claude/hooks/README.md). Messages
64
+ # are EDGE-TRIGGERED only — never per-session. And every `busctl` call's stdout
65
+ # is redirected to /dev/null: `busctl call` prints the returned job object path,
66
+ # and a SessionStart hook's stdout is injected into session context.
67
+
68
+ # ------------------------------------------------------------------ constants
69
+ # Revision marker read by memory-backstop-resolve.sh (#9239): the resolver
70
+ # greps it (never sources the file) and execs the highest-revision installed
71
+ # copy. CONTRACT: bump on EVERY behavioural change to this file — an un-bumped
72
+ # edit is invisible to resolution, never self-publishes to the managed path,
73
+ # and fails scripts/check-backstop-revision.sh in CI.
74
+ readonly BACKSTOP_REVISION=3
75
+
76
+ # All four cap values live here so raising one is a one-token change. They are
77
+ # re-validated against a two-sided band on every run (see validate_caps): a
78
+ # floor alone does not encode "below the harm point".
79
+ readonly SCOPE_HIGH_BYTES=6442450944 # 6 GiB — throttle; above the 3.85 GiB honest peak
80
+ readonly SCOPE_MAX_BYTES=7516192768 # 7 GiB — kill; 8.0 GB with baseline, vs the 9.5 GB harm point
81
+ readonly FLEET_HIGH_BYTES=17179869184 # 16 GiB — clears four concurrent `tsc`
82
+ readonly FLEET_MAX_BYTES=21474836480 # 20 GiB — last resort: three simultaneously-maxed sessions
83
+
84
+ readonly SCOPE_TASKS_MAX=4096 # per-session pids bound — ~2x an honest vitest burst, ~1/9 of the 37984 terminal-scope ceiling
85
+ readonly FLEET_TASKS_MAX=24576 # adopted-fleet bound — several full sessions, still under the shared parent limit
86
+
87
+ readonly SLICE_NAME="soleur-agents.slice"
88
+ readonly MAX_TREE=256
89
+ readonly MAX_WALK_HOPS=8
90
+ readonly MAX_SWEEP=8
91
+ readonly MAX_REPAIR=32
92
+
93
+ # ------------------------------------------------------------------ helpers
94
+
95
+ # Canonicalize via cd -P / pwd -P so a symlinked .claude/ does not produce two
96
+ # disjoint flock inodes (precedent: agent-token-tee.sh, skill-invocation-logger.sh).
97
+ _repo_root() {
98
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
99
+ (cd -P "$CLAUDE_PROJECT_DIR" 2>/dev/null && pwd -P) || echo "$CLAUDE_PROJECT_DIR"
100
+ return
101
+ fi
102
+ (cd -P "$(dirname "${BASH_SOURCE[0]}")/../.." 2>/dev/null && pwd -P)
103
+ }
104
+
105
+ # Any /proc-derived string that reaches a systemMessage is untrusted. A cgroup
106
+ # basename is the one such string here, and the scope directory is user-owned
107
+ # (Delegate=true), so anything running inside the session — a build script, an
108
+ # npm postinstall, an MCP server — can create a cgroup whose name ends in
109
+ # `.scope` and contains arbitrary bytes. That name would otherwise be emitted
110
+ # into the NEXT session's context: session N's untrusted code writing into
111
+ # session N+1's model context, and raw ANSI into the operator's terminal.
112
+ # jq escapes control bytes correctly for transport, which does not help — the
113
+ # consumer un-escapes them.
114
+ _sanitize() {
115
+ local s=${1:-}
116
+ [[ "$s" =~ ^[A-Za-z0-9:._@-]+$ ]] || { printf '<unprintable>'; return 0; }
117
+ printf '%s' "$s"
118
+ }
119
+
120
+ # Byte counts in operator-facing text are rendered in GiB. "7516192768-byte" is
121
+ # not a limit a non-technical operator can act on.
122
+ _human_bytes() {
123
+ local b=${1:-0}
124
+ [[ "$b" =~ ^[0-9]+$ ]] || { printf '%s' "$b"; return 0; }
125
+ awk -v b="$b" 'BEGIN{ printf (b >= 1073741824 ? "%.1f GiB" : "%d bytes"), (b >= 1073741824 ? b/1073741824 : b) }'
126
+ }
127
+
128
+ # Operator-visible channel for an exit-0 hook.
129
+ emit_message() {
130
+ local msg=$1
131
+ jq -n --arg m "$msg" '{systemMessage:$m}' 2>/dev/null
132
+ }
133
+
134
+ # Two-sided range validation. Each floor is set so the band CANNOT admit a value
135
+ # this design rejected: 4 GiB per-session high sits below the 3.85 GiB measured
136
+ # honest concurrency peak (a brake that low is a permanent tax, not a brake), and
137
+ # the per-session ceiling keeps the cap below the 9.5 GB harm point after the
138
+ # ~0.5 GiB non-migrated baseline is added back.
139
+ validate_caps() {
140
+ local sh=${1:-} sm=${2:-} fh=${3:-} fm=${4:-} st=${5:-} ft=${6:-}
141
+ local v
142
+ for v in "$sh" "$sm" "$fh" "$fm" "$st" "$ft"; do
143
+ [[ "$v" =~ ^[0-9]+$ ]] || { printf 'not_numeric:%s' "$v"; return 1; }
144
+ done
145
+ # 3 GiB .. 8 GiB : floor > the 2.45 GB tsc peak; ceiling 8.59 GB < 9.5 GB harm point
146
+ (( sm >= 3221225472 && sm <= 8589934592 )) || { printf 'scope_max:%s' "$sm"; return 1; }
147
+ # 5 GiB .. < scope_max : floor clears the 3.85 GiB honest concurrency peak
148
+ (( sh >= 5368709120 && sh < sm )) || { printf 'scope_high:%s' "$sh"; return 1; }
149
+ # 10 GiB .. 24 GiB : floor >= one session ceiling + the idle fleet
150
+ (( fm >= 10737418240 && fm <= 25769803776 )) || { printf 'fleet_max:%s' "$fm"; return 1; }
151
+ # 14 GiB .. < fleet_max : floor clears three concurrent `tsc` (13.35 GiB)
152
+ (( fh >= 15032385536 && fh < fm )) || { printf 'fleet_high:%s' "$fh"; return 1; }
153
+ # Cross-LEVEL relation. Each band above is independent, so they happily admit a
154
+ # combination that cannot compose — e.g. scope_max 8 GiB with fleet_max 10 GiB,
155
+ # a fleet that cannot hold two sessions at their own ceiling. The per-level
156
+ # bands encode "is this value sane"; this encodes "do these two levels agree".
157
+ (( fm >= 2 * sm )) || { printf 'fleet_max_below_two_sessions:%s' "$fm"; return 1; }
158
+ # 2048 .. 12288 : floor clears an agent + MCP + a vitest worker burst; ceiling
159
+ # stays a third of the terminal scope's measured 37984 ceiling, so a capped
160
+ # runaway loses its own forks but cannot starve siblings.
161
+ (( st >= 2048 && st <= 12288 )) || { printf 'scope_tasks:%s' "$st"; return 1; }
162
+ # 8192 .. 32768 : floor admits several sessions at their own cap, ceiling stays
163
+ # under the shared parent limit the 2026-09-29 incident ran into.
164
+ (( ft >= 8192 && ft <= 32768 )) || { printf 'fleet_tasks:%s' "$ft"; return 1; }
165
+ # Same cross-level rule as memory: a fleet bound that cannot hold a few
166
+ # capped sessions is a denial of the workload, not a bound on a runaway.
167
+ (( ft >= 4 * st )) || { printf 'fleet_tasks_below_four_sessions:%s' "$ft"; return 1; }
168
+ return 0
169
+ }
170
+
171
+ # Label-anchored PPid. NEVER `awk '{print $4}'` over /proc/<pid>/stat: that
172
+ # breaks on any ancestor whose comm contains a space, and fails SOFT — it yields
173
+ # a garbage pid that reads as "process not found".
174
+ read_ppid() {
175
+ local pid=$1 procroot=${2:-/proc}
176
+ local line
177
+ # Probe readability first: a PID can vanish mid-walk (this hook runs while the
178
+ # session is actively spawning and reaping children), and letting the redirect
179
+ # fail would spray "No such file or directory" onto a SessionStart hook's
180
+ # stderr on every run.
181
+ [[ -r "$procroot/$pid/status" ]] || return 1
182
+ while IFS= read -r line; do
183
+ if [[ "$line" == PPid:* ]]; then
184
+ line=${line#PPid:}
185
+ printf '%s' "${line//[[:space:]]/}"
186
+ return 0
187
+ fi
188
+ done < "$procroot/$pid/status" 2>/dev/null
189
+ return 1
190
+ }
191
+
192
+ # starttime is /proc/<pid>/stat field 22. `comm` (field 2) is parenthesised and
193
+ # is the ONLY field that can contain the delimiter — including ')' itself — so
194
+ # split on the LAST ')' and index from there. Used to disambiguate PID reuse.
195
+ read_starttime() {
196
+ local pid=$1 procroot=${2:-/proc}
197
+ local raw tail_
198
+ raw=$(cat "$procroot/$pid/stat" 2>/dev/null) || return 1
199
+ [[ "$raw" == *')'* ]] || return 1
200
+ tail_=${raw##*')'}
201
+ # After the last ')': field 3 (state) onward. starttime is field 22 overall,
202
+ # i.e. the 20th token here.
203
+ # shellcheck disable=SC2086
204
+ set -- $tail_
205
+ [[ $# -ge 20 ]] || return 1
206
+ printf '%s' "${20}"
207
+ }
208
+
209
+ # Session agent names. `codex` shares its binary name with the managed
210
+ # app-server daemon — the shared per-user backend every codex session talks to.
211
+ # Adopting THAT would bind every codex session to one session's caps, so the
212
+ # daemon shapes are excluded by cmdline, not by comm.
213
+ _is_agent_comm() {
214
+ case "$1" in claude|devin|codex) return 0 ;; *) return 1 ;; esac
215
+ }
216
+ _is_agent_daemon() {
217
+ case "$1" in *app-server*|*code-mode-host*|*" daemon "*) return 0 ;; *) return 1 ;; esac
218
+ }
219
+
220
+ # Positive identity match, and adopt NOTHING on failure. `readlink /proc/<pid>/exe`
221
+ # returns EMPTY (not an error) for an exited process, so a walk that treats "no
222
+ # match" as "keep walking" and then adopts the last pid examined could put
223
+ # warp-terminal or the login shell under a 7 GiB cap bound to itself.
224
+ # Echoes "<pid> <identity-signal>" on success.
225
+ discover_agent_pid() {
226
+ local pid=${1:-$$} procroot=${2:-/proc}
227
+ local hops=0 exe comm cmdline
228
+
229
+ while (( hops < MAX_WALK_HOPS )); do
230
+ hops=$((hops + 1))
231
+ # Reject unusable / dangerous candidates outright.
232
+ if [[ ! "$pid" =~ ^[0-9]+$ ]] || (( pid <= 1 )); then
233
+ return 1
234
+ fi
235
+
236
+ exe=$(readlink "$procroot/$pid/exe" 2>/dev/null || true)
237
+ comm=$(cat "$procroot/$pid/comm" 2>/dev/null || true)
238
+
239
+ if [[ -n "$exe" && "$exe" == */claude/versions/* ]]; then
240
+ printf '%s exe' "$pid"; return 0
241
+ fi
242
+ if [[ -n "${CLAUDE_CODE_EXECPATH:-}" && -n "$exe" && "$exe" == "${CLAUDE_CODE_EXECPATH}" ]]; then
243
+ printf '%s execpath' "$pid"; return 0
244
+ fi
245
+ # The daemon check is codex-scoped: only that binary multiplexes session
246
+ # and backend roles under one comm, and a devin/claude argv containing the
247
+ # word "daemon" is a session prompt, not a backend.
248
+ case "$comm" in
249
+ claude|devin) printf '%s comm' "$pid"; return 0 ;;
250
+ codex)
251
+ cmdline=$(tr '\0' ' ' < "$procroot/$pid/cmdline" 2>/dev/null || true)
252
+ if ! _is_agent_daemon "$cmdline"; then
253
+ printf '%s comm' "$pid"; return 0
254
+ fi
255
+ ;;
256
+ esac
257
+
258
+ pid=$(read_ppid "$pid" "$procroot") || return 1
259
+ done
260
+ return 1
261
+ }
262
+
263
+ # Whole descendant set in ONE bounded /proc pass. SessionStart runs CONCURRENTLY
264
+ # with MCP startup and MCP servers are direct children of `claude`, so adopting
265
+ # only claude's own pid loses that race and leaves headless Chrome — the largest
266
+ # memory risk on the box — outside the cap.
267
+ collect_descendants() {
268
+ local root=$1 procroot=${2:-/proc}
269
+ local d pid ppid
270
+ local -A parent=()
271
+ local -a order=()
272
+
273
+ for d in "$procroot"/[0-9]*; do
274
+ pid=${d##*/}
275
+ [[ "$pid" =~ ^[0-9]+$ ]] || continue
276
+ ppid=$(read_ppid "$pid" "$procroot") || continue
277
+ parent[$pid]=$ppid
278
+ order+=("$pid")
279
+ done
280
+
281
+ local -A keep=([$root]=1)
282
+ local changed=1 pass=0
283
+ while (( changed == 1 && pass < 12 )); do
284
+ changed=0; pass=$((pass + 1))
285
+ for pid in "${order[@]}"; do
286
+ [[ -n "${keep[$pid]:-}" ]] && continue
287
+ if [[ -n "${keep[${parent[$pid]:-}]:-}" ]]; then keep[$pid]=1; changed=1; fi
288
+ done
289
+ done
290
+
291
+ # NOTE: iteration order of an associative array is bash hash order, i.e.
292
+ # arbitrary. Above MAX_TREE the kept subset is therefore arbitrary too, and it
293
+ # could drop the very process the design names as the biggest risk (headless
294
+ # Chrome under an MCP server). Emit a sentinel line so the caller can record
295
+ # that truncation happened rather than silently capping an arbitrary subset.
296
+ local n=0
297
+ for pid in "${!keep[@]}"; do
298
+ if (( n >= MAX_TREE )); then printf 'TRUNCATED\n'; break; fi
299
+ printf '%s\n' "$pid"; n=$((n + 1))
300
+ done
301
+ }
302
+
303
+ # Adopt UNADOPTED sibling agent sessions loose in user scopes. Claude sessions
304
+ # self-adopt at SessionStart, but devin/codex sessions never reach this hook's
305
+ # own ancestry walk (codex fires no .claude hooks at all), and simultaneous
306
+ # SessionStarts lose the apply flock — the 2026-09-29 pids.max incident ran
307
+ # entirely through unadopted panes. Every run that reaches this point sweeps
308
+ # the strays its siblings left. Candidates are session-ROOT agent processes —
309
+ # an agent whose parent is also an agent is a descendant (devin acp under a
310
+ # devin TUI), not a root. Best-effort throughout: a swept process that exits
311
+ # mid-adopt costs only itself, and MAX_SWEEP bounds the work per run.
312
+ sweep_unadopted_agents() {
313
+ local self_pid=$1
314
+ local d pid comm cmdl root cg ts scope swept=0
315
+ local -A parent=() is_agent=() roots=()
316
+
317
+ for d in /proc/[0-9]*; do
318
+ pid=${d##*/}
319
+ comm=$(cat "$d/comm" 2>/dev/null) || continue
320
+ _is_agent_comm "$comm" || continue
321
+ if [[ "$comm" == "codex" ]]; then
322
+ cmdl=$(tr '\0' ' ' < "$d/cmdline" 2>/dev/null)
323
+ _is_agent_daemon "$cmdl" && continue
324
+ fi
325
+ parent[$pid]=$(read_ppid "$pid" /proc || echo 0)
326
+ is_agent[$pid]=1
327
+ done
328
+
329
+ for pid in "${!is_agent[@]}"; do
330
+ root=$pid
331
+ while [[ -n "${is_agent[${parent[$root]:-0}]:-}" ]]; do
332
+ root=${parent[$root]}
333
+ done
334
+ roots[$root]=1
335
+ done
336
+
337
+ for pid in "${!roots[@]}"; do
338
+ (( swept >= MAX_SWEEP )) && break
339
+ [[ "$pid" == "$self_pid" ]] && continue
340
+ # A root that is an ANCESTOR of our own agent (devin TUI over devin acp)
341
+ # would drag self_pid into its new scope on the descendant attach — stealing
342
+ # it from the scope this run just created. Skip anything above us.
343
+ local anc=$self_pid ahops=0 is_ancestor=0
344
+ while (( ahops < MAX_WALK_HOPS * 2 )); do
345
+ anc=$(read_ppid "$anc" /proc) || break
346
+ [[ -z "$anc" || "$anc" == "0" ]] && break
347
+ [[ "$anc" == "$pid" ]] && { is_ancestor=1; break; }
348
+ ahops=$((ahops + 1))
349
+ done
350
+ (( is_ancestor == 1 )) && continue
351
+ cg=$(cut -d: -f3 < "/proc/$pid/cgroup" 2>/dev/null | head -1)
352
+ # Only user-slice scopes: already-adopted sessions end soleur-agent-*.scope;
353
+ # anything not in a .scope (session.slice, service units) is left alone.
354
+ ts=${cg##*/}
355
+ case "$cg" in *"/user-"*) ;; *) continue ;; esac
356
+ [[ "$ts" == soleur-agent-*.scope ]] && continue
357
+ [[ "$ts" == *.scope ]] || continue
358
+
359
+ scope="soleur-agent-${pid}.scope"
360
+ "${TO[@]}" busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
361
+ org.freedesktop.systemd1.Manager StartTransientUnit "ssa(sv)a(sa(sv))" \
362
+ "$scope" "fail" 11 \
363
+ "PIDs" "au" 1 "$pid" \
364
+ "Slice" "s" "$SLICE_NAME" \
365
+ "MemoryHigh" "t" "$SCOPE_HIGH_BYTES" \
366
+ "MemoryMax" "t" "$SCOPE_MAX_BYTES" \
367
+ "MemorySwapMax" "t" 0 \
368
+ "TasksMax" "t" "$SCOPE_TASKS_MAX" \
369
+ "OOMPolicy" "s" "continue" \
370
+ "Delegate" "b" true \
371
+ "BindsTo" "as" 1 "$ts" \
372
+ "After" "as" 1 "$ts" \
373
+ "Description" "s" "Soleur agent session $pid" \
374
+ 0 \
375
+ >/dev/null 2>&1 || continue
376
+
377
+ local -a stree=()
378
+ mapfile -t stree < <(collect_descendants "$pid" /proc)
379
+ for sp in "${stree[@]}"; do
380
+ [[ "$sp" == "TRUNCATED" || "$sp" == "$pid" ]] && continue
381
+ [[ -r "/proc/$sp/cgroup" ]] || continue
382
+ # Do not steal a process already adopted into its own soleur-agent scope
383
+ # (a sibling's self-adoption landed first) — inter-scope churn gains nothing.
384
+ local scg; scg=$(cut -d: -f3 < "/proc/$sp/cgroup" 2>/dev/null | head -1)
385
+ [[ "${scg##*/}" == soleur-agent-*.scope ]] && continue
386
+ "${TO[@]}" busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
387
+ org.freedesktop.systemd1.Manager AttachProcessesToUnit "ssau" \
388
+ "$scope" "/" 1 "$sp" >/dev/null 2>&1
389
+ done
390
+ swept=$((swept + 1))
391
+ done
392
+ printf '%s' "$swept"
393
+ }
394
+
395
+ # Converge stale-adopted soleur-agent-*.scope units onto the CURRENT cap
396
+ # constants — runtime-only, in place, no session restart (#9239).
397
+ # sweep_unadopted_agents skips soleur-agent-*.scope members by construction, so
398
+ # a session adopted by an OLDER copy of this hook kept the older caps until its
399
+ # own next SessionStart — and a session that never re-fires kept them forever
400
+ # (the third 2026-09-29 herdr crash ran through exactly that window). The first
401
+ # run of ANY new copy on the host repairs every stale scope the same way the
402
+ # re-entry branch refreshes its own.
403
+ #
404
+ # RUNTIME-ONLY, the same rule the slice/scope writes in main live by: busctl
405
+ # SetUnitProperties with runtime=true — never `systemctl set-property` without
406
+ # --runtime, which permanently mutates ~/.config/systemd/user.control/.
407
+ #
408
+ # BindsTo is NEVER set or re-derived here. Deriving it from a member's cgroup
409
+ # binds an already-adopted scope to ITSELF — the kill-switch destruction the
410
+ # re-entry branch documents. It is not a runtime-settable property anyway, so a
411
+ # repair that needed it would fail rather than bind.
412
+ #
413
+ # OOMPolicy is deliberately NOT in the call either: it is creation-only on
414
+ # scopes (measured on systemd 261 — SetUnitProperties rejects it with "Cannot
415
+ # set property OOMPolicy", and the call is ALL-OR-NOTHING, so including it
416
+ # drops the four caps with it). Every version of this hook has created scopes
417
+ # with OOMPolicy=continue, so there is nothing stale on that axis to repair.
418
+ #
419
+ # $1: optional path to a file of unit names, one per line — the injectable
420
+ # unit list the suite drives (the same PATH-ARGUMENT convention as the fake
421
+ # /proc roots; never an env seam). Empty → enumerate live units.
422
+ # Prints the number of scopes repaired. Per-scope failures are logged on
423
+ # stderr — the harness-discarded channel, still visible when run by hand — and
424
+ # are never fatal: a scope that refuses repair costs only its own cap.
425
+ # Per-scope repair decision, extracted so repair_stale_scopes' batched
426
+ # readback parser stays flat. Returns 0 only when a SetUnitProperties repair
427
+ # was issued AND succeeded — busctl failures are logged on stderr and never
428
+ # counted.
429
+ repair_eval_scope() { # <unit> <tasksmax> <memmax> <memhigh> <swapmax>
430
+ local u="$1" tm="$2" mm="$3" mh="$4" ms="$5"
431
+ # Empty readback: the scope vanished between enumeration and show. Treating
432
+ # empty as "stale" would SetUnitProperties a nonexistent unit — which loads
433
+ # it INACTIVE (per the readback comment in main): a resurrected husk, not a
434
+ # repair.
435
+ [[ -n "$tm$mm$mh$ms" ]] || return 1
436
+ [[ "$tm" == "$SCOPE_TASKS_MAX" && "$mm" == "$SCOPE_MAX_BYTES" \
437
+ && "$mh" == "$SCOPE_HIGH_BYTES" && "$ms" == "0" ]] && return 1
438
+ ${TO[@]+"${TO[@]}"} busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
439
+ org.freedesktop.systemd1.Manager SetUnitProperties "sba(sv)" \
440
+ "$u" true 4 \
441
+ "MemoryHigh" "t" "$SCOPE_HIGH_BYTES" \
442
+ "MemoryMax" "t" "$SCOPE_MAX_BYTES" \
443
+ "MemorySwapMax" "t" 0 \
444
+ "TasksMax" "t" "$SCOPE_TASKS_MAX" \
445
+ >/dev/null 2>&1 \
446
+ || { printf 'memory-backstop: repair of %s failed\n' "$(_sanitize "$u")" >&2; return 1; }
447
+ return 0
448
+ }
449
+
450
+ repair_stale_scopes() {
451
+ local units_file=${1:-}
452
+ local -a units=()
453
+ local u repaired=0 checked=0
454
+
455
+ if [[ -n "$units_file" ]]; then
456
+ # mapfile is bash >=4 — a read loop keeps the file bash-3.2-safe like the
457
+ # rest of this hook (the macOS path runs on 3.2).
458
+ while IFS= read -r u; do units+=("$u"); done < "$units_file" 2>/dev/null || units=()
459
+ else
460
+ # --no-legend drops the header/footer, but the two-column STATE MARKER
461
+ # field still leads every row: blank on healthy units, ● or * on failed
462
+ # ones — so field 1 is NOT the unit name. Strip leading whitespace and the
463
+ # marker, take field 1, then apply the owned-name glob.
464
+ local _line
465
+ while IFS= read -r _line; do
466
+ _line="${_line#"${_line%%[![:space:]]*}"}"
467
+ _line=${_line#● }; _line=${_line#\* }
468
+ u=${_line%% *}
469
+ [[ "$u" == soleur-agent-*.scope ]] && units+=("$u")
470
+ done < <(${TO[@]+"${TO[@]}"} systemctl --user list-units --no-legend --no-pager 'soleur-agent-*.scope' 2>/dev/null)
471
+ fi
472
+
473
+ # `"${arr[@]+"${arr[@]}"}"` whole-quoted: bare `"${arr[@]}"` on an EMPTY
474
+ # array trips `set -u` on bash 3.2, and an unquoted outer expansion would
475
+ # word-split elements — this file runs wherever a checkout runs.
476
+ local -a target=()
477
+ for u in "${units[@]+"${units[@]}"}"; do
478
+ (( checked >= MAX_REPAIR )) && break
479
+ [[ "$u" == soleur-agent-*.scope ]] || continue # owned units only — never a foreign scope
480
+ checked=$((checked + 1))
481
+ target+=("$u")
482
+ done
483
+ (( ${#target[@]} == 0 )) && { printf '%s' 0; return 0; }
484
+
485
+ # ONE batched readback, not a per-scope call loop: `systemctl show` accepts
486
+ # multiple unit names and emits blank-line-separated blocks keyed on Id —
487
+ # measured 166ms → ~50ms for a converged 16-scope fleet, and it halves the
488
+ # flock hold time under concurrent SessionStarts (PR #9241 review).
489
+ # `${TO[@]+...}` is the same timeout-wrapper discipline the busctl calls
490
+ # carry: a wedged sd-bus must not stall SessionStart.
491
+ local cur="" tm="" mm="" mh="" ms="" k v
492
+ while IFS='=' read -r k v; do
493
+ if [[ -z "$k" ]]; then
494
+ if [[ -n "$cur" ]]; then
495
+ repair_eval_scope "$cur" "$tm" "$mm" "$mh" "$ms" && repaired=$((repaired + 1))
496
+ cur=""; tm=""; mm=""; mh=""; ms=""
497
+ fi
498
+ continue
499
+ fi
500
+ case "$k" in
501
+ Id) cur=$v ;;
502
+ TasksMax) tm=$v ;;
503
+ MemoryMax) mm=$v ;;
504
+ MemoryHigh) mh=$v ;;
505
+ MemorySwapMax) ms=$v ;;
506
+ esac
507
+ done < <(${TO[@]+"${TO[@]}"} systemctl --user show \
508
+ -p Id -p TasksMax -p MemoryMax -p MemoryHigh -p MemorySwapMax \
509
+ -- "${target[@]}" 2>/dev/null)
510
+ # The last block ends at EOF without a trailing blank line.
511
+ [[ -n "$cur" ]] && { repair_eval_scope "$cur" "$tm" "$mm" "$mh" "$ms" && repaired=$((repaired + 1)); :; }
512
+ printf '%s' "$repaired"
513
+ }
514
+
515
+ # The terminal's own scope, from cgroup membership. Must end in `.scope`; the
516
+ # name contains dots and dashes (app-gnome-dev.warp.Warp-3591813.scope) and must
517
+ # be passed quoted.
518
+ terminal_scope_for() {
519
+ local pid=$1
520
+ local cg base
521
+ cg=$(cut -d: -f3 < "/proc/$pid/cgroup" 2>/dev/null | head -1) || return 1
522
+ [[ -n "$cg" ]] || return 1
523
+ base=${cg##*/}
524
+ [[ "$base" == *.scope ]] || return 1
525
+ printf '%s' "$base"
526
+ }
527
+
528
+ # Kill attribution and near-miss. Fires on INCREASE, never on non-zero:
529
+ # memory.events counters are monotonic for the life of the cgroup, so a
530
+ # non-zero test would re-emit the same post-mortem on every SessionStart
531
+ # forever, for a kill the operator already saw.
532
+ #
533
+ # memory.events is read from a PATH ARGUMENT, deliberately not an env var —
534
+ # this must not re-introduce the injection surface the design removed.
535
+ #
536
+ # The wording must not claim causation: at slice MemoryMax the kernel kills the
537
+ # largest task in the subtree, which can sit in ANOTHER session's scope, so a
538
+ # message may reach a session that did not cause the kill.
539
+ attribution_message() {
540
+ local events_path=$1 peak_path=${2:-} last_oom=${3:-0} last_high=${4:-0}
541
+ local oom_kill high peak_h=""
542
+ [[ -r "$events_path" ]] || return 0
543
+ oom_kill=$(awk '/^oom_kill /{print $2}' "$events_path" 2>/dev/null)
544
+ high=$(awk '/^high /{print $2}' "$events_path" 2>/dev/null)
545
+ [[ "$oom_kill" =~ ^[0-9]+$ ]] || oom_kill=0
546
+ [[ "$high" =~ ^[0-9]+$ ]] || high=0
547
+ [[ "$last_oom" =~ ^[0-9]+$ ]] || last_oom=0
548
+ [[ "$last_high" =~ ^[0-9]+$ ]] || last_high=0
549
+
550
+ if [[ -n "$peak_path" && -r "$peak_path" ]]; then
551
+ local pb; pb=$(cat "$peak_path" 2>/dev/null)
552
+ [[ "$pb" =~ ^[0-9]+$ ]] && peak_h=$(awk -v b="$pb" 'BEGIN{printf "%.2f GiB", b/1073741824}')
553
+ fi
554
+
555
+ if (( oom_kill > last_oom )); then
556
+ printf '%s' "Soleur memory backstop: a process in this session was terminated by the kernel after this session's memory cap was reached.
557
+
558
+ What this means: a command running here hit this session's $(_human_bytes "$SCOPE_MAX_BYTES") memory limit${peak_h:+ (peak ${peak_h})} and the kernel stopped the largest process in the session. Uncommitted work in that command may be lost. This is not a crash in your code.
559
+
560
+ Note: the fleet-wide limit is shared, so a kill can occasionally land in a session other than the one that grew.
561
+
562
+ To raise this session's limit now (replace <pid> with the number in the scope name from \`systemctl --user list-units 'soleur-agent-*'\`):
563
+ systemctl --user set-property --runtime soleur-agent-<pid>.scope MemoryHigh=infinity MemoryMax=infinity
564
+ To turn the backstop off entirely: set SOLEUR_DISABLE_MEMORY_BACKSTOP=1"
565
+ return 0
566
+ fi
567
+
568
+ if (( high > last_high )); then
569
+ printf '%s' "Soleur memory backstop: this session crossed its memory brake $(( high - last_high )) time(s)${peak_h:+ (peak ${peak_h})} but nothing was terminated.
570
+
571
+ This is a warning, not an error — the brake throttles before the hard limit. If it keeps happening, the cap is probably too low for your workload; raise it in .claude/hooks/memory-backstop.sh."
572
+ return 0
573
+ fi
574
+ return 0
575
+ }
576
+
577
+ # ------------------------------------------------------------------ main
578
+ main() {
579
+ set -uo pipefail # deliberately NOT -e; scoped to main so sourcing leaks nothing
580
+
581
+ local project_dir log_file lock_file stamp_file
582
+ project_dir=$(_repo_root)
583
+ log_file="$project_dir/.claude/.memory-backstop.jsonl"
584
+ lock_file="$project_dir/.claude/.memory-backstop.lock"
585
+ stamp_file="$project_dir/.claude/.memory-backstop.stamp"
586
+ mkdir -p "$project_dir/.claude" 2>/dev/null
587
+
588
+ local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
589
+ local outcome="skipped" reason="" scope="" terminal_scope="" tree_size=0
590
+ local identity_signal="" claude_pid="" msg=""
591
+ local slice_high_before="" slice_max_before=""
592
+ local scope_high_after="" scope_max_after="" scope_swap_after="" scope_tasks_after=""
593
+ local slice_high_after="" slice_max_after="" slice_swap_after="" slice_tasks_after=""
594
+ local attached=0 tree_truncated="false" swept=0 repaired=0 refresh_rc=""
595
+
596
+ _log() {
597
+ local rotator
598
+ rotator="$(dirname "${BASH_SOURCE[0]}")/lib/log-rotation.sh"
599
+ if [[ -f "$rotator" ]]; then
600
+ # shellcheck source=/dev/null
601
+ source "$rotator" 2>/dev/null || true
602
+ declare -F rotate_if_needed >/dev/null 2>&1 && rotate_if_needed "$log_file" 2>/dev/null
603
+ fi
604
+ local line
605
+ if command -v jq >/dev/null 2>&1; then
606
+ line=$(jq -nc \
607
+ --arg ts "$ts" --arg outcome "$outcome" --arg reason "$reason" \
608
+ --arg scope "$scope" --arg tscope "$terminal_scope" --arg slice "$SLICE_NAME" \
609
+ --arg sig "$identity_signal" --arg pid "$claude_pid" \
610
+ --arg shb "$slice_high_before" --arg smb "$slice_max_before" \
611
+ --argjson tree "$tree_size" \
612
+ --argjson sh "$SCOPE_HIGH_BYTES" --argjson sm "$SCOPE_MAX_BYTES" \
613
+ --argjson fh "$FLEET_HIGH_BYTES" --argjson fm "$FLEET_MAX_BYTES" \
614
+ --argjson st "$SCOPE_TASKS_MAX" --argjson ft "$FLEET_TASKS_MAX" \
615
+ --argjson bsr "$BACKSTOP_REVISION" \
616
+ --arg rfrom "${SOLEUR_BACKSTOP_RESOLVED_FROM:-}" \
617
+ --argjson rep "${repaired:-0}" \
618
+ --argjson rrc "${refresh_rc:-null}" \
619
+ '{schema:2, ts:$ts, pid:(($pid|tonumber?) // null), tree_size:$tree, scope:$scope,
620
+ terminal_scope:$tscope, slice:$slice, scope_high:$sh, scope_max:$sm,
621
+ slice_high:$fh, slice_max:$fm, scope_tasks:$st, slice_tasks:$ft,
622
+ slice_high_before:$shb, slice_max_before:$smb,
623
+ backstop_revision:$bsr, resolved_from:$rfrom, repaired:$rep,
624
+ refresh_rc:$rrc,
625
+ swap_max:0, identity_signal:$sig, outcome:$outcome, reason:$reason}' 2>/dev/null)
626
+ fi
627
+ # An empty $line means the jq filter produced no object (an empty stream from
628
+ # a `?`-suppressed conversion, say). Writing it would append a blank line and
629
+ # the sink would look present-but-useless, so fall back to a minimal record
630
+ # that always has the two fields any reader needs.
631
+ # printf fallback builds JSON by hand — strip \ and " (quoting) and
632
+ # control chars (a newline would tear the JSONL line in two), matching
633
+ # resolver_emit's fallback (memory-backstop-resolve.sh).
634
+ local rfrom_sanitized="${SOLEUR_BACKSTOP_RESOLVED_FROM:-}"
635
+ rfrom_sanitized="${rfrom_sanitized//\\/}"; rfrom_sanitized="${rfrom_sanitized//\"/}"
636
+ rfrom_sanitized="${rfrom_sanitized//[[:cntrl:]]/}"
637
+ [[ -z "$line" ]] && line="{\"schema\":2,\"ts\":\"$ts\",\"outcome\":\"$outcome\",\"reason\":\"$reason\",\"backstop_revision\":$BACKSTOP_REVISION,\"resolved_from\":\"$rfrom_sanitized\",\"repaired\":${repaired:-0},\"refresh_rc\":null,\"log_degraded\":true}"
638
+ printf '%s\n' "$line" >> "$log_file" 2>/dev/null
639
+ }
640
+
641
+ # (1) Kill switch first. R12: if this is set you are unprotected and nothing
642
+ # else will tell you.
643
+ if [[ "${SOLEUR_DISABLE_MEMORY_BACKSTOP:-}" == "1" ]]; then
644
+ reason="disabled"; _log; exit 0
645
+ fi
646
+
647
+ # (2) Environment gates. Gate on the SOCKET, not on parsing a busctl error
648
+ # string — an error-string match rots with every systemd release.
649
+ if ! command -v busctl >/dev/null 2>&1; then
650
+ reason="no_busctl"; _log; exit 0
651
+ fi
652
+ # jq is required, not optional. A hand-rolled JSON fallback was tried and
653
+ # removed: it wrote lines WITHOUT the counter fields while the reader
654
+ # (_last_logged) required jq to read them, so `last_oom_kill` was 0 forever and
655
+ # the OOM post-mortem re-emitted on EVERY SessionStart — inverting the exact
656
+ # edge-triggered contract the increase-comparison exists to enforce. Declining
657
+ # loudly beats a second, silently-wrong output mode.
658
+ if ! command -v jq >/dev/null 2>&1; then
659
+ reason="no_jq"; _log; exit 0
660
+ fi
661
+ local bus="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/bus"
662
+ if [[ ! -S "$bus" ]]; then
663
+ reason="no_bus"; _log; exit 0
664
+ fi
665
+
666
+ # macOS ships no `timeout`; hardcoding it made a sibling hook exit 127 and go
667
+ # silently dark. A wedged D-Bus must not stall every SessionStart.
668
+ local TO=(); command -v timeout >/dev/null 2>&1 && TO=(timeout 5)
669
+
670
+ if ! "${TO[@]}" busctl --user --no-pager status >/dev/null 2>&1; then
671
+ reason="no_bus"; _log; exit 0
672
+ fi
673
+
674
+ # (3) Validate caps BEFORE any D-Bus call. Refusing is correct: a cap outside
675
+ # the validated band is either useless or dangerous, and a hook that
676
+ # half-applies is worse than one that declines. Refusal ALWAYS messages —
677
+ # these are readonly constants in a tracked file, so this can essentially only
678
+ # fire after an edit, and the person who just edited them is the one who needs
679
+ # to be told.
680
+ local bad
681
+ if ! bad=$(validate_caps "$SCOPE_HIGH_BYTES" "$SCOPE_MAX_BYTES" "$FLEET_HIGH_BYTES" "$FLEET_MAX_BYTES" "$SCOPE_TASKS_MAX" "$FLEET_TASKS_MAX"); then
682
+ outcome="refused"; reason="cap_out_of_range"; _log
683
+ emit_message "Soleur memory backstop: REFUSED to apply — a cap value is outside its validated range (${bad}).
684
+
685
+ Accepted ranges:
686
+ SCOPE_MAX_BYTES 3 GiB .. 8 GiB (floor above the 2.45 GB tsc peak; ceiling below the 9.5 GB harm point)
687
+ SCOPE_HIGH_BYTES 5 GiB .. < SCOPE_MAX_BYTES
688
+ FLEET_MAX_BYTES 10 GiB .. 24 GiB
689
+ FLEET_HIGH_BYTES 14 GiB .. < FLEET_MAX_BYTES
690
+ SCOPE_TASKS_MAX 2048 .. 12288
691
+ FLEET_TASKS_MAX 8192 .. 32768 (and >= 4x SCOPE_TASKS_MAX)
692
+
693
+ Nothing was applied — this session is UNPROTECTED. To restore the shipped values:
694
+ git checkout -- .claude/hooks/memory-backstop.sh"
695
+ exit 0
696
+ fi
697
+
698
+ # (4) Identity. Adopt nothing without a positive match.
699
+ local found
700
+ if ! found=$(discover_agent_pid "$$" /proc); then
701
+ reason="claude_pid_not_found"; _log; _maybe_never_worked "$stamp_file" "$log_file"; exit 0
702
+ fi
703
+ claude_pid=${found%% *}
704
+ identity_signal=${found##* }
705
+
706
+ if ! terminal_scope=$(terminal_scope_for "$claude_pid"); then
707
+ reason="no_terminal_scope"; _log; exit 0
708
+ fi
709
+
710
+ # (5) Serialize discover→apply→log. SessionStart fires up to four ways per
711
+ # PID; `startup` then a fast `/clear`, or overlapping `resume`+`compact`, has
712
+ # both invocations reach SetUnitProperties and re-sweep different trees.
713
+ exec 9>>"$lock_file" 2>/dev/null
714
+ if ! flock -w 30 -x 9 2>/dev/null; then
715
+ reason="concurrent_apply"; _log; exit 0
716
+ fi
717
+
718
+ local starttime; starttime=$(read_starttime "$claude_pid" /proc || echo "0")
719
+ scope="soleur-agent-${claude_pid}.scope"
720
+
721
+ # Read the slice's PRE-EXISTING values. 14 active worktrees on this box and
722
+ # .claude/hooks/ is per-checkout, so the moment caps are tuned old- and
723
+ # new-version sessions flap the SHARED slice. Logging only what we wrote would
724
+ # make that flap invisible.
725
+ slice_high_before=$(systemctl --user show "$SLICE_NAME" -p MemoryHigh --value 2>/dev/null)
726
+ slice_max_before=$(systemctl --user show "$SLICE_NAME" -p MemoryMax --value 2>/dev/null)
727
+
728
+ # (6) Slice first: the fleet bound and the oomd preference are live before
729
+ # anything can be charged. runtime=true writes only under /run/user/<uid>/ —
730
+ # `systemctl --user set-property` without it permanently mutates
731
+ # ~/.config/systemd/user.control/, which this hook has no business doing.
732
+ #
733
+ # ManagedOOMPreference=avoid: systemd-oomd is a USERSPACE killer that SIGKILLs
734
+ # every PID in the cgroup it selects and ignores OOMPolicy entirely. Its
735
+ # candidate set is the monitored cgroup's direct children, so once soleur.slice
736
+ # exists the whole fleet becomes one target. Honest caveat: oomctl currently
737
+ # monitors zero cgroups, so this property is armed but its runtime efficacy is
738
+ # UNVALIDATED — the readback proves a string was transmitted, not that
739
+ # behaviour changed.
740
+ "${TO[@]}" busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
741
+ org.freedesktop.systemd1.Manager SetUnitProperties "sba(sv)" \
742
+ "$SLICE_NAME" true 5 \
743
+ "MemoryHigh" "t" "$FLEET_HIGH_BYTES" \
744
+ "MemoryMax" "t" "$FLEET_MAX_BYTES" \
745
+ "MemorySwapMax" "t" 0 \
746
+ "TasksMax" "t" "$FLEET_TASKS_MAX" \
747
+ "ManagedOOMPreference" "s" "avoid" \
748
+ >/dev/null 2>&1
749
+
750
+ # ...and on the PARENT slice too. systemd-oomd chooses among the MONITORED
751
+ # cgroup's DIRECT CHILDREN, and the monitored cgroup is user@<uid>.service —
752
+ # whose direct child is `soleur.slice`, not `soleur-agents.slice`. Setting
753
+ # `avoid` only on the latter arms it on a unit oomd never consults, which is a
754
+ # targeting error rather than the separately-disclosed "efficacy unvalidated"
755
+ # caveat. (systemd creates `soleur.slice` implicitly via the dash convention.)
756
+ "${TO[@]}" busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
757
+ org.freedesktop.systemd1.Manager SetUnitProperties "sba(sv)" \
758
+ "soleur.slice" true 1 \
759
+ "ManagedOOMPreference" "s" "avoid" \
760
+ >/dev/null 2>&1
761
+
762
+ # (7) Adopt the tree.
763
+ local -a tree=()
764
+ mapfile -t tree < <(collect_descendants "$claude_pid" /proc)
765
+ if [[ "${tree[-1]:-}" == "TRUNCATED" ]]; then
766
+ tree_truncated="true"
767
+ unset 'tree[-1]'
768
+ fi
769
+ (( ${#tree[@]} == 0 )) && tree=("$claude_pid")
770
+ tree_size=${#tree[@]}
771
+
772
+ # ADOPT claude's OWN PID ONLY in StartTransientUnit, then attach the rest.
773
+ # Measured: systemd fails the ENTIRE call with "Failed to set unit properties:
774
+ # No such process" if ANY pid in the array has exited between collection and
775
+ # the call — and an agent session spawns and reaps short-lived children
776
+ # continuously, so a whole-tree array loses that race on most runs. Passing the
777
+ # one stable PID makes scope creation deterministic; descendants are attached
778
+ # separately below, where a dead PID costs only itself. Anything spawned after
779
+ # this point inherits the cgroup automatically.
780
+ local -a pid_args=("PIDs" "au" 1 "$claude_pid")
781
+
782
+ # OOMPolicy=continue is passed EXPLICITLY: transient scopes default to `stop`,
783
+ # which makes systemd kill the whole scope — including claude — on any OOM,
784
+ # losing the session this exists to protect. With `continue` the kernel kills
785
+ # the single largest task, which at MemoryMax is the runaway by construction.
786
+ #
787
+ # Delegate=true is REQUIRED, not cosmetic: without it AttachProcessesToUnit
788
+ # fails with "Process migration not available on non-delegated units", and the
789
+ # re-entry re-sweep below is unimplementable — the only other route is writing
790
+ # cgroup.procs directly, which is #7151's single-writer violation.
791
+ "${TO[@]}" busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
792
+ org.freedesktop.systemd1.Manager StartTransientUnit "ssa(sv)a(sa(sv))" \
793
+ "$scope" "fail" 11 \
794
+ "${pid_args[@]}" \
795
+ "Slice" "s" "$SLICE_NAME" \
796
+ "MemoryHigh" "t" "$SCOPE_HIGH_BYTES" \
797
+ "MemoryMax" "t" "$SCOPE_MAX_BYTES" \
798
+ "MemorySwapMax" "t" 0 \
799
+ "TasksMax" "t" "$SCOPE_TASKS_MAX" \
800
+ "OOMPolicy" "s" "continue" \
801
+ "Delegate" "b" true \
802
+ "BindsTo" "as" 1 "$terminal_scope" \
803
+ "After" "as" 1 "$terminal_scope" \
804
+ "Description" "s" "Soleur agent session $claude_pid" \
805
+ 0 \
806
+ >/dev/null 2>&1
807
+ local start_rc=$?
808
+ # ^ That trailing `0` is the empty `aux` array of the signature
809
+ # `ssa(sv)a(sa(sv))`. Omitting it makes busctl fail with "Too few parameters
810
+ # for signature" — and because this hook redirects busctl's stdout AND stderr
811
+ # (the job object path would otherwise land in session context), the failure is
812
+ # invisible: the run falls through to the re-entry branch, invents a
813
+ # PID-reuse-disambiguated scope name, and every subsequent readback is empty.
814
+ # Caught only by the membership verification below.
815
+
816
+ if (( start_rc != 0 )); then
817
+ # Unit already exists (the normal re-entry case) — OR a stale scope left
818
+ # `active (abandoned)` by a straggler whose PID has since been REUSED. Do not
819
+ # treat rc=1 as "already adopted": refreshing a scope this process is not a
820
+ # member of reports success while nothing is capped, which is #7151's failure
821
+ # shape by a different route.
822
+ local our_cg; our_cg=$(cut -d: -f3 < "/proc/$claude_pid/cgroup" 2>/dev/null | head -1)
823
+ if [[ "${our_cg##*/}" != "$scope" ]]; then
824
+ scope="soleur-agent-${claude_pid}-${starttime}.scope"
825
+ reason="pid_reuse_disambiguated"
826
+ "${TO[@]}" busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
827
+ org.freedesktop.systemd1.Manager StartTransientUnit "ssa(sv)a(sa(sv))" \
828
+ "$scope" "fail" 11 \
829
+ "${pid_args[@]}" \
830
+ "Slice" "s" "$SLICE_NAME" \
831
+ "MemoryHigh" "t" "$SCOPE_HIGH_BYTES" \
832
+ "MemoryMax" "t" "$SCOPE_MAX_BYTES" \
833
+ "MemorySwapMax" "t" 0 \
834
+ "TasksMax" "t" "$SCOPE_TASKS_MAX" \
835
+ "OOMPolicy" "s" "continue" \
836
+ "Delegate" "b" true \
837
+ "BindsTo" "as" 1 "$terminal_scope" \
838
+ "After" "as" 1 "$terminal_scope" \
839
+ "Description" "s" "Soleur agent session $claude_pid" \
840
+ 0 \
841
+ >/dev/null 2>&1
842
+ else
843
+ # Ours. Read attribution BEFORE refreshing, then refresh caps in place —
844
+ # this is how a changed cap lands without a session restart.
845
+ local scg="/sys/fs/cgroup${our_cg}"
846
+ local last_oom last_high
847
+ last_oom=$(_last_logged "$log_file" last_oom_kill "$scope")
848
+ last_high=$(_last_logged "$log_file" last_high "$scope")
849
+ msg=$(attribution_message "$scg/memory.events" "$scg/memory.peak" "$last_oom" "$last_high")
850
+
851
+ # NEVER re-derive the terminal scope here: our own cgroup basename is now
852
+ # our scope, so re-deriving would bind the scope to ITSELF and silently
853
+ # destroy the kill switch. Preserve the existing BindsTo instead.
854
+ local existing_bt; existing_bt=$(systemctl --user show "$scope" -p BindsTo --value 2>/dev/null)
855
+ terminal_scope="${existing_bt:-$terminal_scope}"
856
+
857
+ # OOMPolicy is deliberately NOT in this call — the same exclusion
858
+ # repair_eval_scope documents: it is creation-only on scopes (measured
859
+ # on systemd 261 — SetUnitProperties rejects it with "Cannot set
860
+ # property OOMPolicy", and the call is ALL-OR-NOTHING, so the form that
861
+ # carried it could never apply any of the four caps: #9246). Every
862
+ # version of this hook has created scopes with OOMPolicy=continue, so
863
+ # the value persists and there is nothing on that axis to refresh.
864
+ "${TO[@]}" busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
865
+ org.freedesktop.systemd1.Manager SetUnitProperties "sba(sv)" \
866
+ "$scope" true 4 \
867
+ "MemoryHigh" "t" "$SCOPE_HIGH_BYTES" \
868
+ "MemoryMax" "t" "$SCOPE_MAX_BYTES" \
869
+ "MemorySwapMax" "t" 0 \
870
+ "TasksMax" "t" "$SCOPE_TASKS_MAX" \
871
+ >/dev/null 2>&1
872
+ # Ledgered as refresh_rc: the rejection signature of #9246 — an
873
+ # all-or-nothing call carrying a creation-only property fails HERE,
874
+ # while the TasksMax readback can still be healed green by
875
+ # repair_stale_scopes in the same pass. (rc alone cannot prove the cap
876
+ # landed — see the readback rationale below; it proves the call was not
877
+ # REJECTED.)
878
+ refresh_rc=$?
879
+
880
+ fi
881
+ fi
882
+
883
+ # Sweep the descendants into the scope. Runs on EVERY path, not just re-entry:
884
+ # on a fresh adoption the MCP servers are already running (SessionStart races
885
+ # MCP startup, and they are direct children of claude), and on re-entry this is
886
+ # what closes that race for anything spawned since. Attaching is per-process
887
+ # and best-effort — one PID that exited between collection and now costs only
888
+ # itself, which is exactly why the tree is not passed to StartTransientUnit.
889
+ for p in "${tree[@]}"; do
890
+ [[ "$p" == "$claude_pid" ]] && continue
891
+ [[ -r "/proc/$p/cgroup" ]] || continue
892
+ local pcg; pcg=$(cut -d: -f3 < "/proc/$p/cgroup" 2>/dev/null | head -1)
893
+ [[ "${pcg##*/}" == "$scope" ]] && { attached=$((attached + 1)); continue; }
894
+ if "${TO[@]}" busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
895
+ org.freedesktop.systemd1.Manager AttachProcessesToUnit "ssau" \
896
+ "$scope" "/" 1 "$p" >/dev/null 2>&1; then
897
+ attached=$((attached + 1))
898
+ fi
899
+ done
900
+
901
+ # (7b) Sweep unadopted sibling agents — inside the flock so simultaneous
902
+ # SessionStarts serialize their sweeps rather than racing StartTransientUnit
903
+ # on the same stray pid.
904
+ swept=$(sweep_unadopted_agents "$claude_pid")
905
+
906
+ # (7c) Converge scopes adopted under OLDER cap constants — the sweep skips
907
+ # soleur-agent-*.scope members by construction, so without this a stale-
908
+ # adopted scope keeps stale caps until its own next SessionStart, or forever.
909
+ # Any new-version run on the host repairs them all; bounded by MAX_REPAIR,
910
+ # runtime-only, BindsTo untouched (#9239).
911
+ repaired=$(repair_stale_scopes)
912
+ [[ "$repaired" =~ ^[0-9]+$ ]] || repaired=0
913
+
914
+ # VERIFY, do not assume. Reporting `applied` because a D-Bus call was issued is
915
+ # precisely the #7151 defect — a green signal over an inert guard.
916
+ #
917
+ # Membership alone is NOT enough, and on the re-entry path it is worse than not
918
+ # enough: that branch is only reached when the PID is already in `$scope`, so
919
+ # asserting membership there re-asserts the branch's own entry condition and
920
+ # can never fail. Every `/clear` and every resume takes that path. So read back
921
+ # the properties that actually constitute the cap, at BOTH levels, and record
922
+ # what was OBSERVED rather than what was intended.
923
+ #
924
+ # Reading back is required rather than checking rc: SetUnitProperties returns
925
+ # rc=0 for a unit that does not exist yet (it loads it inactive), so a zero
926
+ # exit proves the call was accepted, not that the running unit carries the cap.
927
+ # And the call is all-or-nothing — one unsupported property (ManagedOOMPreference
928
+ # needs systemd >= 247) drops MemoryHigh, MemoryMax AND MemorySwapMax with it,
929
+ # which would silently restore the swap-exhaustion half of the incident.
930
+ local final_cg
931
+ final_cg=$(cut -d: -f3 < "/proc/$claude_pid/cgroup" 2>/dev/null | head -1)
932
+ scope_max_after=$(systemctl --user show "$scope" -p MemoryMax --value 2>/dev/null)
933
+ scope_high_after=$(systemctl --user show "$scope" -p MemoryHigh --value 2>/dev/null)
934
+ scope_swap_after=$(systemctl --user show "$scope" -p MemorySwapMax --value 2>/dev/null)
935
+ scope_tasks_after=$(systemctl --user show "$scope" -p TasksMax --value 2>/dev/null)
936
+ slice_high_after=$(systemctl --user show "$SLICE_NAME" -p MemoryHigh --value 2>/dev/null)
937
+ slice_max_after=$(systemctl --user show "$SLICE_NAME" -p MemoryMax --value 2>/dev/null)
938
+ slice_swap_after=$(systemctl --user show "$SLICE_NAME" -p MemorySwapMax --value 2>/dev/null)
939
+ slice_tasks_after=$(systemctl --user show "$SLICE_NAME" -p TasksMax --value 2>/dev/null)
940
+
941
+ if [[ "${final_cg##*/}" != "$scope" ]]; then
942
+ outcome="failed"
943
+ reason="adoption_unverified"
944
+ msg="Soleur memory backstop could NOT cap this session — it is running UNPROTECTED.
945
+
946
+ The scope was requested but this process is not a member of it:
947
+ expected: $scope
948
+ actual: $(_sanitize "${final_cg##*/}")
949
+
950
+ Nothing else is affected and your session is fine, but a runaway command here is
951
+ not bounded. Details: .claude/.memory-backstop.jsonl"
952
+ elif [[ "$scope_max_after" != "$SCOPE_MAX_BYTES" || "$scope_high_after" != "$SCOPE_HIGH_BYTES" \
953
+ || "$scope_swap_after" != "0" || "$scope_tasks_after" != "$SCOPE_TASKS_MAX" ]]; then
954
+ outcome="failed"
955
+ reason="scope_caps_unverified"
956
+ msg="Soleur memory backstop: this session's scope exists but its limits were NOT applied.
957
+
958
+ MemoryMax expected $(_human_bytes "$SCOPE_MAX_BYTES"), got ${scope_max_after:-<unset>}
959
+ MemoryHigh expected $(_human_bytes "$SCOPE_HIGH_BYTES"), got ${scope_high_after:-<unset>}
960
+ MemorySwapMax expected 0, got ${scope_swap_after:-<unset>}
961
+ TasksMax expected $SCOPE_TASKS_MAX, got ${scope_tasks_after:-<unset>}
962
+
963
+ A runaway command here is NOT bounded. Details: .claude/.memory-backstop.jsonl"
964
+ elif [[ "$slice_max_after" != "$FLEET_MAX_BYTES" || "$slice_high_after" != "$FLEET_HIGH_BYTES" \
965
+ || "$slice_swap_after" != "0" || "$slice_tasks_after" != "$FLEET_TASKS_MAX" ]]; then
966
+ # This session is capped, but the SHARED bound is not there — so N sessions
967
+ # can still exhaust the box between them, and swap is uncapped at the slice.
968
+ outcome="failed"
969
+ reason="fleet_caps_unverified"
970
+ msg="Soleur memory backstop: this session is capped, but the FLEET-WIDE limit was not applied.
971
+
972
+ $SLICE_NAME MemoryMax expected $(_human_bytes "$FLEET_MAX_BYTES"), got ${slice_max_after:-<unset>}
973
+ $SLICE_NAME MemoryHigh expected $(_human_bytes "$FLEET_HIGH_BYTES"), got ${slice_high_after:-<unset>}
974
+ $SLICE_NAME MemorySwapMax expected 0, got ${slice_swap_after:-<unset>}
975
+ $SLICE_NAME TasksMax expected $FLEET_TASKS_MAX, got ${slice_tasks_after:-<unset>}
976
+
977
+ Each session is still individually bounded, but several sessions together are not.
978
+ On systemd older than 247 this is expected: ManagedOOMPreference is unsupported and
979
+ fails the whole property call. Details: .claude/.memory-backstop.jsonl"
980
+ else
981
+ outcome="applied"
982
+ # Distinguish a fresh adoption (caps proven by StartTransientUnit succeeding)
983
+ # from a refresh (caps proven only by the readback above) — otherwise the log
984
+ # cannot tell the two apart after the fact.
985
+ [[ -z "$reason" ]] && reason=$( (( start_rc == 0 )) && echo "ok_fresh" || echo "ok_refreshed" )
986
+ fi
987
+
988
+ # Record the counters this run observed so the next run compares against them.
989
+ local scg2 cur_oom=0 cur_high=0
990
+ scg2="/sys/fs/cgroup$(cut -d: -f3 < "/proc/$claude_pid/cgroup" 2>/dev/null | head -1)"
991
+ if [[ -r "$scg2/memory.events" ]]; then
992
+ cur_oom=$(awk '/^oom_kill /{print $2}' "$scg2/memory.events" 2>/dev/null || echo 0)
993
+ cur_high=$(awk '/^high /{print $2}' "$scg2/memory.events" 2>/dev/null || echo 0)
994
+ fi
995
+ [[ "$cur_oom" =~ ^[0-9]+$ ]] || cur_oom=0
996
+ [[ "$cur_high" =~ ^[0-9]+$ ]] || cur_high=0
997
+
998
+ _log_with_counters "$log_file" "$cur_oom" "$cur_high"
999
+
1000
+ # (8) Edge-triggered messages only — never per-session.
1001
+ if [[ -n "$msg" ]]; then
1002
+ emit_message "$msg"
1003
+ elif [[ ! -f "$stamp_file" ]]; then
1004
+ printf '%s\n' "$(hostname 2>/dev/null || echo host):${SCOPE_MAX_BYTES}:${FLEET_MAX_BYTES}" > "$stamp_file" 2>/dev/null
1005
+ emit_message "Soleur memory backstop is active on this machine.
1006
+
1007
+ Each agent session now runs in its own systemd scope capped on memory AND task
1008
+ count, and all of them share fleet-wide limits — so one runaway command can no
1009
+ longer exhaust memory, swap, or the shared fork budget and take down every
1010
+ open session.
1011
+
1012
+ If a session is stopped unexpectedly you will get a message explaining what hit
1013
+ which limit. To stop just one session: systemctl --user stop soleur-agent-<pid>.scope
1014
+ To raise one session's limit: systemctl --user set-property --runtime soleur-agent-<pid>.scope MemoryHigh=infinity MemoryMax=infinity
1015
+ To stop EVERY agent session on this box: systemctl --user stop soleur.slice (this kills them all)
1016
+ To turn the backstop off entirely: set SOLEUR_DISABLE_MEMORY_BACKSTOP=1"
1017
+ fi
1018
+
1019
+ exit 0
1020
+ }
1021
+
1022
+ # Last recorded value of a counter field, for the increase comparison.
1023
+ # Counters are per-SCOPE, but the log is per-worktree and several sessions share
1024
+ # it. Without the scope filter, session B (a fresh scope, counters at 0) writes
1025
+ # last_oom_kill=0, and session A — which has already seen 3 kills and already
1026
+ # messaged — then reads 0, decides 3 > 0, and re-emits its post-mortem. The ADR's
1027
+ # own scenario is six concurrent sessions, so that is the default condition, not
1028
+ # an edge case.
1029
+ _last_logged() {
1030
+ local log_file=$1 field=$2 want_scope=$3
1031
+ [[ -r "$log_file" ]] || { printf '0'; return 0; }
1032
+ local v
1033
+ v=$(tail -200 "$log_file" 2>/dev/null \
1034
+ | jq -r --arg f "$field" --arg s "$want_scope" \
1035
+ 'select(.scope == $s and .[$f] != null) | .[$f]' 2>/dev/null | tail -1)
1036
+ [[ "$v" =~ ^[0-9]+$ ]] || v=0
1037
+ printf '%s' "$v"
1038
+ }
1039
+
1040
+ # Append the applied line, carrying the counters forward for the next run's
1041
+ # increase comparison.
1042
+ _log_with_counters() {
1043
+ local log_file=$1 cur_oom=$2 cur_high=$3
1044
+ # Rotate here too. `_log` is only reached on the DECLINE paths, so on a machine
1045
+ # where the hook always applies nothing ever rotated and the sink grew forever.
1046
+ local _rot; _rot="$(dirname "${BASH_SOURCE[0]}")/lib/log-rotation.sh"
1047
+ if [[ -f "$_rot" ]]; then
1048
+ # shellcheck source=/dev/null
1049
+ source "$_rot" 2>/dev/null || true
1050
+ declare -F rotate_if_needed >/dev/null 2>&1 && rotate_if_needed "$log_file" 2>/dev/null
1051
+ fi
1052
+ if command -v jq >/dev/null 2>&1; then
1053
+ jq -nc \
1054
+ --arg ts "$ts" --arg outcome "$outcome" --arg reason "$reason" \
1055
+ --arg scope "$scope" --arg tscope "$terminal_scope" --arg slice "$SLICE_NAME" \
1056
+ --arg sig "$identity_signal" --arg pid "$claude_pid" \
1057
+ --arg shb "$slice_high_before" --arg smb "$slice_max_before" \
1058
+ --argjson tree "$tree_size" --argjson oom "$cur_oom" --argjson hi "$cur_high" \
1059
+ --argjson sh "$SCOPE_HIGH_BYTES" --argjson sm "$SCOPE_MAX_BYTES" \
1060
+ --argjson fh "$FLEET_HIGH_BYTES" --argjson fm "$FLEET_MAX_BYTES" \
1061
+ --argjson st "$SCOPE_TASKS_MAX" --argjson ft "$FLEET_TASKS_MAX" \
1062
+ --arg sha "$scope_high_after" --arg sma "$scope_max_after" --arg swa "$scope_swap_after" \
1063
+ --arg sta "$scope_tasks_after" \
1064
+ --arg fha "$slice_high_after" --arg fma "$slice_max_after" --arg fwa "$slice_swap_after" \
1065
+ --arg fta "$slice_tasks_after" \
1066
+ --argjson att "$attached" --argjson trunc "$tree_truncated" \
1067
+ --argjson swept "$swept" \
1068
+ --argjson bsr "$BACKSTOP_REVISION" \
1069
+ --arg rfrom "${SOLEUR_BACKSTOP_RESOLVED_FROM:-}" \
1070
+ --argjson rep "${repaired:-0}" \
1071
+ --argjson rrc "${refresh_rc:-null}" \
1072
+ '{schema:2, ts:$ts, pid:(($pid|tonumber?) // null), tree_size:$tree, scope:$scope,
1073
+ terminal_scope:$tscope, slice:$slice,
1074
+ scope_high:$sh, scope_max:$sm, slice_high:$fh, slice_max:$fm,
1075
+ scope_tasks:$st, slice_tasks:$ft,
1076
+ scope_high_after:$sha, scope_max_after:$sma, scope_swap_after:$swa,
1077
+ scope_tasks_after:$sta,
1078
+ slice_high_after:$fha, slice_max_after:$fma, slice_swap_after:$fwa,
1079
+ slice_tasks_after:$fta,
1080
+ slice_high_before:$shb, slice_max_before:$smb,
1081
+ backstop_revision:$bsr, resolved_from:$rfrom, repaired:$rep,
1082
+ refresh_rc:$rrc,
1083
+ attached:$att, tree_truncated:$trunc, swept:$swept,
1084
+ swap_max:0, identity_signal:$sig, outcome:$outcome, reason:$reason,
1085
+ last_oom_kill:$oom, last_high:$hi}' 2>/dev/null >> "$log_file"
1086
+ fi
1087
+ }
1088
+
1089
+ # R11 — the never-worked edge trigger. The regression channel is "previously
1090
+ # succeeded and now fails", so on a machine where it NEVER succeeded there is no
1091
+ # "previously" and it never fires: a non-standard install (npm-global gives
1092
+ # exe=/usr/bin/node) would log `skipped` forever while the operator believes they
1093
+ # are protected. That is #7151's own class, so it needs its own detector.
1094
+ _maybe_never_worked() {
1095
+ local stamp_file=$1 log_file=$2
1096
+ [[ -f "$stamp_file" ]] && return 0
1097
+ [[ -f "${stamp_file}.nagged" ]] && return 0
1098
+ printf 'nagged\n' > "${stamp_file}.nagged" 2>/dev/null
1099
+ emit_message "Soleur memory backstop could not identify this agent session, so NO memory cap is in effect here.
1100
+
1101
+ This machine has a working systemd user bus, so the backstop should have applied.
1102
+ The likely cause is a non-standard agent install (the process identity check
1103
+ looks for a claude/devin/codex executable in the ancestry).
1104
+
1105
+ Nothing is broken and your session is unaffected — but you are not protected from
1106
+ a runaway command exhausting memory. Details: .claude/.memory-backstop.jsonl"
1107
+ }
1108
+
1109
+ # `if`, not `[[ ... ]] && main`: as the file's LAST command the `&&` form returns
1110
+ # 1 when the condition is false, so `source` itself would report failure to the
1111
+ # test harness even though sourcing succeeded.
1112
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
1113
+ main "$@"
1114
+ fi