@soleur/opencode 0.0.0-stage → 3.354.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +660 -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 +29 -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 +361 -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 +171 -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 +818 -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 +411 -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 +509 -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 +115 -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 +165 -0
- package/scripts/sync-opencode-adapter.ts +498 -0
- package/scripts/sync-pr-behind.sh +647 -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 +67 -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 +143 -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 +120 -0
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +97 -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 +670 -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 +331 -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 +141 -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 +1620 -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-evidence-comment.md +36 -0
- package/skills/review/references/review-todo-structure.md +165 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +97 -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 +3023 -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/cleanup-merged-archival.md +39 -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/delegated-act-authoring-form.md +7 -0
- package/skills/ship/references/detached-battery-verdict.md +13 -0
- package/skills/ship/references/epic-close-sweep.md +62 -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 +179 -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/verification-scaffold/SKILL.md +73 -0
- package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
- package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
- package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
- package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
- package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
- package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
- package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
- package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
- package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
- package/skills/verification-scaffold/references/guards.tsv +21 -0
- package/skills/verification-scaffold/references/verification-readme.template +55 -0
- package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
- package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
- package/skills/verification-scaffold/test/generator.test.sh +319 -0
- package/skills/verification-scaffold/test/manifest.test.sh +144 -0
- package/skills/work/SKILL.md +1480 -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,843 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tmp-classify.sh — the single "safe to move" classifier for Soleur scratch dirs.
|
|
3
|
+
#
|
|
4
|
+
# WHY
|
|
5
|
+
# ---
|
|
6
|
+
# /tmp and /var/tmp accumulate tens of thousands of orphaned session dirs from
|
|
7
|
+
# Soleur runs (27 GB / ~67k entries measured 2026-09-24). Deleting the wrong
|
|
8
|
+
# entry destroys live session work or authored files, so EVERY consumer —
|
|
9
|
+
# soleur-tmp-purge.sh, tmpfs-guard Reaper 3, and the worktree-manager
|
|
10
|
+
# session-start sweep — sources THIS module rather than re-implementing
|
|
11
|
+
# "safe to move". Three divergent copies of a destructive predicate is the
|
|
12
|
+
# defect class this file exists to prevent.
|
|
13
|
+
#
|
|
14
|
+
# USAGE
|
|
15
|
+
# -----
|
|
16
|
+
# source "<repo>/plugins/soleur/scripts/lib/tmp-classify.sh" # repo-side
|
|
17
|
+
# source "$SCRIPT_DIR/../../../scripts/lib/tmp-classify.sh" # shipped plugin
|
|
18
|
+
#
|
|
19
|
+
# class=$(tc_classify_entry "$dir") # attribution ladder, below
|
|
20
|
+
# tc_entry_is_live "$dir" <pid> # conjunctive liveness
|
|
21
|
+
# tc_quarantine_move "$dir" "$base" <class-subdir>
|
|
22
|
+
#
|
|
23
|
+
# ATTRIBUTION LADDER (first match wins)
|
|
24
|
+
# worktree .git FILE present → owning repo's registry classification —
|
|
25
|
+
# verified attribution, evaluated BEFORE protected/prefix so
|
|
26
|
+
# a signature collision can never steal a registered tree
|
|
27
|
+
# protected never a candidate (systemd-private-*, skill-security-scan-*,
|
|
28
|
+
# bare tmp.*, quarantine artifacts, claude-<uid>, ...)
|
|
29
|
+
# marker <dir>/.soleur-owned valid marker → declared-owned; a marker
|
|
30
|
+
# that exists but fails verification (foreign/absent
|
|
31
|
+
# namespace, owner_root cycle) VETOES the schema rung below —
|
|
32
|
+
# it classifies unattributable, never schema
|
|
33
|
+
# schema soleur-run.<pid>.XXXXXXXX → declared-owned
|
|
34
|
+
# prefix:<name> allowlisted prefix + required subpath signature present —
|
|
35
|
+
# evaluated BEFORE standalone-clone so `.git/config`-signature
|
|
36
|
+
# fixture classes (pirgate-*, deploygap-gate-*) stay reachable
|
|
37
|
+
# standalone-clone .git DIRECTORY present → report-only, never a candidate
|
|
38
|
+
# empty empty dir + long random-looking name → rmdir candidate
|
|
39
|
+
# file:<name> allowlisted regular-file name-shape
|
|
40
|
+
# unattributable report only — never moved
|
|
41
|
+
#
|
|
42
|
+
# LIVENESS (conjunctive — every check must pass for "dead")
|
|
43
|
+
# owner pid absent from procfs AND no process fd/cwd beneath the dir AND no
|
|
44
|
+
# process environ contains the dir path AND the marker/schema's recorded
|
|
45
|
+
# pid-namespace matches the caller's. PPID is never consulted: a dead
|
|
46
|
+
# owner's children reparent to init. environ is exec-time, so it can only
|
|
47
|
+
# ever show the path for processes exec'd AFTER the export — it is a
|
|
48
|
+
# descendant scan, not owner evidence. pid-present-but-foreign (reuse) is
|
|
49
|
+
# retentive: spare on ambiguity.
|
|
50
|
+
#
|
|
51
|
+
# SEAMS (env overrides, for tests and future multi-base callers)
|
|
52
|
+
# TMP_CLASSIFY_PROC procfs root (default /proc)
|
|
53
|
+
# TMP_CLASSIFY_AGE_FLOOR_MIN min tree age, minutes (default 1440)
|
|
54
|
+
# TMP_CLASSIFY_WT_FLOOR_MIN worktree age floor (default 4320)
|
|
55
|
+
# TMP_CLASSIFY_RETAIN_FLOOR_MIN retain-since hard floor (default 10080)
|
|
56
|
+
# TMP_CLASSIFY_RETAIN_DIR retain-since stamp dir
|
|
57
|
+
# TMP_CLASSIFY_UID uid scope (default $(id -u))
|
|
58
|
+
# TMP_CLASSIFY_LEDGER disposition ledger (~/.local/state/soleur/tmp-purge-ledger.log;
|
|
59
|
+
# SOLEUR_PURGE_LEDGER also honored)
|
|
60
|
+
# TMP_CLASSIFY_INUSE_TOKEN_CAP environ-token cap (default 50000; on
|
|
61
|
+
# truncation map-misses fall back to a
|
|
62
|
+
# per-candidate walk — never a false dead)
|
|
63
|
+
# TMP_CLASSIFY_INUSE_TTL_S map staleness bound (default 120; consumers
|
|
64
|
+
# call tc_inuse_map_refresh before actions)
|
|
65
|
+
#
|
|
66
|
+
# PORTABILITY: GNU find -printf and /proc are Linux-isms; on hosts without them
|
|
67
|
+
# the affected rungs fail CLOSED (entry reads as fresh/live → retained).
|
|
68
|
+
|
|
69
|
+
[[ -n "${_SOLEUR_TMP_CLASSIFY_SOURCED:-}" ]] && return 0
|
|
70
|
+
_SOLEUR_TMP_CLASSIFY_SOURCED=1
|
|
71
|
+
|
|
72
|
+
TC_PROC="${TMP_CLASSIFY_PROC:-/proc}"
|
|
73
|
+
TC_AGE_FLOOR_MIN="${TMP_CLASSIFY_AGE_FLOOR_MIN:-1440}"
|
|
74
|
+
TC_WT_FLOOR_MIN="${TMP_CLASSIFY_WT_FLOOR_MIN:-4320}"
|
|
75
|
+
TC_RETAIN_FLOOR_MIN="${TMP_CLASSIFY_RETAIN_FLOOR_MIN:-10080}"
|
|
76
|
+
TC_RETAIN_DIR="${TMP_CLASSIFY_RETAIN_DIR:-${XDG_STATE_HOME:-${HOME:-/nonexistent}/.local/state}/soleur/tmp-retain}"
|
|
77
|
+
TC_UID="${TMP_CLASSIFY_UID:-$(id -u)}"
|
|
78
|
+
TC_LEDGER="${SOLEUR_PURGE_LEDGER:-${TMP_CLASSIFY_LEDGER:-${XDG_STATE_HOME:-${HOME:-/nonexistent}/.local/state}/soleur/tmp-purge-ledger.log}}"
|
|
79
|
+
TC_INUSE_TTL_S="${TMP_CLASSIFY_INUSE_TTL_S:-120}"
|
|
80
|
+
|
|
81
|
+
# tc_uid_of <path> — portable owner uid (GNU stat -c, BSD stat -f).
|
|
82
|
+
tc_uid_of() { stat -c %u -- "$1" 2>/dev/null || stat -f %u -- "$1" 2>/dev/null; }
|
|
83
|
+
|
|
84
|
+
# Ambient git state must never leak into the safety conjuncts: a poisoned
|
|
85
|
+
# GIT_DIR/GIT_CONFIG_COUNT in the caller's environment would make a candidate's
|
|
86
|
+
# clean/merged/upstream verdicts answer for a DIFFERENT repo, and a candidate-
|
|
87
|
+
# controlled gitdir could run core.fsmonitor hooks. Every conjunct-side git
|
|
88
|
+
# call goes through this.
|
|
89
|
+
_tc_git() {
|
|
90
|
+
env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_CONFIG_COUNT \
|
|
91
|
+
-u GIT_CONFIG_NOSYSTEM -u GIT_CONFIG_GLOBAL -u GIT_CONFIG_SYSTEM \
|
|
92
|
+
git -c core.fsmonitor=false "$@"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# --- Protected names ---------------------------------------------------------
|
|
96
|
+
# Evaluated BEFORE every ladder rung: a future allowlist row must never outrank
|
|
97
|
+
# a protection. Quarantine artifacts are excluded from enumeration entirely
|
|
98
|
+
# here as a second layer beneath the caller's own exclusion.
|
|
99
|
+
|
|
100
|
+
# Takes a path OR a basename; ${1##*/} avoids a spawn per candidate — sweep
|
|
101
|
+
# callers enumerate tens of thousands of entries on a shared base.
|
|
102
|
+
tc_is_protected() {
|
|
103
|
+
local name="${1##*/}"
|
|
104
|
+
case "$name" in
|
|
105
|
+
soleur-quarantine|soleur-quarantine.*|*.quarantine-meta) return 0 ;;
|
|
106
|
+
tmp|tmp.*) return 0 ;; # bare mktemp — unattributable, tmpfiles' job
|
|
107
|
+
plan-*|shared-*|vbcr*) return 0 ;;
|
|
108
|
+
skill-security-scan-*) return 0 ;; # #6760 bounded by the scanner's own startup reap; generic sweepers must not out-race the evidence window
|
|
109
|
+
systemd-private-*) return 0 ;;
|
|
110
|
+
playwright*|node-compile-cache|claude-*) return 0 ;;
|
|
111
|
+
.|..) return 0 ;;
|
|
112
|
+
*) return 1 ;;
|
|
113
|
+
esac
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# --- Marker / schema parsing ---------------------------------------------------
|
|
117
|
+
|
|
118
|
+
# tc_marker_owner_pid <dir> [depth] → prints owner pid AND sets TC_PID.
|
|
119
|
+
# rc 0 = verified owner; rc 1 = no marker (caller may try the schema rung);
|
|
120
|
+
# rc 2 = marker PRESENT but unverifiable (wrong uid, foreign/absent/unknown
|
|
121
|
+
# pid-namespace, owner_root cycle) — a VETO: callers must not fall back to
|
|
122
|
+
# the schema rung, because a rejected declaration means the dir's provenance
|
|
123
|
+
# is in doubt and every recorded identity is untrustworthy.
|
|
124
|
+
# Marker contract: regular file, not a symlink, owned by TC_UID, carrying
|
|
125
|
+
# `pid=<top-level-harness-pid>` OR `owner_root=<soleur-run.* dir>`, plus
|
|
126
|
+
# `schema=` and `ns=pid:[<inode>]`. `ns=` must parse AND equal our namespace:
|
|
127
|
+
# `pid:[unknown]` (a writer whose readlink failed) and absent-ns markers are
|
|
128
|
+
# both unverifiable — never silently trusted.
|
|
129
|
+
tc_marker_owner_pid() {
|
|
130
|
+
local dir="$1" depth="${2:-0}" m="$1/.soleur-owned" pid="" oroot="" ns="" myns
|
|
131
|
+
(( depth <= 4 )) || return 2 # owner_root cycle bound — unverifiable
|
|
132
|
+
[[ -f "$m" && ! -L "$m" ]] || return 1
|
|
133
|
+
[[ "$(tc_uid_of "$m")" == "$TC_UID" ]] || return 2
|
|
134
|
+
ns="$(sed -n 's/^ns=\(pid:\[[0-9]*\]\)$/\1/p' "$m" 2>/dev/null | head -1)"
|
|
135
|
+
myns="$(tc_my_pid_ns)"
|
|
136
|
+
if [[ -z "$ns" || "$ns" != "$myns" || "$myns" == "pid:[unknown]" ]]; then
|
|
137
|
+
return 2 # namespace absent, unparseable, foreign, or unverifiable
|
|
138
|
+
fi
|
|
139
|
+
pid="$(sed -n 's/^pid=\([0-9][0-9]*\)$/\1/p' "$m" 2>/dev/null | head -1)"
|
|
140
|
+
if [[ -z "$pid" ]]; then
|
|
141
|
+
oroot="$(sed -n 's/^owner_root=\([^[:space:]]*\)$/\1/p' "$m" 2>/dev/null | head -1)"
|
|
142
|
+
[[ -n "$oroot" ]] || return 2
|
|
143
|
+
if tc_schema_owner_pid "${oroot##*/}" >/dev/null 2>&1; then pid="$TC_PID"; fi
|
|
144
|
+
if [[ -z "$pid" ]] && tc_marker_owner_pid "$oroot" "$((depth + 1))" >/dev/null 2>&1; then pid="$TC_PID"; fi
|
|
145
|
+
[[ -n "$pid" ]] || return 2
|
|
146
|
+
fi
|
|
147
|
+
TC_PID="$pid"
|
|
148
|
+
printf '%s' "$pid"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# tc_schema_owner_pid <basename> → prints pid AND sets TC_PID from
|
|
152
|
+
# soleur-run.<pid>.XXXXXXXX.
|
|
153
|
+
tc_schema_owner_pid() {
|
|
154
|
+
local n="$1"
|
|
155
|
+
[[ "$n" =~ ^soleur-run\.([0-9]+)\.[A-Za-z0-9_-]{8,}$ ]] || return 1
|
|
156
|
+
TC_PID="${BASH_REMATCH[1]}"
|
|
157
|
+
printf '%s' "$TC_PID"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# tc_my_pid_ns → `pid:[<inode>]` of the caller's own pid namespace.
|
|
161
|
+
tc_my_pid_ns() {
|
|
162
|
+
readlink "$TC_PROC/self/ns/pid" 2>/dev/null || printf 'pid:[unknown]'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# --- Liveness ------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
# tc_owner_alive <pid> — /proc presence AND same-uid. A live pid owned by a
|
|
168
|
+
# DIFFERENT uid cannot be our marker's owner — the declaring writer runs as
|
|
169
|
+
# TC_UID — so `pid=1`-style claims (deliberate anti-reap or namespace
|
|
170
|
+
# confusion) do not count as a live owner. Genuine same-uid pid reuse stays
|
|
171
|
+
# retentive through the fd/environ conjuncts below.
|
|
172
|
+
tc_owner_alive() {
|
|
173
|
+
local pid="$1"
|
|
174
|
+
[[ "$pid" =~ ^[0-9]+$ && -d "$TC_PROC/$pid" ]] || return 1
|
|
175
|
+
[[ "$(tc_uid_of "$TC_PROC/$pid")" == "$TC_UID" ]]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# tc_owner_pid_verify <dir> — resolve the declared owner honoring precedence:
|
|
179
|
+
# marker FIRST (it carries the pid-namespace discriminator), schema name only
|
|
180
|
+
# when NO marker exists. rc: 0 = owner pid in TC_PID (TC_DECL_KIND set to
|
|
181
|
+
# marker|schema), 1 = no declaration, 2 = declaration present but
|
|
182
|
+
# unverifiable — consumers must RETAIN rc 2 and never fall through to schema.
|
|
183
|
+
tc_owner_pid_verify() {
|
|
184
|
+
local dir="$1" rc
|
|
185
|
+
TC_DECL_KIND=""
|
|
186
|
+
if [[ -e "$dir/.soleur-owned" || -L "$dir/.soleur-owned" ]]; then
|
|
187
|
+
# `if` context is load-bearing: a plain call's nonzero rc aborts under
|
|
188
|
+
# set -e before it can be inspected.
|
|
189
|
+
if tc_marker_owner_pid "$dir" >/dev/null 2>&1; then rc=0; else rc=$?; fi
|
|
190
|
+
if (( rc == 0 )); then TC_DECL_KIND="marker"; return 0; fi
|
|
191
|
+
(( rc == 2 )) && return 2
|
|
192
|
+
# rc 1 with a marker path existing is impossible here — belt: veto.
|
|
193
|
+
return 2
|
|
194
|
+
fi
|
|
195
|
+
if tc_schema_owner_pid "${dir##*/}" >/dev/null 2>&1; then
|
|
196
|
+
TC_DECL_KIND="schema"; return 0
|
|
197
|
+
fi
|
|
198
|
+
return 1
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# tc_build_inuse_map <base>... — ONE /proc pass marking every top-level entry
|
|
202
|
+
# under the given bases that any live process touches via cwd, an open fd, a
|
|
203
|
+
# memory mapping, a bound unix socket, or an environ token. Callers sweeping
|
|
204
|
+
# many candidates (the session-start sweep) build it once and get O(1)
|
|
205
|
+
# membership from tc_tree_has_live_handles instead of paying a full procfs
|
|
206
|
+
# walk per candidate. map_files and net/unix are included so the coverage is
|
|
207
|
+
# the UNION of every handle class the guard's reapers know (JVM
|
|
208
|
+
# open-mmap-close, Chrome SingletonSocket) — the maps must never diverge on
|
|
209
|
+
# what counts as "live".
|
|
210
|
+
declare -gA TC_INUSE_MAP=()
|
|
211
|
+
declare -ga _TC_MAP_BASES=()
|
|
212
|
+
|
|
213
|
+
# Internal marker — dynamic-scoped over the caller's `_tc_bases` array.
|
|
214
|
+
_tc_mark_inuse() {
|
|
215
|
+
local t="$1" rest top bb
|
|
216
|
+
for bb in "${_tc_bases[@]}"; do
|
|
217
|
+
case "$t" in
|
|
218
|
+
"$bb"/*)
|
|
219
|
+
rest="${t#"$bb"/}"
|
|
220
|
+
top="${rest%%/*}"
|
|
221
|
+
[[ -n "$top" ]] && TC_INUSE_MAP["$bb/$top"]=1
|
|
222
|
+
;;
|
|
223
|
+
esac
|
|
224
|
+
done
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
tc_build_inuse_map() {
|
|
228
|
+
TC_INUSE_MAP=()
|
|
229
|
+
_TC_MAP_BASES=("$@")
|
|
230
|
+
[[ -d "$TC_PROC" ]] || return 1 # sentinel stays unset → per-candidate fallback
|
|
231
|
+
local -a _tc_bases=("$@")
|
|
232
|
+
local target tok
|
|
233
|
+
|
|
234
|
+
# cwd + fd + map_files targets in ONE find: `-printf '%l'` hands back link
|
|
235
|
+
# targets with no per-entry readlink spawn — ~30k syscalls collapse to a
|
|
236
|
+
# single walk. map_files is load-bearing: open()+mmap(MAP_SHARED)+close()
|
|
237
|
+
# leaves no fd to find — the JVM hsperfdata class — and its path appears
|
|
238
|
+
# only here. Pseudo links (`socket:[…]`, `anon_inode:…`) are not absolute
|
|
239
|
+
# and never match a base.
|
|
240
|
+
while IFS= read -r target; do
|
|
241
|
+
[[ "$target" == /* ]] && _tc_mark_inuse "$target"
|
|
242
|
+
done < <(find "$TC_PROC"/[0-9]*/fd "$TC_PROC"/[0-9]*/cwd \
|
|
243
|
+
"$TC_PROC"/[0-9]*/map_files -type l -printf '%l\n' 2>/dev/null)
|
|
244
|
+
|
|
245
|
+
# Bound unix-domain sockets — a socket fd readlinks to `socket:[inode]`,
|
|
246
|
+
# never its filesystem path, so a dir held only by a listener is invisible
|
|
247
|
+
# to the walk above. /proc/net/unix is the only place the path appears; one
|
|
248
|
+
# file read, same 7-column strip the guard's map uses.
|
|
249
|
+
if [[ -r "$TC_PROC/net/unix" ]]; then
|
|
250
|
+
while IFS= read -r target; do
|
|
251
|
+
[[ -n "$target" ]] && _tc_mark_inuse "$target"
|
|
252
|
+
done < <(
|
|
253
|
+
awk '{ line = $0
|
|
254
|
+
if (sub(/^([^ ]+[ ]+){7}/, "", line) && substr(line, 1, 1) == "/") print line }' \
|
|
255
|
+
"$TC_PROC/net/unix" 2>/dev/null || true
|
|
256
|
+
)
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
# environ values are NUL-separated VAR=val with colon-joined path lists; a
|
|
260
|
+
# descendant of a dead owner can still carry TMPDIR=<root>. One concatenated
|
|
261
|
+
# pass rather than a grep per pid (a per-pid spawn measured ~10s for a
|
|
262
|
+
# single candidate on a busy host). The cap bounds pathological environ
|
|
263
|
+
# volume — on truncation a marker is set and map MISSES fall back to the
|
|
264
|
+
# per-candidate walk, so a capped map can never prove a false "dead".
|
|
265
|
+
local cap="${TMP_CLASSIFY_INUSE_TOKEN_CAP:-50000}"
|
|
266
|
+
while IFS= read -r tok; do
|
|
267
|
+
if [[ "$tok" == "/__TC_TRUNC__" ]]; then TC_INUSE_MAP["__truncated__"]=1; continue; fi
|
|
268
|
+
[[ -n "$tok" ]] && _tc_mark_inuse "$tok"
|
|
269
|
+
done < <(cat "$TC_PROC"/[0-9]*/environ 2>/dev/null \
|
|
270
|
+
| tr '\0:' '\n' \
|
|
271
|
+
| grep -oE '/[^[:space:]"'"'"']+' 2>/dev/null \
|
|
272
|
+
| awk -v cap="$cap" -v sent="/__TC_TRUNC__" \
|
|
273
|
+
'NR <= cap { print; next } { print sent; exit }')
|
|
274
|
+
|
|
275
|
+
TC_INUSE_MAP["__built_ts__"]="${EPOCHSECONDS:-$(date +%s)}"
|
|
276
|
+
TC_INUSE_MAP["__built__"]=1 # set LAST — presence proves the build ran
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# tc_inuse_map_refresh [ttl_s] — rebuild TC_INUSE_MAP when older than the
|
|
280
|
+
# staleness bound. The map is a snapshot; a multi-minute apply must not treat
|
|
281
|
+
# scan-start evidence as action-time truth (a dir can acquire a live handle
|
|
282
|
+
# mid-run). Cheap when fresh: one assoc read.
|
|
283
|
+
tc_inuse_map_refresh() {
|
|
284
|
+
local ttl="${1:-$TC_INUSE_TTL_S}" now
|
|
285
|
+
[[ -n "${TC_INUSE_MAP[__built__]:-}" ]] || return 0
|
|
286
|
+
now="${EPOCHSECONDS:-$(date +%s)}"
|
|
287
|
+
if (( now - ${TC_INUSE_MAP[__built_ts__]:-0} > ttl )); then
|
|
288
|
+
tc_build_inuse_map "${_TC_MAP_BASES[@]}" || true
|
|
289
|
+
fi
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# _tc_walk_handles <dir> — the per-candidate procfs walk: cwd, fd, map_files,
|
|
293
|
+
# environ substring, and bound unix-socket paths. Fails CLOSED (returns
|
|
294
|
+
# "live") when procfs is unreadable — a degraded host must never prove "dead".
|
|
295
|
+
_tc_walk_handles() {
|
|
296
|
+
local dir="$1" p fd target
|
|
297
|
+
[[ -d "$TC_PROC" ]] || return 0
|
|
298
|
+
for p in "$TC_PROC"/[0-9]*; do
|
|
299
|
+
[[ -d "$p" ]] || continue
|
|
300
|
+
if target="$(readlink "$p/cwd" 2>/dev/null)" && [[ "$target" == "$dir" || "$target" == "$dir"/* ]]; then
|
|
301
|
+
return 0
|
|
302
|
+
fi
|
|
303
|
+
for fd in "$p"/fd/* "$p"/map_files/*; do
|
|
304
|
+
[[ -e "$fd" || -L "$fd" ]] || continue
|
|
305
|
+
target="$(readlink "$fd" 2>/dev/null)" || continue
|
|
306
|
+
if [[ "$target" == "$dir" || "$target" == "$dir"/* ]]; then return 0; fi
|
|
307
|
+
done
|
|
308
|
+
if grep -aqF -- "$dir" "$p/environ" 2>/dev/null; then return 0; fi
|
|
309
|
+
done
|
|
310
|
+
# Bound unix sockets: fd links never carry the path — /proc/net/unix does.
|
|
311
|
+
if [[ -r "$TC_PROC/net/unix" ]] && grep -aqF -- "$dir" "$TC_PROC/net/unix" 2>/dev/null; then
|
|
312
|
+
return 0
|
|
313
|
+
fi
|
|
314
|
+
return 1
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# tc_tree_has_live_handles <dir> — map-fast-path over _tc_walk_handles.
|
|
318
|
+
# A truncated map can never prove a negative: on miss it walks instead.
|
|
319
|
+
tc_tree_has_live_handles() {
|
|
320
|
+
local dir="$1"
|
|
321
|
+
[[ -d "$TC_PROC" ]] || return 0
|
|
322
|
+
if [[ -n "${TC_INUSE_MAP[__built__]:-}" ]]; then
|
|
323
|
+
local a="$dir"
|
|
324
|
+
while [[ "$a" == */* && "$a" != "/" ]]; do
|
|
325
|
+
[[ -n "${TC_INUSE_MAP[$a]:-}" ]] && return 0
|
|
326
|
+
a="${a%/*}"
|
|
327
|
+
done
|
|
328
|
+
[[ -z "${TC_INUSE_MAP[__truncated__]:-}" ]] && return 1
|
|
329
|
+
# truncated — a map miss is not evidence of death; walk it.
|
|
330
|
+
fi
|
|
331
|
+
_tc_walk_handles "$dir"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# tc_tree_has_live_handles_now <dir> — the walk, ALWAYS, ignoring the map.
|
|
335
|
+
# For action-time re-verification right before a TERMINAL delete: the map can
|
|
336
|
+
# be seconds stale mid-scan and tmpfs deletion has no quarantine to appeal to.
|
|
337
|
+
tc_tree_has_live_handles_now() {
|
|
338
|
+
_tc_walk_handles "$1"
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# tc_entry_is_live <dir> <owner-pid|""> — the full conjunct. rc 0 = live (retain).
|
|
342
|
+
tc_entry_is_live() {
|
|
343
|
+
local dir="$1" pid="${2:-}"
|
|
344
|
+
if [[ -n "$pid" ]] && tc_owner_alive "$pid"; then return 0; fi
|
|
345
|
+
if tc_tree_has_live_handles "$dir"; then return 0; fi
|
|
346
|
+
return 1
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# --- Freshness -------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
# tc_tree_age_min <dir> — minutes since the NEWEST mtime anywhere in the tree.
|
|
352
|
+
# A top-level dir mtime does not advance on nested writes (the _FRESH_TOP
|
|
353
|
+
# defect class). Missing find -printf (BSD find) → prints 0 (fresh →
|
|
354
|
+
# retained): a top-level-stat fallback would miss nested writes and read a
|
|
355
|
+
# fresh tree as stale — the fail direction here is deletion, so the probe
|
|
356
|
+
# fails closed.
|
|
357
|
+
tc_tree_age_min() {
|
|
358
|
+
local dir="$1" newest now
|
|
359
|
+
newest="$(find "$dir" -printf '%T@\n' 2>/dev/null \
|
|
360
|
+
| awk 'NR==1 || $1 > m { m = $1 } END { if (NR) print m }' \
|
|
361
|
+
| cut -d. -f1)"
|
|
362
|
+
# awk max-scan: same IO as sort|head but O(1) memory and no full-buffer sort.
|
|
363
|
+
[[ "$newest" =~ ^[0-9]+$ ]] || { echo 0; return 0; }
|
|
364
|
+
now="$(date +%s)"
|
|
365
|
+
if (( newest <= 0 || newest > now )); then echo 0; else echo $(( (now - newest) / 60 )); fi
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# --- Empty-dir rung ----------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
# tc_is_reapable_empty <dir> — empty AND a random-looking ≥15-char basename
|
|
371
|
+
# AND not a mountpoint (device-id compare against parent doubles as the
|
|
372
|
+
# BSD-portable mountpoint check).
|
|
373
|
+
tc_is_reapable_empty() {
|
|
374
|
+
local dir="$1" name
|
|
375
|
+
name="${dir##*/}"
|
|
376
|
+
[[ "$name" =~ ^[A-Za-z0-9_.-]{15,}$ ]] || return 1
|
|
377
|
+
[[ -d "$dir" ]] || return 1
|
|
378
|
+
# Emptiness: a caller sweeping a whole base may precompute TC_NONEMPTY
|
|
379
|
+
# (one `find -mindepth 2` pass marking every dir that has children) — the
|
|
380
|
+
# per-candidate `ls` spawn is the same cost class as the basename spawn this
|
|
381
|
+
# function just dropped. Falls back to the per-dir check when unset.
|
|
382
|
+
if [[ -n "${TC_NONEMPTY_BUILT:-}" ]]; then
|
|
383
|
+
[[ -n "${TC_NONEMPTY[$dir]:-}" ]] && return 1
|
|
384
|
+
else
|
|
385
|
+
[[ -z "$(ls -A -- "$dir" 2>/dev/null)" ]] || return 1
|
|
386
|
+
fi
|
|
387
|
+
local dev_self dev_parent
|
|
388
|
+
dev_self="$(stat -c %d -- "$dir" 2>/dev/null || stat -f %d -- "$dir" 2>/dev/null || echo x)"
|
|
389
|
+
dev_parent="$(stat -c %d -- "$dir/.." 2>/dev/null || stat -f %d -- "$dir/.." 2>/dev/null || echo y)"
|
|
390
|
+
[[ "$dev_self" != "x" && "$dev_self" == "$dev_parent" ]]
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# --- Git / worktree arm ---------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
# tc_classify_git_dir <dir> → one of:
|
|
396
|
+
# standalone-clone (.git is a real directory — report-only, never candidate)
|
|
397
|
+
# not-git (no .git at all)
|
|
398
|
+
# unverifiable (gitfile/gitdir/porcelain could not be resolved — RETAIN)
|
|
399
|
+
# unregistered (owning repo's registry read OK, path absent — quarantine)
|
|
400
|
+
# registered (registry read OK, path present — further gates apply)
|
|
401
|
+
# Prints the verdict AND sets TC_GITCLASS — hot-path callers invoke it directly
|
|
402
|
+
# (`tc_classify_git_dir "$d" >/dev/null; v="$TC_GITCLASS"`) to skip the fork.
|
|
403
|
+
# _tc_realpath <path> — canonicalize; empty output = unavailable (callers must
|
|
404
|
+
# treat that as UNVERIFIABLE, never fall back to the raw path: a non-
|
|
405
|
+
# canonicalized compare against git's canonical porcelain output misreads a
|
|
406
|
+
# registered worktree as unregistered → quarantine-move → registry damage).
|
|
407
|
+
_tc_realpath() {
|
|
408
|
+
command -v realpath >/dev/null 2>&1 || return 1
|
|
409
|
+
realpath -m -- "$1" 2>/dev/null
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
tc_classify_git_dir() {
|
|
413
|
+
local dir="$1" gf="$1/.git" gitdir main_gitdir
|
|
414
|
+
_gc() { TC_GITCLASS="$1"; printf '%s\n' "$1"; }
|
|
415
|
+
if [[ -d "$gf" && ! -L "$gf" ]]; then _gc "standalone-clone"; return 0; fi
|
|
416
|
+
[[ -f "$gf" && ! -L "$gf" ]] || { _gc "not-git"; return 0; }
|
|
417
|
+
gitdir="$(sed -n 's/^gitdir:[[:space:]]*//p' "$gf" 2>/dev/null | head -1)"
|
|
418
|
+
[[ -n "$gitdir" ]] || { _gc "unverifiable"; return 0; }
|
|
419
|
+
[[ "$gitdir" != /* ]] && gitdir="$dir/$gitdir"
|
|
420
|
+
gitdir="$(_tc_realpath "$gitdir")"
|
|
421
|
+
[[ -n "$gitdir" && -d "$gitdir" ]] || { _gc "unverifiable"; return 0; }
|
|
422
|
+
# gitdir shape is <main>/.git/worktrees/<name> — main repo gitdir is two up.
|
|
423
|
+
main_gitdir="${gitdir%/*}"; main_gitdir="${main_gitdir%/*}"
|
|
424
|
+
[[ -d "$main_gitdir/objects" || -f "$main_gitdir/HEAD" ]] || { _gc "unverifiable"; return 0; }
|
|
425
|
+
local real_dir listing
|
|
426
|
+
real_dir="$(_tc_realpath "$dir")"
|
|
427
|
+
[[ -n "$real_dir" ]] || { _gc "unverifiable"; return 0; }
|
|
428
|
+
if ! listing="$(_tc_git --git-dir="$main_gitdir" worktree list --porcelain 2>/dev/null)"; then
|
|
429
|
+
_gc "unverifiable"; return 0
|
|
430
|
+
fi
|
|
431
|
+
[[ -n "$listing" ]] || { _gc "unverifiable"; return 0; }
|
|
432
|
+
local wt found=0
|
|
433
|
+
while IFS= read -r wt; do
|
|
434
|
+
[[ -n "$wt" ]] || continue
|
|
435
|
+
if [[ "$(_tc_realpath "$wt")" == "$real_dir" ]]; then
|
|
436
|
+
found=1; break
|
|
437
|
+
fi
|
|
438
|
+
done <<< "$(printf '%s\n' "$listing" | sed -n 's/^worktree //p')"
|
|
439
|
+
if (( found == 1 )); then _gc "registered"; else _gc "unregistered"; fi
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# tc_git_main_dir <dir> → prints the owning repo's gitdir (rc 1 if unverifiable).
|
|
443
|
+
tc_git_main_dir() {
|
|
444
|
+
local dir="$1" gf="$1/.git" gitdir
|
|
445
|
+
[[ -f "$gf" && ! -L "$gf" ]] || return 1
|
|
446
|
+
gitdir="$(sed -n 's/^gitdir:[[:space:]]*//p' "$gf" 2>/dev/null | head -1)"
|
|
447
|
+
[[ -n "$gitdir" ]] || return 1
|
|
448
|
+
[[ "$gitdir" != /* ]] && gitdir="$dir/$gitdir"
|
|
449
|
+
gitdir="$(_tc_realpath "$gitdir")"
|
|
450
|
+
[[ -n "$gitdir" && -d "$gitdir" ]] || return 1
|
|
451
|
+
dirname "$(dirname "$gitdir")"
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# tc_worktree_clean <dir> — no modified AND no untracked AND no ignored files.
|
|
455
|
+
# `--ignored` is load-bearing: porcelain omits ignored paths, so a worktree
|
|
456
|
+
# holding only `.env.local` must NOT read clean.
|
|
457
|
+
tc_worktree_clean() {
|
|
458
|
+
local dir="$1" out
|
|
459
|
+
out="$(_tc_git -C "$dir" status --porcelain --ignored 2>/dev/null)" || return 1
|
|
460
|
+
[[ -z "$out" ]]
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
# tc_default_ref <main_gitdir> — the owning repo's default ref AS LAST FETCHED:
|
|
464
|
+
# origin/HEAD → the main worktree's checked-out branch → main/master. No fetch.
|
|
465
|
+
tc_default_ref() {
|
|
466
|
+
local main="$1" sym
|
|
467
|
+
sym="$(_tc_git --git-dir="$main" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null)"
|
|
468
|
+
[[ -n "$sym" ]] && { printf '%s' "$sym"; return 0; }
|
|
469
|
+
sym="$(_tc_git --git-dir="$main" symbolic-ref --quiet --short HEAD 2>/dev/null)"
|
|
470
|
+
[[ -n "$sym" ]] && { printf '%s' "$sym"; return 0; }
|
|
471
|
+
for sym in main master; do
|
|
472
|
+
_tc_git --git-dir="$main" rev-parse --verify --quiet "$sym" >/dev/null 2>&1 && { printf '%s' "$sym"; return 0; }
|
|
473
|
+
done
|
|
474
|
+
return 1
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# tc_worktree_safe_to_remove <dir> — registered + clean + merged + no-unpushed
|
|
478
|
+
# + no live process inside. Caller applies the age floor separately.
|
|
479
|
+
tc_worktree_safe_to_remove() {
|
|
480
|
+
local dir="$1" main head upstream
|
|
481
|
+
main="$(tc_git_main_dir "$dir")" || return 1
|
|
482
|
+
tc_worktree_clean "$dir" || return 1
|
|
483
|
+
head="$(_tc_git -C "$dir" rev-parse HEAD 2>/dev/null)" || return 1
|
|
484
|
+
local defref
|
|
485
|
+
defref="$(tc_default_ref "$main")" || return 1
|
|
486
|
+
_tc_git --git-dir="$main" merge-base --is-ancestor "$head" "$defref" 2>/dev/null || return 1
|
|
487
|
+
upstream="$(_tc_git -C "$dir" rev-parse --abbrev-ref '@{upstream}' 2>/dev/null || true)"
|
|
488
|
+
if [[ -n "$upstream" ]]; then
|
|
489
|
+
local ahead
|
|
490
|
+
ahead="$(_tc_git -C "$dir" rev-list --count "${upstream}..HEAD" 2>/dev/null || echo 999)"
|
|
491
|
+
[[ "$ahead" == "0" ]] || return 1
|
|
492
|
+
fi
|
|
493
|
+
# A worktree with no recorded upstream and a detached-or-merged HEAD is fine;
|
|
494
|
+
# a named branch with no upstream is retained by the caller's risk floor.
|
|
495
|
+
if tc_tree_has_live_handles "$dir"; then return 1; fi
|
|
496
|
+
return 0
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# --- Prefix allowlist (purge-only rung) ------------------------------------------
|
|
500
|
+
# Each row: prefix|required-subpath-glob|writer-citation. The subpath is the
|
|
501
|
+
# content signature that makes prefix matching attributable rather than the
|
|
502
|
+
# rejected name-only heuristic. FROZEN — new producers get markers, not rows.
|
|
503
|
+
# All globs are matched against paths RELATIVE to the candidate dir.
|
|
504
|
+
|
|
505
|
+
TC_DIR_ALLOWLIST=(
|
|
506
|
+
"rung2-archive.*|git-data-bootstrap.sh|tests/scripts/lib/git-data-birth-readiness-gate.sh"
|
|
507
|
+
"gdboot.*|rows|scripts/lib/git-data-boot-signal-poll.sh"
|
|
508
|
+
"infra-suites.*|_*.test.sh.meta|apps/web-platform/infra/run-registered-suites.sh"
|
|
509
|
+
"soleur-inc-*|.claude|.claude/hooks/lib/test-incident-sandbox.sh"
|
|
510
|
+
"mutbat.*|plugins|fixture producers (mutbat)"
|
|
511
|
+
"mutbat2.*|pristine|fixture producers (mutbat2)"
|
|
512
|
+
"pirgate-*|.git/config|plugins/soleur/test/ship-incident-pir-gate.test.ts"
|
|
513
|
+
"kbcov-*|engineering|plugins/soleur/test/kb-coverage.test.ts"
|
|
514
|
+
"gdpr-gate-incidents-*|.claude/.rule-incidents.jsonl|plugins/soleur/test/gdpr-gate.test.ts"
|
|
515
|
+
"deploygap-gate-*|.git/config|deploygap test fixtures"
|
|
516
|
+
"harness-discovery-*|plugin.json|harness discovery fixtures"
|
|
517
|
+
"cron-filing-fixture-*|justified.md|cron filing fixtures"
|
|
518
|
+
"luks-escrow.*|state/escrow.state|luks fixtures"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Regular-file classes — signature is the name shape itself (no content reads).
|
|
522
|
+
TC_FILE_ALLOWLIST=(
|
|
523
|
+
"inngest-ci-*.sh|scripts/inngest fixtures"
|
|
524
|
+
"inngest-arm-*|inngest arm fixtures"
|
|
525
|
+
"pr-*-body.md|pr body scratch files"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# tc_prefix_class <dir> → prints `prefix:<prefix>` AND sets TC_PREFIX_CLASS
|
|
529
|
+
# when the basename matches an allowlist prefix AND the required subpath
|
|
530
|
+
# exists. rc 1 otherwise.
|
|
531
|
+
tc_prefix_class() {
|
|
532
|
+
local dir="$1" name row prefix sig
|
|
533
|
+
name="${dir##*/}"
|
|
534
|
+
for row in "${TC_DIR_ALLOWLIST[@]}"; do
|
|
535
|
+
prefix="${row%%|*}"; sig="${row#*|}"; sig="${sig%%|*}"
|
|
536
|
+
# shellcheck disable=SC2254 # prefix is a deliberate glob from the frozen list
|
|
537
|
+
case "$name" in $prefix)
|
|
538
|
+
compgen -G "$dir/$sig" >/dev/null 2>&1 && { TC_PREFIX_CLASS="prefix:$prefix"; printf '%s\n' "$TC_PREFIX_CLASS"; return 0; }
|
|
539
|
+
;;
|
|
540
|
+
esac
|
|
541
|
+
done
|
|
542
|
+
return 1
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# tc_file_class <file> → prints `file:<shape>` AND sets TC_FILE_CLASS for
|
|
546
|
+
# allowlisted regular files.
|
|
547
|
+
tc_file_class() {
|
|
548
|
+
local f="$1" name row prefix
|
|
549
|
+
[[ -f "$f" && ! -L "$f" ]] || return 1
|
|
550
|
+
[[ "$(tc_uid_of "$f")" == "$TC_UID" ]] || return 1
|
|
551
|
+
name="${f##*/}"
|
|
552
|
+
for row in "${TC_FILE_ALLOWLIST[@]}"; do
|
|
553
|
+
prefix="${row%%|*}"
|
|
554
|
+
# shellcheck disable=SC2254
|
|
555
|
+
case "$name" in $prefix) TC_FILE_CLASS="file:$prefix"; printf 'file:%s\n' "$prefix"; return 0 ;; esac
|
|
556
|
+
done
|
|
557
|
+
return 1
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# --- Top-level classification -----------------------------------------------------
|
|
561
|
+
|
|
562
|
+
# tc_classify_entry <path> → prints the class; never mutates.
|
|
563
|
+
# ALSO sets the TC_CLASS global — a whole-base caller can invoke it as
|
|
564
|
+
# `tc_classify_entry "$p" >/dev/null; cls="$TC_CLASS"` and skip the
|
|
565
|
+
# command-substitution fork (81k entries × ~0.5ms is the difference between a
|
|
566
|
+
# ~20s scan and a ~60s one).
|
|
567
|
+
TC_CLASS=""
|
|
568
|
+
tc_classify_entry() {
|
|
569
|
+
local p="$1" name rc gc
|
|
570
|
+
name="${p##*/}"
|
|
571
|
+
_class() { TC_CLASS="$1"; printf '%s\n' "$1"; }
|
|
572
|
+
# Git-pointer rung FIRST: a `.git` FILE resolves the entry through the
|
|
573
|
+
# OWNING repo's worktree registry — verified attribution that must outrank
|
|
574
|
+
# every name heuristic INCLUDING the prefix allowlist (a signature collision
|
|
575
|
+
# inside a registered worktree — e.g. `soleur-inc-*` containing `.claude/` —
|
|
576
|
+
# must never hijack the tree out of the registry-aware arm). Without this
|
|
577
|
+
# rung the measured backlog class (`tmp.*` dirs bearing `.git`, ~1.5k on the
|
|
578
|
+
# reference host) would be invisible to every consumer. `.git` DIRECTORIES
|
|
579
|
+
# (standalone clones) keep protected-first ordering — report-only either way.
|
|
580
|
+
if [[ -d "$p" && -f "$p/.git" && ! -L "$p/.git" ]]; then
|
|
581
|
+
tc_classify_git_dir "$p" >/dev/null; gc="$TC_GITCLASS"
|
|
582
|
+
case "$gc" in
|
|
583
|
+
registered|unregistered|unverifiable) _class "worktree:$gc"; return 0 ;;
|
|
584
|
+
esac
|
|
585
|
+
fi
|
|
586
|
+
tc_is_protected "$p" && { _class "protected"; return 0; }
|
|
587
|
+
if [[ -f "$p" && ! -d "$p" ]]; then
|
|
588
|
+
if tc_file_class "$p" >/dev/null 2>&1; then _class "$TC_FILE_CLASS"; else _class "unattributable"; fi
|
|
589
|
+
return 0
|
|
590
|
+
fi
|
|
591
|
+
[[ -d "$p" ]] || { _class "unattributable"; return 0; }
|
|
592
|
+
# marker rung — rc 2 (marker present but unverifiable) VETOES the schema
|
|
593
|
+
# rung: a rejected declaration means every recorded identity is untrusted.
|
|
594
|
+
# The `if` wrapper is load-bearing: a plain call returning nonzero aborts
|
|
595
|
+
# under a caller's set -e before the rc can be read.
|
|
596
|
+
if tc_marker_owner_pid "$p" >/dev/null 2>&1; then rc=0; else rc=$?; fi
|
|
597
|
+
case "$rc" in
|
|
598
|
+
0) _class "marker:$TC_PID"; return 0 ;;
|
|
599
|
+
2) _class "unattributable"; return 0 ;;
|
|
600
|
+
esac
|
|
601
|
+
# schema rung — only when NO marker exists (a vetoed marker already returned)
|
|
602
|
+
if tc_schema_owner_pid "$name" >/dev/null 2>&1; then _class "schema:$TC_PID"; return 0; fi
|
|
603
|
+
# prefix rung — BEFORE standalone-clone: `.git/config`-signature fixture
|
|
604
|
+
# classes (pirgate-*, deploygap-gate-*) are attributable by content
|
|
605
|
+
# signature; letting the clone rung preempt them made those rows dead code.
|
|
606
|
+
if tc_prefix_class "$p" >/dev/null 2>&1; then _class "$TC_PREFIX_CLASS"; return 0; fi
|
|
607
|
+
# standalone-clone rung (.git dir; report-only)
|
|
608
|
+
tc_classify_git_dir "$p" >/dev/null; gc="$TC_GITCLASS"
|
|
609
|
+
[[ "$gc" == "standalone-clone" ]] && { _class "standalone-clone"; return 0; }
|
|
610
|
+
# empty rung
|
|
611
|
+
tc_is_reapable_empty "$p" && { _class "empty"; return 0; }
|
|
612
|
+
_class "unattributable"
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
# --- Retain-since stamps ----------------------------------------------------------
|
|
616
|
+
# Bound the retentive arm: an entry retained as unverifiable gets a stamp; once
|
|
617
|
+
# the stamp is older than TC_RETAIN_FLOOR_MIN the caller escalates it to the
|
|
618
|
+
# operator-decision list instead of re-reporting forever.
|
|
619
|
+
|
|
620
|
+
tc_retain_stamp() { # <path> — record/refresh first-seen; prints age in min
|
|
621
|
+
local p="$1" sf now mtime
|
|
622
|
+
sf="$TC_RETAIN_DIR/$(printf '%s' "$p" | sha256sum 2>/dev/null | cut -d' ' -f1 || printf '%s' "$p" | md5sum | cut -d' ' -f1)"
|
|
623
|
+
now="$(date +%s)"
|
|
624
|
+
if [[ ! -f "$sf" ]]; then
|
|
625
|
+
mkdir -p -- "$TC_RETAIN_DIR" 2>/dev/null || { echo 0; return 0; }
|
|
626
|
+
printf '%s\t%s\n' "$now" "$p" > "$sf" 2>/dev/null || { echo 0; return 0; }
|
|
627
|
+
echo 0; return 0
|
|
628
|
+
fi
|
|
629
|
+
mtime="$(stat -c %Y -- "$sf" 2>/dev/null || stat -f %m -- "$sf" 2>/dev/null || echo "$now")"
|
|
630
|
+
echo $(( (now - mtime) / 60 ))
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
# --- Quarantine move ---------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
# tc_tree_has_gitref <dir> — any `.git` file OR dir beneath the tree's top
|
|
636
|
+
# level. A nested worktree or clone makes the whole tree registry-relevant:
|
|
637
|
+
# deleting or quarantining it leaves the owning repo's .git/worktrees/<id>
|
|
638
|
+
# metadata dangling (prunable) and destroys dirty content no conjunct
|
|
639
|
+
# inspected. Reapers must retain such trees — the worktree arm owns them.
|
|
640
|
+
tc_tree_has_gitref() {
|
|
641
|
+
[[ -d "$1" ]] || return 1
|
|
642
|
+
# mindepth 2: the candidate's OWN .git (a worktree pointer file) is at depth
|
|
643
|
+
# 1 — registry-handled by the worktree arm, not a nested ref. Nested refs
|
|
644
|
+
# live at depth >= 2. head -1 short-circuits; find may take SIGPIPE — rc
|
|
645
|
+
# is ignored anyway.
|
|
646
|
+
[[ -n "$(find "$1" -mindepth 2 -name .git -print 2>/dev/null | head -1)" ]]
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
# tc_tree_has_mount <dir> — any entry on a DIFFERENT device than the tree's
|
|
650
|
+
# root. A bind/loop mount inside a candidate is live foreign filesystem
|
|
651
|
+
# content: `find -delete` would empty it, `mv` would relocate the mountpoint
|
|
652
|
+
# into quarantine where the drain later deletes through it. Fail CLOSED: a
|
|
653
|
+
# probe that can't see devices answers "has mount" (retain).
|
|
654
|
+
tc_tree_has_mount() {
|
|
655
|
+
local dir="$1" dev lines
|
|
656
|
+
[[ -d "$dir" ]] || return 1 # a regular file can't contain a mount
|
|
657
|
+
dev="$(stat -c %d -- "$dir" 2>/dev/null || stat -f %d -- "$dir" 2>/dev/null)"
|
|
658
|
+
[[ -n "$dev" ]] || return 0
|
|
659
|
+
# %D is the device number (find's %d is depth — same letter, wrong field).
|
|
660
|
+
lines="$(find "$dir" -mindepth 1 -printf '%D\n' 2>/dev/null)"
|
|
661
|
+
if [[ -z "$lines" ]]; then
|
|
662
|
+
# Empty tree → no mount possible. Non-empty tree with a probe that
|
|
663
|
+
# produced nothing (no -printf) → can't verify → treat as mounted.
|
|
664
|
+
[[ -n "$(ls -A -- "$dir" 2>/dev/null)" ]]
|
|
665
|
+
return
|
|
666
|
+
fi
|
|
667
|
+
grep -qvx -- "$dev" <<<"$lines"
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
# tc_quar_age_min <entry> — minutes since the entry entered quarantine.
|
|
671
|
+
# rename(2) bumps ctime, so %Z records quarantine-ARRIVAL. Aging on content
|
|
672
|
+
# mtime instead (mv preserves it) collapses the recovery window to ~0 for the
|
|
673
|
+
# stale backlog that is quarantine's main population. Missing/unreadable ctime
|
|
674
|
+
# → 0 (fresh → retained; the recovery window errs long, never short).
|
|
675
|
+
tc_quar_age_min() {
|
|
676
|
+
local e="$1" ct now
|
|
677
|
+
ct="$(stat -c %Z -- "$e" 2>/dev/null || stat -f %c -- "$e" 2>/dev/null || echo 0)"
|
|
678
|
+
now="$(date +%s)"
|
|
679
|
+
[[ "$ct" =~ ^[0-9]+$ && "$ct" -gt 0 && "$ct" -le "$now" ]] || { echo 0; return 0; }
|
|
680
|
+
echo $(( (now - ct) / 60 ))
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
# tc_ledger_append <action> <class> <orig> <dest> — ONE ledger for every
|
|
684
|
+
# destructive action across every consumer (purge, Reaper 3, session sweep),
|
|
685
|
+
# so --restore's replay picture is complete. TSV fields: a path containing a
|
|
686
|
+
# tab or newline cannot be recorded faithfully — drop loudly rather than
|
|
687
|
+
# corrupt the file's row structure.
|
|
688
|
+
tc_ledger_append() {
|
|
689
|
+
local line
|
|
690
|
+
case "$1$2$3$4" in
|
|
691
|
+
*$'\t'*|*$'\n'*)
|
|
692
|
+
printf 'LEDGER-DROP: tab/newline in field — action NOT recorded: %s %s\n' "$1" "$3" >&2
|
|
693
|
+
return 0 ;;
|
|
694
|
+
esac
|
|
695
|
+
mkdir -p -- "$(dirname "$TC_LEDGER")" 2>/dev/null || true
|
|
696
|
+
line="$(date -u +%Y-%m-%dT%H:%M:%SZ)"$'\t'"$1"$'\t'"$2"$'\t'"$3"$'\t'"$4"
|
|
697
|
+
if ! printf '%s\n' "$line" >> "$TC_LEDGER" 2>/dev/null; then
|
|
698
|
+
printf 'LEDGER-DROP: cannot append %s — action NOT recorded: %s\n' "$TC_LEDGER" "$line" >&2
|
|
699
|
+
printf 'LEDGER-DROP: %s\n' "$line"
|
|
700
|
+
fi
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
# tc_drain_quarantine <base> <dry> <scratch_ttl_min> <wt_ttl_min> [<deadline_epoch> [<max_entries>]] — the ONLY
|
|
704
|
+
# delete permitted inside soleur-quarantine.<uid>/: entries whose QUARANTINE
|
|
705
|
+
# dwell exceeds the class TTL (subdir name encodes it). A symlinked or
|
|
706
|
+
# foreign-owned quarantine root/class dir refuses outright — a squatter on a
|
|
707
|
+
# world-writable base must not aim the drain at an arbitrary tree. Sets
|
|
708
|
+
# TC_DRAINED and TC_DRAINED_BYTES (both RESET on every call — a caller looping over
|
|
709
|
+
# bases sums them itself).
|
|
710
|
+
#
|
|
711
|
+
# Optional trailing arguments (#9677), both 0 = unbounded so the guard and the purge, which pass
|
|
712
|
+
# four arguments, are unchanged: <deadline_epoch> stops the drain between top-level entries once
|
|
713
|
+
# the clock reaches it, and <max_entries> caps how many entries one call may delete. Both are checked
|
|
714
|
+
# BETWEEN entries only: the mount probe, `du -sk` and `find -delete` of one entry cannot be interrupted,
|
|
715
|
+
# so a single very large entry can overrun the deadline. The enumeration is pre-filtered with
|
|
716
|
+
# `find -cmin +(ttl-1)` (looser than the authoritative tc_quar_age_min re-check below), so unexpired
|
|
717
|
+
# entries cost one find, not a fork each (measured 12 ms/entry otherwise). An entry whose delete is cut
|
|
718
|
+
# short is NOT retried promptly: deleting its children advances its own ctime, so its TTL restarts and
|
|
719
|
+
# the next call sees it as fresh (no data loss — the error direction is retention).
|
|
720
|
+
TC_DRAINED=0
|
|
721
|
+
TC_DRAINED_BYTES=0
|
|
722
|
+
tc_drain_quarantine() {
|
|
723
|
+
local base="$1" dry="${2:-0}" sttl="${3:-10080}" wttl="${4:-43200}" deadline="${5:-0}" cap="${6:-0}"
|
|
724
|
+
local qroot cls ttl e kb attempts=0
|
|
725
|
+
TC_DRAINED=0; TC_DRAINED_BYTES=0
|
|
726
|
+
[[ "$deadline" =~ ^[0-9]+$ ]] || deadline=0
|
|
727
|
+
[[ "$cap" =~ ^[0-9]+$ ]] || cap=0
|
|
728
|
+
# TTLs arrive from env (SOLEUR_PURGE_QUAR_*_TTL_MIN, TMPFS_GUARD_QUAR_*_TTL_MIN) and are used in
|
|
729
|
+
# arithmetic below: `-1`, `abc` (evaluates as an unset variable = 0) and `7d` (arithmetic error)
|
|
730
|
+
# all DRAIN MORE or abort. Only plain digits are valid; anything else falls back to the default
|
|
731
|
+
# (never a shorter window) with a WARN. `10#` pins decimal so `08` is not an octal error.
|
|
732
|
+
[[ "$sttl" =~ ^[0-9]{1,9}$ ]] || { printf 'WARN tc_drain_quarantine: invalid scratch TTL %q — using the default 10080 min\n' "$sttl" >&2; sttl=10080; }
|
|
733
|
+
[[ "$wttl" =~ ^[0-9]{1,9}$ ]] || { printf 'WARN tc_drain_quarantine: invalid worktrees TTL %q — using the default 43200 min\n' "$wttl" >&2; wttl=43200; }
|
|
734
|
+
sttl=$((10#$sttl)); wttl=$((10#$wttl))
|
|
735
|
+
qroot="$base/soleur-quarantine.$TC_UID"
|
|
736
|
+
[[ -d "$qroot" && ! -L "$qroot" ]] || return 0
|
|
737
|
+
[[ "$(tc_uid_of "$qroot")" == "$TC_UID" ]] || return 0
|
|
738
|
+
for cls in "$qroot"/*/; do
|
|
739
|
+
cls="${cls%/}"
|
|
740
|
+
[[ -d "$cls" && ! -L "$cls" ]] || continue
|
|
741
|
+
[[ "$(tc_uid_of "$cls")" == "$TC_UID" ]] || continue
|
|
742
|
+
case "${cls##*/}" in worktrees) ttl="$wttl" ;; *) ttl="$sttl" ;; esac
|
|
743
|
+
while IFS= read -r -d '' e; do
|
|
744
|
+
[[ -e "$e" && ! -L "$e" ]] || continue # planted link — never follow
|
|
745
|
+
if (( deadline > 0 )) && (( "${EPOCHSECONDS:-$(date +%s)}" >= deadline )); then return 0; fi
|
|
746
|
+
if (( cap > 0 )) && (( attempts >= cap )); then return 0; fi
|
|
747
|
+
(( "$(tc_quar_age_min "$e")" >= ttl )) || continue
|
|
748
|
+
tc_tree_has_mount "$e" && continue # never delete into a mount
|
|
749
|
+
if [[ "$dry" == "1" ]]; then
|
|
750
|
+
printf 'would drain %s\n' "$e"; continue
|
|
751
|
+
fi
|
|
752
|
+
attempts=$((attempts + 1))
|
|
753
|
+
# du -sk (portable; -b is GNU-only) BEFORE the delete — the bytes are gone afterwards. An
|
|
754
|
+
# unreadable size counts as 0 rather than aborting the drain.
|
|
755
|
+
# `|| true` is load-bearing: du exits 1 on any unreadable subtree or a file that vanishes mid-walk,
|
|
756
|
+
# and under pipefail that fails the assignment — the guard and the purge call this function bare
|
|
757
|
+
# under `set -e`, so without it one such entry would abort them on every run (#9677 review).
|
|
758
|
+
kb="$(du -skx -- "$e" 2>/dev/null | cut -f1)" || true
|
|
759
|
+
[[ "$kb" =~ ^[0-9]+$ ]] || kb=0
|
|
760
|
+
if find "$e" -xdev -depth -delete 2>/dev/null; then
|
|
761
|
+
tc_ledger_append "drain" "${cls##*/}" "$e" "-"
|
|
762
|
+
TC_DRAINED=$((TC_DRAINED + 1))
|
|
763
|
+
TC_DRAINED_BYTES=$((TC_DRAINED_BYTES + kb * 1024))
|
|
764
|
+
fi
|
|
765
|
+
done < <(find "$cls" -mindepth 1 -maxdepth 1 -cmin "+$(( ttl > 0 ? ttl - 1 : 0 ))" -print0 2>/dev/null)
|
|
766
|
+
done
|
|
767
|
+
return 0
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
# tc_file_in_use <file> — a regular FILE held open: any /proc/*/fd symlink or
|
|
771
|
+
# environ value naming it. For the allowlisted file classes a >24h-old file
|
|
772
|
+
# can still be mid-write (pr-*-body.md consumed by `gh -F`); the top-level map
|
|
773
|
+
# marks directories, so files need their own probe.
|
|
774
|
+
tc_file_in_use() {
|
|
775
|
+
local f="$1"
|
|
776
|
+
[[ -d "$TC_PROC" ]] || return 0 # can't check → assume live (fail closed)
|
|
777
|
+
[[ -n "$(find "$TC_PROC"/[0-9]*/fd -lname "$f" -print -quit 2>/dev/null)" ]] && return 0
|
|
778
|
+
grep -lqsaF -- "$f" "$TC_PROC"/[0-9]*/environ 2>/dev/null && return 0
|
|
779
|
+
return 1
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
# tc_reap_decide <dir> <age_floor_min> — the SHARED "safe to reap" conjunct
|
|
783
|
+
# chain for declared-owner candidates; single-sourced so consumers cannot
|
|
784
|
+
# diverge on the safety gates. Prints a verdict token:
|
|
785
|
+
# reap every conjunct passed — caller may delete/quarantine
|
|
786
|
+
# retain:<why> a conjunct failed or evidence was unverifiable
|
|
787
|
+
# Sets TC_DECL_KIND=marker|schema on success — consumers use it for the
|
|
788
|
+
# disposition split (schema-named roots may direct-delete on tmpfs; marker
|
|
789
|
+
# dirs quarantine on every base — a marker is a self-declared file, weaker
|
|
790
|
+
# attribution than the creation-time schema).
|
|
791
|
+
tc_reap_decide() {
|
|
792
|
+
local dir="$1" floor="$2" rc
|
|
793
|
+
TC_DECL_KIND=""
|
|
794
|
+
# `if` context is load-bearing: a plain call's nonzero rc aborts under
|
|
795
|
+
# set -e before it can be inspected.
|
|
796
|
+
if tc_owner_pid_verify "$dir" >/dev/null 2>&1; then rc=0; else rc=$?; fi
|
|
797
|
+
if (( rc == 2 )); then printf 'retain:unverifiable'; return 0
|
|
798
|
+
elif (( rc != 0 )); then printf 'retain:no-owner'; return 0
|
|
799
|
+
fi
|
|
800
|
+
tc_owner_alive "$TC_PID" && { printf 'retain:owner-live'; return 0; }
|
|
801
|
+
tc_inuse_map_refresh || true
|
|
802
|
+
tc_tree_has_live_handles "$dir" && { printf 'retain:live-handles'; return 0; }
|
|
803
|
+
(( "$(tc_tree_age_min "$dir")" >= floor )) || { printf 'retain:fresh'; return 0; }
|
|
804
|
+
tc_tree_has_gitref "$dir" && { printf 'retain:nested-git'; return 0; }
|
|
805
|
+
tc_tree_has_mount "$dir" && { printf 'retain:nested-mount'; return 0; }
|
|
806
|
+
printf 'reap'
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
# --- Quarantine move ---------------------------------------------------------------
|
|
810
|
+
|
|
811
|
+
# tc_quarantine_move <path> <base> <class-subdir>
|
|
812
|
+
# mv into <base>/soleur-quarantine.<uid>/<class>/ with the pre-move checks:
|
|
813
|
+
# quarantine root not a symlink, owned by TC_UID, same-device (EXDEV refuses —
|
|
814
|
+
# mv, never copy), basename collision → numeric suffix with `mv -n` closing the
|
|
815
|
+
# check-then-act window. Prints the final quarantine path. rc 1 = refused
|
|
816
|
+
# (nothing moved).
|
|
817
|
+
tc_quarantine_move() {
|
|
818
|
+
local src="$1" base="$2" cls="$3"
|
|
819
|
+
local qroot qdir
|
|
820
|
+
qroot="$base/soleur-quarantine.$TC_UID"; qdir="$qroot/$cls"
|
|
821
|
+
local src_dev base_dev
|
|
822
|
+
src_dev="$(stat -c %d -- "$src" 2>/dev/null || stat -f %d -- "$src" 2>/dev/null || echo x)"
|
|
823
|
+
base_dev="$(stat -c %d -- "$base" 2>/dev/null || stat -f %d -- "$base" 2>/dev/null || echo y)"
|
|
824
|
+
[[ "$src_dev" != "x" && "$src_dev" == "$base_dev" ]] || return 1 # EXDEV → refuse
|
|
825
|
+
[[ ! -L "$qroot" && ! -L "$qdir" ]] || return 1
|
|
826
|
+
# A foreign-owned qroot means a squatter pre-created our quarantine dir —
|
|
827
|
+
# moved trees (which can carry .env-local-class secrets) would land inside
|
|
828
|
+
# an attacker-owned root, and restore would replay tampered content.
|
|
829
|
+
if [[ -d "$qroot" ]] && [[ "$(tc_uid_of "$qroot")" != "$TC_UID" ]]; then return 1; fi
|
|
830
|
+
mkdir -p -- "$qdir" 2>/dev/null || return 1
|
|
831
|
+
chmod 0700 "$qroot" "$qdir" 2>/dev/null || return 1
|
|
832
|
+
local dest name i=0
|
|
833
|
+
name="${src##*/}"
|
|
834
|
+
dest="$qdir/$name"
|
|
835
|
+
while [[ -e "$dest" || -L "$dest" ]]; do
|
|
836
|
+
i=$((i + 1)); dest="$qdir/$name.$i"
|
|
837
|
+
done
|
|
838
|
+
# `mv -n` returns success even when it skips (a racer claimed $dest in the
|
|
839
|
+
# check-then-act gap) — verify the move actually happened.
|
|
840
|
+
mv -n -- "$src" "$dest" 2>/dev/null || return 1
|
|
841
|
+
[[ ! -e "$src" && -e "$dest" ]] || return 1
|
|
842
|
+
printf '%s' "$dest"
|
|
843
|
+
}
|