@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.
- package/.claude-plugin/agents.manifest.json +476 -0
- package/.claude-plugin/plugin.json +42 -0
- package/README.md +44 -2
- package/agents/community/.gitkeep +0 -0
- package/agents/engineering/cto.md +87 -0
- package/agents/engineering/design/ddd-architect.md +95 -0
- package/agents/engineering/discovery/agent-finder.md +214 -0
- package/agents/engineering/discovery/functional-discovery.md +182 -0
- package/agents/engineering/infra/infra-security.md +151 -0
- package/agents/engineering/infra/platform-strategist.md +109 -0
- package/agents/engineering/infra/terraform-architect.md +171 -0
- package/agents/engineering/prompt-engineer.md +37 -0
- package/agents/engineering/research/best-practices-researcher.md +115 -0
- package/agents/engineering/research/framework-docs-researcher.md +93 -0
- package/agents/engineering/research/git-history-analyzer.md +53 -0
- package/agents/engineering/research/learnings-researcher.md +285 -0
- package/agents/engineering/research/repo-research-analyst.md +118 -0
- package/agents/engineering/review/agent-native-reviewer.md +279 -0
- package/agents/engineering/review/architecture-strategist.md +61 -0
- package/agents/engineering/review/code-quality-analyst.md +92 -0
- package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
- package/agents/engineering/review/data-integrity-guardian.md +82 -0
- package/agents/engineering/review/data-migration-expert.md +98 -0
- package/agents/engineering/review/deployment-verification-agent.md +183 -0
- package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
- package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
- package/agents/engineering/review/legacy-code-expert.md +55 -0
- package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
- package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
- package/agents/engineering/review/performance-oracle.md +130 -0
- package/agents/engineering/review/security-sentinel.md +167 -0
- package/agents/engineering/review/semgrep-sast.md +36 -0
- package/agents/engineering/review/test-design-reviewer.md +150 -0
- package/agents/engineering/review/user-impact-reviewer.md +70 -0
- package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
- package/agents/finance/budget-analyst.md +29 -0
- package/agents/finance/cfo.md +44 -0
- package/agents/finance/financial-reporter.md +30 -0
- package/agents/finance/revenue-analyst.md +30 -0
- package/agents/legal/clo.md +115 -0
- package/agents/legal/legal-compliance-auditor.md +110 -0
- package/agents/legal/legal-document-generator.md +134 -0
- package/agents/marketing/analytics-analyst.md +25 -0
- package/agents/marketing/brand-architect.md +181 -0
- package/agents/marketing/cmo.md +76 -0
- package/agents/marketing/conversion-optimizer.md +19 -0
- package/agents/marketing/copywriter.md +20 -0
- package/agents/marketing/fact-checker.md +70 -0
- package/agents/marketing/growth-strategist.md +145 -0
- package/agents/marketing/paid-media-strategist.md +28 -0
- package/agents/marketing/pricing-strategist.md +17 -0
- package/agents/marketing/programmatic-seo-specialist.md +25 -0
- package/agents/marketing/retention-strategist.md +23 -0
- package/agents/marketing/seo-aeo-analyst.md +164 -0
- package/agents/operations/coo.md +50 -0
- package/agents/operations/ops-advisor.md +78 -0
- package/agents/operations/ops-provisioner.md +101 -0
- package/agents/operations/ops-research.md +52 -0
- package/agents/operations/service-automator.md +278 -0
- package/agents/product/business-validator.md +206 -0
- package/agents/product/competitive-intelligence.md +89 -0
- package/agents/product/cpo.md +63 -0
- package/agents/product/design/ux-design-lead.md +209 -0
- package/agents/product/spec-flow-analyzer.md +122 -0
- package/agents/sales/cro.md +44 -0
- package/agents/sales/deal-architect.md +29 -0
- package/agents/sales/outbound-strategist.md +29 -0
- package/agents/sales/pipeline-analyst.md +29 -0
- package/agents/support/cco.md +48 -0
- package/agents/support/community-manager.md +570 -0
- package/agents/support/ticket-triage.md +70 -0
- package/bin/soleur-opencode.js +29 -0
- package/commands/go.md +659 -0
- package/commands/help.md +273 -0
- package/commands/sync.md +977 -0
- package/commands/version.md +103 -0
- package/hooks/antigravity-hooks.json +84 -0
- package/hooks/antigravity-session-start.sh +15 -0
- package/hooks/bare-root-cwd-guard.sh +131 -0
- package/hooks/browser-snapshot-credential-guard.sh +200 -0
- package/hooks/codex-session-start.sh +9 -0
- package/hooks/compaction-state.sh +525 -0
- package/hooks/destructive-command-guard.sh +1387 -0
- package/hooks/devin-session-start.sh +90 -0
- package/hooks/ext-filter-detect.sh +171 -0
- package/hooks/hooks.json +133 -0
- package/hooks/lib/hook-tool-kind.sh +24 -0
- package/hooks/lib/log-rotation.sh +176 -0
- package/hooks/lib/shell-argv.pl +576 -0
- package/hooks/memory-backstop.sh +1114 -0
- package/hooks/operator-stage-approval.sh +391 -0
- package/hooks/stop-hook.sh +367 -0
- package/hooks/unkept-promise-hook.sh +383 -0
- package/hooks/version-banner.sh +127 -0
- package/hooks/welcome-hook.sh +50 -0
- package/install.js +108 -0
- package/lib/agent-registry.ts +154 -0
- package/lib/c4-canonical-cli.mjs +34 -0
- package/lib/c4-canonical.mjs +46 -0
- package/lib/c4-from-components.ts +554 -0
- package/lib/go-routing.ts +81 -0
- package/lib/grok-inspect-contract.ts +311 -0
- package/lib/harness-model-map.ts +101 -0
- package/lib/harness-parity.ts +726 -0
- package/lib/harness-tool-map.ts +180 -0
- package/lib/harness.ts +716 -0
- package/lib/heartbeat-live-reconcile.ts +1153 -0
- package/lib/heartbeat-manifest.ts +342 -0
- package/lib/kb-coverage.ts +252 -0
- package/lib/pr-merge-poll.ts +103 -0
- package/lib/workflow-fidelity.ts +388 -0
- package/opencode/INSTRUCTIONS.md +152 -0
- package/opencode/agents/engineering/cto.md +10 -0
- package/opencode/agents/engineering/design/ddd-architect.md +10 -0
- package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
- package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
- package/opencode/agents/engineering/infra/infra-security.md +10 -0
- package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
- package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
- package/opencode/agents/engineering/prompt-engineer.md +10 -0
- package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
- package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
- package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
- package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
- package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
- package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
- package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
- package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
- package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
- package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
- package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
- package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
- package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
- package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
- package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
- package/opencode/agents/engineering/review/performance-oracle.md +10 -0
- package/opencode/agents/engineering/review/security-sentinel.md +10 -0
- package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
- package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
- package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
- package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
- package/opencode/agents/finance/budget-analyst.md +10 -0
- package/opencode/agents/finance/cfo.md +10 -0
- package/opencode/agents/finance/financial-reporter.md +10 -0
- package/opencode/agents/finance/revenue-analyst.md +10 -0
- package/opencode/agents/legal/clo.md +10 -0
- package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
- package/opencode/agents/legal/legal-document-generator.md +10 -0
- package/opencode/agents/marketing/analytics-analyst.md +10 -0
- package/opencode/agents/marketing/brand-architect.md +10 -0
- package/opencode/agents/marketing/cmo.md +10 -0
- package/opencode/agents/marketing/conversion-optimizer.md +10 -0
- package/opencode/agents/marketing/copywriter.md +10 -0
- package/opencode/agents/marketing/fact-checker.md +10 -0
- package/opencode/agents/marketing/growth-strategist.md +10 -0
- package/opencode/agents/marketing/paid-media-strategist.md +10 -0
- package/opencode/agents/marketing/pricing-strategist.md +10 -0
- package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
- package/opencode/agents/marketing/retention-strategist.md +10 -0
- package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
- package/opencode/agents/operations/coo.md +10 -0
- package/opencode/agents/operations/ops-advisor.md +10 -0
- package/opencode/agents/operations/ops-provisioner.md +10 -0
- package/opencode/agents/operations/ops-research.md +10 -0
- package/opencode/agents/operations/service-automator.md +10 -0
- package/opencode/agents/product/business-validator.md +10 -0
- package/opencode/agents/product/competitive-intelligence.md +10 -0
- package/opencode/agents/product/cpo.md +10 -0
- package/opencode/agents/product/design/ux-design-lead.md +10 -0
- package/opencode/agents/product/spec-flow-analyzer.md +10 -0
- package/opencode/agents/sales/cro.md +10 -0
- package/opencode/agents/sales/deal-architect.md +10 -0
- package/opencode/agents/sales/outbound-strategist.md +10 -0
- package/opencode/agents/sales/pipeline-analyst.md +10 -0
- package/opencode/agents/support/cco.md +10 -0
- package/opencode/agents/support/community-manager.md +10 -0
- package/opencode/agents/support/ticket-triage.md +10 -0
- package/opencode/commands/go.md +10 -0
- package/opencode/commands/help.md +10 -0
- package/opencode/commands/sync.md +10 -0
- package/opencode/commands/version.md +11 -0
- package/opencode/opencode.json +8 -0
- package/opencode/plugin.ts +493 -0
- package/package.json +36 -4
- package/scripts/admin-merge-ready.sh +545 -0
- package/scripts/admin-merge-ready.test.sh +753 -0
- package/scripts/affected-tests.sh +908 -0
- package/scripts/affected-tests.test.sh +1797 -0
- package/scripts/alpha-metrics.sh +78 -0
- package/scripts/alpha-metrics.test.sh +115 -0
- package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
- package/scripts/audit-flag-flip.sh +107 -0
- package/scripts/check-red-on-main.sh +316 -0
- package/scripts/ci-head-verdict.sh +363 -0
- package/scripts/cloud-detect.sh +193 -0
- package/scripts/delegation-verify.sh +657 -0
- package/scripts/delegation-verify.test.sh +1424 -0
- package/scripts/deploy-arm.sh +475 -0
- package/scripts/domain-model-drift.sh +419 -0
- package/scripts/emit-decision.sh +144 -0
- package/scripts/emit-decision.test.sh +144 -0
- package/scripts/ext-filter-divergence-corpus.txt +15 -0
- package/scripts/ext-filter-divergence.sh +189 -0
- package/scripts/generate-c4-from-components.ts +442 -0
- package/scripts/grok-fidelity-gate.sh +68 -0
- package/scripts/grok-pre-push-gate.sh +232 -0
- package/scripts/harness-discovery-smoke.ts +906 -0
- package/scripts/harness-parity-census.ts +92 -0
- package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
- package/scripts/install-opencode-adapter.ts +520 -0
- package/scripts/lib/affected-tests/common.sh +365 -0
- package/scripts/lib/affected-tests/go.sh +157 -0
- package/scripts/lib/affected-tests/js.sh +188 -0
- package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
- package/scripts/lib/affected-tests/python.sh +185 -0
- package/scripts/lib/context-sanitize.sh +49 -0
- package/scripts/lib/domain-model-lib.sh +180 -0
- package/scripts/lib/ext-filter-grammar.tsv +34 -0
- package/scripts/lib/operator-script.sh +1236 -0
- package/scripts/lib/proc.sh +580 -0
- package/scripts/lib/session-state.sh +748 -0
- package/scripts/lib/tmp-classify.sh +843 -0
- package/scripts/lib/wrapper-boundary.sh +47 -0
- package/scripts/lint-ratchet.py +1547 -0
- package/scripts/monitor-pr-checks.sh +441 -0
- package/scripts/pipeline-tally.sh +756 -0
- package/scripts/pipeline-tally.test.sh +740 -0
- package/scripts/plugin-version.sh +528 -0
- package/scripts/precommit-guard.sh +113 -0
- package/scripts/precommit-guard.test.sh +254 -0
- package/scripts/reconcile-live-heartbeats.ts +705 -0
- package/scripts/render-c4-model.sh +217 -0
- package/scripts/resolve-git-root.sh +55 -0
- package/scripts/resolve-regenerable-conflicts.sh +363 -0
- package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
- package/scripts/ruff-requirements.txt +22 -0
- package/scripts/run-in-pid-namespace.sh +98 -0
- package/scripts/run-in-pid-namespace.test.sh +290 -0
- package/scripts/scan-ancestor-signal-helpers.py +556 -0
- package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
- package/scripts/setup-ralph-loop.sh +238 -0
- package/scripts/sync-cursor-name-map.ts +316 -0
- package/scripts/sync-grok-agent-compat.ts +188 -0
- package/scripts/sync-npm-package.ts +178 -0
- package/scripts/sync-opencode-adapter.ts +479 -0
- package/scripts/sync-pr-behind.sh +534 -0
- package/scripts/sync-pr-behind.test.sh +252 -0
- package/scripts/taste-profile-update.sh +180 -0
- package/scripts/taste-profile-update.test.sh +167 -0
- package/scripts/write-budget-marker.sh +23 -0
- package/scripts/write-kb-coverage.ts +113 -0
- package/skills/admin-ip-refresh/SKILL.md +81 -0
- package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
- package/skills/agent-browser/SKILL.md +698 -0
- package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
- package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
- package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
- package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
- package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
- package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
- package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
- package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
- package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
- package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
- package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
- package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
- package/skills/agent-native-architecture/SKILL.md +479 -0
- package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
- package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
- package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
- package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
- package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
- package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
- package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
- package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
- package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
- package/skills/agent-native-architecture/references/product-implications.md +478 -0
- package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
- package/skills/agent-native-architecture/references/self-modification.md +294 -0
- package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
- package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
- package/skills/agent-native-audit/SKILL.md +300 -0
- package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
- package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
- package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
- package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
- package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
- package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
- package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
- package/skills/architecture/SKILL.md +436 -0
- package/skills/architecture/references/adr-template.md +154 -0
- package/skills/architecture/references/likec4-reference.md +136 -0
- package/skills/architecture/references/nfr-reference.md +126 -0
- package/skills/archive-kb/SKILL.md +86 -0
- package/skills/archive-kb/scripts/archive-kb.sh +300 -0
- package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
- package/skills/atdd-developer/SKILL.md +65 -0
- package/skills/brainstorm/SKILL.md +701 -0
- package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
- package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
- package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
- package/skills/brainstorm/references/ui-surface-terms.md +42 -0
- package/skills/brainstorm-techniques/SKILL.md +231 -0
- package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
- package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
- package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
- package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
- package/skills/campaign-calendar/SKILL.md +145 -0
- package/skills/cf-token-scope/SKILL.md +164 -0
- package/skills/cf-token-scope/references/widen-playbook.md +114 -0
- package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
- package/skills/changelog/SKILL.md +142 -0
- package/skills/code-to-prd/SKILL.md +66 -0
- package/skills/code-to-prd/references/banner-template.md +12 -0
- package/skills/code-to-prd/references/prd-template.md +41 -0
- package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
- package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
- package/skills/code-to-prd/test/fixture/.env.example +7 -0
- package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
- package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
- package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
- package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
- package/skills/code-to-prd/test/fixture/package.json +6 -0
- package/skills/cohort-status/SKILL.md +62 -0
- package/skills/community/SKILL.md +210 -0
- package/skills/community/scripts/bsky-community.sh +644 -0
- package/skills/community/scripts/bsky-setup.sh +631 -0
- package/skills/community/scripts/community-router.sh +93 -0
- package/skills/community/scripts/discord-community.sh +487 -0
- package/skills/community/scripts/discord-setup.sh +784 -0
- package/skills/community/scripts/github-community.sh +706 -0
- package/skills/community/scripts/hn-community.sh +233 -0
- package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
- package/skills/community/scripts/linkedin-community.sh +750 -0
- package/skills/community/scripts/linkedin-setup.sh +758 -0
- package/skills/community/scripts/x-community.sh +890 -0
- package/skills/community/scripts/x-setup.sh +713 -0
- package/skills/community/test/community-argv.test.sh +2922 -0
- package/skills/community/test/github-community.test.sh +981 -0
- package/skills/competitive-analysis/SKILL.md +69 -0
- package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
- package/skills/compound/SKILL.md +673 -0
- package/skills/compound/scripts/token-efficiency-report.sh +334 -0
- package/skills/compound/test/phase-16.test.sh +379 -0
- package/skills/compound-capture/SKILL.md +791 -0
- package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
- package/skills/compound-capture/assets/resolution-template.md +116 -0
- package/skills/compound-capture/references/yaml-schema.md +67 -0
- package/skills/compound-capture/schema.yaml +181 -0
- package/skills/constraint-scaffold/SKILL.md +175 -0
- package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
- package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
- package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
- package/skills/constraint-scaffold/references/shared-runner.template +135 -0
- package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
- package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
- package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
- package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
- package/skills/constraint-scaffold/test/generator.test.sh +93 -0
- package/skills/constraint-scaffold/test/parity.test.sh +285 -0
- package/skills/content-writer/SKILL.md +242 -0
- package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
- package/skills/cron-delete/SKILL.md +56 -0
- package/skills/cron-list/SKILL.md +54 -0
- package/skills/deepen-plan/SKILL.md +937 -0
- package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
- package/skills/deploy/SKILL.md +115 -0
- package/skills/deploy/references/hetzner-setup.md +65 -0
- package/skills/deploy/scripts/deploy.sh +37 -0
- package/skills/deploy-docs/SKILL.md +95 -0
- package/skills/dhh-rails-style/SKILL.md +203 -0
- package/skills/dhh-rails-style/references/architecture.md +717 -0
- package/skills/dhh-rails-style/references/controllers.md +323 -0
- package/skills/dhh-rails-style/references/frontend.md +556 -0
- package/skills/dhh-rails-style/references/gems.md +316 -0
- package/skills/dhh-rails-style/references/models.md +388 -0
- package/skills/dhh-rails-style/references/testing.md +354 -0
- package/skills/discord-content/SKILL.md +126 -0
- package/skills/docs-site/SKILL.md +214 -0
- package/skills/drain-labeled-backlog/SKILL.md +203 -0
- package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
- package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
- package/skills/drain-prs/SKILL.md +142 -0
- package/skills/drain-prs/scripts/triage-prs.sh +192 -0
- package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
- package/skills/dspy-ruby/SKILL.md +634 -0
- package/skills/dspy-ruby/assets/config-template.rb +359 -0
- package/skills/dspy-ruby/assets/module-template.rb +326 -0
- package/skills/dspy-ruby/assets/signature-template.rb +143 -0
- package/skills/dspy-ruby/references/core-concepts.md +268 -0
- package/skills/dspy-ruby/references/optimization.md +623 -0
- package/skills/dspy-ruby/references/providers.md +335 -0
- package/skills/eval-harness/README.md +194 -0
- package/skills/eval-harness/SKILL.md +157 -0
- package/skills/eval-harness/enums/go-routes.json +13 -0
- package/skills/eval-harness/enums/incident-threshold.json +5 -0
- package/skills/eval-harness/enums/lane.json +5 -0
- package/skills/eval-harness/enums/tool-selection.json +20 -0
- package/skills/eval-harness/enums/triage-levels.json +5 -0
- package/skills/eval-harness/gated-skills.json +34 -0
- package/skills/eval-harness/models.generated.json +5 -0
- package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
- package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
- package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
- package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
- package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
- package/skills/eval-harness/prompts/go-baseline.txt +6 -0
- package/skills/eval-harness/prompts/go-skill.txt +21 -0
- package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
- package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
- package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
- package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
- package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
- package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
- package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
- package/skills/eval-harness/prompts/triage-skill.txt +10 -0
- package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
- package/skills/eval-harness/scripts/extract-block.cjs +87 -0
- package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
- package/skills/eval-harness/scripts/gen-models.sh +41 -0
- package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
- package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
- package/skills/eval-harness/scripts/parse-label.cjs +93 -0
- package/skills/eval-harness/scripts/verdict.cjs +132 -0
- package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
- package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
- package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
- package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
- package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
- package/skills/eval-harness/test/eval-gate.test.sh +132 -0
- package/skills/eval-harness/test/extract-block.test.sh +87 -0
- package/skills/eval-harness/test/gate-classification.test.sh +77 -0
- package/skills/eval-harness/test/gen-models.test.sh +75 -0
- package/skills/eval-harness/test/measure-classification.test.sh +74 -0
- package/skills/eval-harness/test/parse-label.test.sh +129 -0
- package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
- package/skills/eval-harness/test/verdict.test.sh +123 -0
- package/skills/every-style-editor/SKILL.md +142 -0
- package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
- package/skills/feature-tweet/SKILL.md +199 -0
- package/skills/feature-video/SKILL.md +412 -0
- package/skills/feature-video/scripts/check_deps.sh +281 -0
- package/skills/file-todos/SKILL.md +267 -0
- package/skills/file-todos/assets/todo-template.md +166 -0
- package/skills/fix-issue/SKILL.md +278 -0
- package/skills/fix-issue/references/agent-authored-exclusion.md +118 -0
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +96 -0
- package/skills/flag-bootstrap/SETUP.md +142 -0
- package/skills/flag-create/SKILL.md +145 -0
- package/skills/flag-create/scripts/create.sh +273 -0
- package/skills/flag-delete/SKILL.md +167 -0
- package/skills/flag-delete/scripts/delete.sh +288 -0
- package/skills/flag-list/SKILL.md +99 -0
- package/skills/flag-list/scripts/list.sh +275 -0
- package/skills/flag-set-role/SKILL.md +229 -0
- package/skills/flag-set-role/scripts/flip.sh +774 -0
- package/skills/frontend-anti-slop/SKILL.md +106 -0
- package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
- package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
- package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
- package/skills/frontend-design/SKILL.md +77 -0
- package/skills/gdpr-gate/NOTICE +136 -0
- package/skills/gdpr-gate/SKILL.md +341 -0
- package/skills/gdpr-gate/references/fields.md +160 -0
- package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
- package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
- package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
- package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
- package/skills/gdpr-gate/references/layers/frontend.md +249 -0
- package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
- package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
- package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
- package/skills/gdpr-gate/references/legal-consent.md +140 -0
- package/skills/gdpr-gate/references/non-negotiables.md +60 -0
- package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
- package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
- package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
- package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
- package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
- package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
- package/skills/gemini-imagegen/SKILL.md +283 -0
- package/skills/gemini-imagegen/requirements.txt +5 -0
- package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
- package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
- package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
- package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
- package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
- package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
- package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
- package/skills/git-worktree/SKILL.md +534 -0
- package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
- package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
- package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
- package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
- package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
- package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
- package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
- package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
- package/skills/go/SKILL.md +54 -0
- package/skills/growth/SKILL.md +164 -0
- package/skills/harvest-debt/SKILL.md +78 -0
- package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
- package/skills/heal-skill/SKILL.md +173 -0
- package/skills/help/SKILL.md +46 -0
- package/skills/incident/NOTICE +39 -0
- package/skills/incident/SKILL.md +395 -0
- package/skills/incident/scripts/dry-run.sh +448 -0
- package/skills/incident/scripts/redact-engine.py +412 -0
- package/skills/incident/scripts/redact-sentinel.sh +44 -0
- package/skills/incident/templates/pir.md +139 -0
- package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
- package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
- package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
- package/skills/incident/test/fixtures/positive-corpus.md +87 -0
- package/skills/incident/test/redact-sentinel.test.sh +1420 -0
- package/skills/invoice/SKILL.md +206 -0
- package/skills/kb-glossary/SKILL.md +71 -0
- package/skills/kb-glossary/references/glossary-format.md +111 -0
- package/skills/kb-glossary/references/rejected-request-register.md +115 -0
- package/skills/kb-search/SKILL.md +276 -0
- package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
- package/skills/legal-audit/SKILL.md +161 -0
- package/skills/legal-generate/NOTICE +242 -0
- package/skills/legal-generate/SKILL.md +244 -0
- package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
- package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
- package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
- package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
- package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
- package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
- package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
- package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
- package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
- package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
- package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
- package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
- package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
- package/skills/linear-fetch/SKILL.md +247 -0
- package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
- package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
- package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
- package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
- package/skills/linear-fetch/test/parity.test.sh +112 -0
- package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
- package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
- package/skills/merge-pr/SKILL.md +668 -0
- package/skills/model-launch-review/SKILL.md +255 -0
- package/skills/model-launch-review/scripts/audit-models.sh +501 -0
- package/skills/one-shot/SKILL.md +316 -0
- package/skills/one-shot/references/token-discipline.md +74 -0
- package/skills/operator-bootstrap/SKILL.md +412 -0
- package/skills/operator-bootstrap/template.sh +439 -0
- package/skills/operator-digest/SKILL.md +287 -0
- package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
- package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
- package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
- package/skills/operator-rephrase/SKILL.md +98 -0
- package/skills/pencil-setup/SKILL.md +201 -0
- package/skills/pencil-setup/scripts/check_deps.sh +556 -0
- package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
- package/skills/pencil-setup/scripts/package-lock.json +1172 -0
- package/skills/pencil-setup/scripts/package.json +14 -0
- package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
- package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
- package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
- package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
- package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
- package/skills/plan/SKILL.md +1140 -0
- package/skills/plan/references/differential-migration-testing.md +185 -0
- package/skills/plan/references/plan-community-discovery.md +42 -0
- package/skills/plan/references/plan-founder-check.md +137 -0
- package/skills/plan/references/plan-functional-overlap.md +25 -0
- package/skills/plan/references/plan-host-fanout.md +51 -0
- package/skills/plan/references/plan-issue-templates.md +779 -0
- package/skills/plan/references/plan-network-outage-checklist.md +131 -0
- package/skills/plan/references/plan-scope-check.md +126 -0
- package/skills/plan/references/plan-sharp-edges.md +325 -0
- package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
- package/skills/plan/references/webapp-cwv-observability.md +143 -0
- package/skills/plan-review/SKILL.md +91 -0
- package/skills/plan-review/lib/named-panel.mjs +58 -0
- package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
- package/skills/postmerge/SKILL.md +642 -0
- package/skills/postmerge/references/deploy-status-debugging.md +138 -0
- package/skills/postmerge/references/graceful-degradation.md +20 -0
- package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
- package/skills/preflight/SKILL.md +1553 -0
- package/skills/preflight/references/check-13-founder-check.md +286 -0
- package/skills/preflight/scripts/founder-check.py +1640 -0
- package/skills/preflight/scripts/parse-form-a.awk +107 -0
- package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
- package/skills/product-roadmap/SKILL.md +357 -0
- package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
- package/skills/provision-cloudflare/SKILL.md +67 -0
- package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
- package/skills/provision-doppler/SKILL.md +58 -0
- package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
- package/skills/provision-github/SKILL.md +59 -0
- package/skills/provision-github/scripts/provision-github.sh +300 -0
- package/skills/provision-hetzner/SKILL.md +76 -0
- package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
- package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
- package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
- package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
- package/skills/qa/SKILL.md +263 -0
- package/skills/qa/references/browser-preflight.md +22 -0
- package/skills/questionnaire-generate/SKILL.md +328 -0
- package/skills/questionnaire-generate/references/questionnaire.template +60 -0
- package/skills/rclone/SKILL.md +162 -0
- package/skills/rclone/scripts/check_setup.sh +63 -0
- package/skills/release-announce/SKILL.md +65 -0
- package/skills/release-docs/SKILL.md +92 -0
- package/skills/reproduce-bug/SKILL.md +358 -0
- package/skills/resolve-debt/SKILL.md +85 -0
- package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
- package/skills/resolve-parallel/SKILL.md +45 -0
- package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
- package/skills/resolve-pr-parallel/SKILL.md +68 -0
- package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
- package/skills/resolve-todo-parallel/SKILL.md +57 -0
- package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
- package/skills/review/SKILL.md +1621 -0
- package/skills/review/references/anti-slop-scanner-hook.md +40 -0
- package/skills/review/references/defect-classes-migrations.md +9 -0
- package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
- package/skills/review/references/review-e2e-testing.md +116 -0
- package/skills/review/references/review-todo-structure.md +165 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +95 -0
- package/skills/review/references/semgrep-custom-rules.yaml +174 -0
- package/skills/review/references/ultrathink-phases.md +98 -0
- package/skills/review/references/wfs.md +6 -0
- package/skills/review/scripts/emit-review-trailer.sh +499 -0
- package/skills/review/scripts/ensure-semgrep.sh +99 -0
- package/skills/review/scripts/fix-round-seats.sh +197 -0
- package/skills/review/test/emit-review-trailer.test.sh +387 -0
- package/skills/review/test/ensure-semgrep.test.sh +111 -0
- package/skills/review/test/fix-round-seats.test.sh +237 -0
- package/skills/review/workflows/README.md +147 -0
- package/skills/review/workflows/review.workflow.js +667 -0
- package/skills/schedule/SKILL.md +796 -0
- package/skills/seo-aeo/SKILL.md +139 -0
- package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
- package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
- package/skills/ship/SKILL.md +3022 -0
- package/skills/ship/references/adr-ordinal-collision.md +47 -0
- package/skills/ship/references/affected-gate-contract.md +115 -0
- package/skills/ship/references/ci-run-diagnosis.md +29 -0
- package/skills/ship/references/ci-workflow-authoring.md +68 -0
- package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
- package/skills/ship/references/cto-ruling-armed-push.md +5 -0
- package/skills/ship/references/decision-challenges-render.md +71 -0
- package/skills/ship/references/detached-battery-verdict.md +13 -0
- package/skills/ship/references/feature-tweet-draft.md +42 -0
- package/skills/ship/references/followthrough-issue-body-template.md +47 -0
- package/skills/ship/references/followthrough-stub-template.sh +92 -0
- package/skills/ship/references/merge-queue-dequeue.md +39 -0
- package/skills/ship/references/operator-holds.md +149 -0
- package/skills/ship/references/pipeline-tally-render.md +43 -0
- package/skills/ship/references/pr-overlap.md +153 -0
- package/skills/ship/references/queue-mode.md +7 -0
- package/skills/ship/references/ready-run-wait.md +36 -0
- package/skills/ship/references/red-on-main-quarantine.md +110 -0
- package/skills/ship/references/register-update-pr-pattern.md +88 -0
- package/skills/ship/references/settle-then-admin-merge.md +130 -0
- package/skills/ship/references/supabase-column-probe.md +13 -0
- package/skills/ship/scripts/auto-close-scan.sh +76 -0
- package/skills/ship/scripts/battery-owed.sh +403 -0
- package/skills/ship/scripts/hold-gate-eval.sh +410 -0
- package/skills/ship/scripts/net-issue-flow.sh +665 -0
- package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
- package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
- package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
- package/skills/skill-creator/SKILL.md +299 -0
- package/skills/skill-creator/references/api-security.md +248 -0
- package/skills/skill-creator/references/authoring-levers.md +110 -0
- package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
- package/skills/skill-creator/references/best-practices.md +431 -0
- package/skills/skill-creator/references/common-patterns.md +639 -0
- package/skills/skill-creator/references/core-principles.md +193 -0
- package/skills/skill-creator/references/executable-code.md +188 -0
- package/skills/skill-creator/references/iteration-and-testing.md +509 -0
- package/skills/skill-creator/references/official-spec.md +193 -0
- package/skills/skill-creator/references/recommended-structure.md +180 -0
- package/skills/skill-creator/references/skill-structure.md +396 -0
- package/skills/skill-creator/references/using-scripts.md +122 -0
- package/skills/skill-creator/references/using-templates.md +120 -0
- package/skills/skill-creator/references/workflows-and-validation.md +541 -0
- package/skills/skill-creator/scripts/init_skill.py +322 -0
- package/skills/skill-creator/scripts/package_skill.py +110 -0
- package/skills/skill-creator/scripts/quick_validate.py +64 -0
- package/skills/skill-creator/templates/router-skill.md +73 -0
- package/skills/skill-creator/templates/simple-skill.md +34 -0
- package/skills/skill-creator/workflows/add-reference.md +96 -0
- package/skills/skill-creator/workflows/add-script.md +104 -0
- package/skills/skill-creator/workflows/add-template.md +83 -0
- package/skills/skill-creator/workflows/add-workflow.md +128 -0
- package/skills/skill-creator/workflows/audit-skill.md +159 -0
- package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
- package/skills/skill-creator/workflows/create-new-skill.md +207 -0
- package/skills/skill-creator/workflows/get-guidance.md +134 -0
- package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
- package/skills/skill-creator/workflows/verify-skill.md +226 -0
- package/skills/skill-security-scan/SKILL.md +145 -0
- package/skills/skill-security-scan/references/disclaimer.md +21 -0
- package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
- package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
- package/skills/skill-security-scan/references/override-mechanism.md +171 -0
- package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
- package/skills/skill-security-scan/references/regex-patterns.md +136 -0
- package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
- package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
- package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
- package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
- package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
- package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
- package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
- package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
- package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
- package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
- package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
- package/skills/skill-security-scan/scripts/lib.sh +197 -0
- package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
- package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
- package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
- package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
- package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
- package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
- package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
- package/skills/social-distribute/SKILL.md +485 -0
- package/skills/spec-templates/SKILL.md +274 -0
- package/skills/sync/SKILL.md +49 -0
- package/skills/test-browser/SKILL.md +464 -0
- package/skills/test-browser/references/feature-map.md +161 -0
- package/skills/test-fix-loop/SKILL.md +159 -0
- package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
- package/skills/triage/SKILL.md +402 -0
- package/skills/trigger-cron/SKILL.md +136 -0
- package/skills/trigger-cron/scripts/trigger.sh +198 -0
- package/skills/user-set-role/SKILL.md +147 -0
- package/skills/user-set-role/scripts/set-role.sh +218 -0
- package/skills/user-story-writer/SKILL.md +77 -0
- package/skills/ux-audit/SKILL.md +251 -0
- package/skills/ux-audit/references/finding.schema.json +62 -0
- package/skills/ux-audit/references/route-list.yaml +95 -0
- package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
- package/skills/ux-audit/scripts/bot-signin.ts +135 -0
- package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
- package/skills/work/SKILL.md +1479 -0
- package/skills/work/references/end-of-work-resume.md +7 -0
- package/skills/work/references/hook-test-main-cwd.md +24 -0
- package/skills/work/references/key-principles.md +83 -0
- package/skills/work/references/property-testing.md +122 -0
- package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
- package/skills/work/references/work-agent-teams.md +97 -0
- package/skills/work/references/work-lifecycle-parallel.md +147 -0
- package/skills/work/references/work-lockfile-bumps.md +97 -0
- package/skills/work/references/work-scratch-sandboxes.md +212 -0
- package/skills/work/references/work-subagent-fanout.md +61 -0
- package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
- 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
|