@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,818 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Soleur's OpenCode plugin — the OpenCode peer of `hooks/hooks.json`.
|
|
4
|
+
*
|
|
5
|
+
* LOAD SHAPE (measured — measurement §6.6 arm A). OpenCode 2.0.32 requires a **default**
|
|
6
|
+
* export carrying an `id` and a `setup` function. The V1 named-export form
|
|
7
|
+
* (`export const Soleur = async (ctx) => ({ hooks })`) is rejected outright:
|
|
8
|
+
* `Plugin must export a default definition with an id and an effect or setup function.`
|
|
9
|
+
* A plain object importing only `node:` builtins is enough to load (arms B/C), so this
|
|
10
|
+
* module deliberately imports nothing from `@opencode/plugin` and carries no runtime
|
|
11
|
+
* dependency. That is also why the pin task resolves to "nothing to pin": there is no vendor
|
|
12
|
+
* package on the module's import graph (measurement §6.6, "Package identity").
|
|
13
|
+
*
|
|
14
|
+
* WHY THIS FILE IS NOT GENERATED. It holds dispatch logic that has to be unit-tested by
|
|
15
|
+
* import, and a generator-owned template string cannot be imported. The generator therefore
|
|
16
|
+
* treats it as a **source file** (`ADAPTER_SOURCE_FILES` in `scripts/sync-opencode-adapter.ts`):
|
|
17
|
+
* it asserts the file exists, never emits it, and never deletes it. The installer copies it
|
|
18
|
+
* into `<configdir>/plugins/` with the `__SOLEUR_PLUGIN_ROOT__` token resolved.
|
|
19
|
+
*
|
|
20
|
+
* WHERE THE GUARDS BIND, AND WHY NOT SOMEWHERE ELSE (measurement §12). The Bash guards bind at
|
|
21
|
+
* `ctx.permission.hook("evaluate")`, whose event carries the tool as `action` and the command
|
|
22
|
+
* as `resources[0]`, with a **mutable** `effect: allow | ask | deny`. Measured live:
|
|
23
|
+
*
|
|
24
|
+
* - `effect = "deny"` + `message` ⇒ the tool ends `{status:"error", error:{type:
|
|
25
|
+
* "permission.rejected", message}}`, it never runs, and the model reads the message.
|
|
26
|
+
* - `effect = "ask"` + `message` ⇒ the tool stays `running` awaiting a decision. A real
|
|
27
|
+
* prompt, so a guard's `ask` is delivered as an `ask` and NOT downgraded to a refusal.
|
|
28
|
+
*
|
|
29
|
+
* `ctx.tool.hook("execute.before")` is bound **as a capture, never as a guard** (#10819). It fires
|
|
30
|
+
* *before* the permission hook, and its only refusal channel is a thrown error, so a `deny`-or-`ask`
|
|
31
|
+
* there would collapse into a throw and preempt the real ask behind it. What it *can* do without
|
|
32
|
+
* deciding anything is publish `input.workdir`, the one value the permission event does not carry:
|
|
33
|
+
* a `shell` call with `workdir=$HOME` was judged against the session directory, so `rm -rf *` was
|
|
34
|
+
* allowed that the destructive guard **denies** in the directory the command asked for. The
|
|
35
|
+
* permission hook consumes that capture by call key and passes it as the envelope `cwd`.
|
|
36
|
+
*
|
|
37
|
+
* INVARIANTS. (1) One predicate, `decidesBash`, answers "is this a call the Bash guards see" — and
|
|
38
|
+
* the envelope, the capture and the consume all ask it; a second copy of that question is how a
|
|
39
|
+
* wire-name change reopens the hole at one site only. (2) The capture runs for every Bash call,
|
|
40
|
+
* including one with no workdir, so a lookup miss means "the tool hook did not fire" rather than
|
|
41
|
+
* "no workdir". (3) A capture is never deleted on consume, so a second evaluation of the same call
|
|
42
|
+
* cannot fall back to the session directory. (4) A degraded correlation is announced; a captured
|
|
43
|
+
* workdir that cannot be used escalates to `ask` instead of judging somewhere the operator did not
|
|
44
|
+
* ask about.
|
|
45
|
+
*
|
|
46
|
+
* WHAT IT CANNOT DO (measurement §6.6). Every V2 hook surface communicates only by mutating
|
|
47
|
+
* the event object it is handed, and `ctx.event.subscribe` is a one-way iterator with no
|
|
48
|
+
* return channel. No hook can defer a stop, force a continuation, or veto a completion.
|
|
49
|
+
* The completion marker is therefore observed, never enforced.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { spawnSync } from "node:child_process";
|
|
53
|
+
import { appendFileSync, existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
54
|
+
import { join, resolve } from "node:path";
|
|
55
|
+
|
|
56
|
+
/** Replaced in place by `install-opencode-adapter.ts` with the checkout path. */
|
|
57
|
+
const INSTALLED_ROOT = "__SOLEUR_PLUGIN_ROOT__";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Where the guard scripts live.
|
|
61
|
+
*
|
|
62
|
+
* In an installed copy the installer has already replaced `INSTALLED_ROOT` with the checkout
|
|
63
|
+
* path, and this module sits at `<configdir>/plugins/`, so only the token knows where the
|
|
64
|
+
* checkout is. In an unresolved copy — the committed tree, a test run — the module still sits
|
|
65
|
+
* at `<checkout>/opencode/`, so its own parent IS the checkout. The two are told apart by the
|
|
66
|
+
* plugin identity file (`.claude-plugin/plugin.json`), which only a checkout carries — the
|
|
67
|
+
* repo's established root-resolution marker (devin/INSTRUCTIONS.md §Detection). (A module that
|
|
68
|
+
* landed at `<configdir>/plugins/` still carrying the literal token must not resolve to a
|
|
69
|
+
* *relative* token: a planted `__SOLEUR_PLUGIN_ROOT__/hooks/` directory inside the session's own
|
|
70
|
+
* working directory would then supply both guard scripts. It is anchored at `/` so that path
|
|
71
|
+
* cannot exist, and `decideBash` turns the failed spawn into a deny naming the dead path.) The
|
|
72
|
+
* env override exists so a test or a local run can point at a checkout explicitly.
|
|
73
|
+
*/
|
|
74
|
+
function resolvePluginRoot(): string {
|
|
75
|
+
const override = process.env["SOLEUR_OPENCODE_PLUGIN_ROOT"];
|
|
76
|
+
if (override !== undefined && override !== "") return override;
|
|
77
|
+
const sibling = resolve(import.meta.dir, "..");
|
|
78
|
+
return existsSync(join(sibling, ".claude-plugin", "plugin.json")) ? sibling : join("/", INSTALLED_ROOT);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const PLUGIN_ROOT = resolvePluginRoot();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* OpenCode tool wire name → the Claude Code tool name the guard scripts decide on.
|
|
85
|
+
*
|
|
86
|
+
* `shell` is the measured wire name of the shell tool on 2.0.32 (§12.1): a live
|
|
87
|
+
* `tool.execute.before` carried `tool="shell"` and a live `permission.evaluate` carried
|
|
88
|
+
* `action="shell"`. `bash` is kept as well because the permission-key table in the V1 docs
|
|
89
|
+
* names the key `bash`; if the wire name ever tracks that table, the mapping still holds.
|
|
90
|
+
* An unmapped name passes through unchanged, and the guards decline a name that is not `Bash`.
|
|
91
|
+
*/
|
|
92
|
+
const CLAUDE_TOOL_NAME: Readonly<Record<string, string>> = {
|
|
93
|
+
shell: "Bash",
|
|
94
|
+
bash: "Bash",
|
|
95
|
+
edit: "Edit",
|
|
96
|
+
write: "Write",
|
|
97
|
+
read: "Read",
|
|
98
|
+
grep: "Grep",
|
|
99
|
+
glob: "Glob",
|
|
100
|
+
apply_patch: "Write",
|
|
101
|
+
skill: "Skill",
|
|
102
|
+
webfetch: "WebFetch",
|
|
103
|
+
websearch: "WebSearch",
|
|
104
|
+
question: "AskUserQuestion",
|
|
105
|
+
task: "Task",
|
|
106
|
+
// The measured V2 spawn tool (§12.8): the same logical agent, named `subagent` on the wire.
|
|
107
|
+
subagent: "Task",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Guards that decide a shell call, in `hooks.json` registration order. The first guard to
|
|
112
|
+
* return a decision ends the walk, matching the Claude registrations, where an earlier hook's
|
|
113
|
+
* `deny` is already final.
|
|
114
|
+
*/
|
|
115
|
+
export const BASH_GUARDS: readonly { id: string; script: string }[] = [
|
|
116
|
+
{ id: "browser-snapshot-credential-guard", script: "hooks/browser-snapshot-credential-guard.sh" },
|
|
117
|
+
{ id: "destructive-command-guard", script: "hooks/destructive-command-guard.sh" },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/** Wall-clock bound for one guard spawn. `hooks.json` registers the same guards at 10 s. */
|
|
121
|
+
const GUARD_TIMEOUT_MS = 12_000;
|
|
122
|
+
|
|
123
|
+
export interface ScriptRun {
|
|
124
|
+
status: number | null;
|
|
125
|
+
stdout: string;
|
|
126
|
+
stderr: string;
|
|
127
|
+
/**
|
|
128
|
+
* The spawn error's code, when the process could not be started at all (`ENOENT`, `EACCES`,
|
|
129
|
+
* `ENOTDIR`). `spawnSync` reports these as `status: null` with the reason only in `error`, so
|
|
130
|
+
* discarding it makes an unenterable working directory indistinguishable from a moved checkout
|
|
131
|
+
* and sends the operator to re-run an installer that will answer `ok`.
|
|
132
|
+
*/
|
|
133
|
+
error?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type ScriptRunner = (script: string, stdin: string, cwd: string) => ScriptRun;
|
|
137
|
+
|
|
138
|
+
/** Run a hook script the way the harness does: JSON envelope in on stdin, JSON out on stdout. */
|
|
139
|
+
export function runScript(script: string, stdin: string, cwd: string): ScriptRun {
|
|
140
|
+
const result = spawnSync("bash", [script], {
|
|
141
|
+
input: stdin,
|
|
142
|
+
cwd,
|
|
143
|
+
encoding: "utf8",
|
|
144
|
+
timeout: GUARD_TIMEOUT_MS,
|
|
145
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
146
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
status: result.status,
|
|
150
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
151
|
+
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
152
|
+
...(result.error === undefined ? {} : { error: result.error.code ?? result.error.message }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Last parseable JSON object in a stream — a script may print a diagnostic line first. */
|
|
157
|
+
function lastJsonObject(text: string): Record<string, unknown> | undefined {
|
|
158
|
+
const whole = text.trim();
|
|
159
|
+
if (whole !== "") {
|
|
160
|
+
try {
|
|
161
|
+
const parsed: unknown = JSON.parse(whole);
|
|
162
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
163
|
+
return parsed as Record<string, unknown>;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// fall through to the line scan
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const lines = text.split("\n");
|
|
170
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
171
|
+
const line = lines[i].trim();
|
|
172
|
+
if (!line.startsWith("{")) continue;
|
|
173
|
+
try {
|
|
174
|
+
const parsed: unknown = JSON.parse(line);
|
|
175
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
176
|
+
return parsed as Record<string, unknown>;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// keep scanning
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function objectField(source: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
|
|
186
|
+
const value = source?.[key];
|
|
187
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
188
|
+
? (value as Record<string, unknown>)
|
|
189
|
+
: undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function claudeToolName(tool: string): string {
|
|
193
|
+
return CLAUDE_TOOL_NAME[tool] ?? tool;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Does a call with this wire name reach the Bash guards?
|
|
198
|
+
*
|
|
199
|
+
* One answer for three askers — the envelope constructor, the `execute.before` capture and the
|
|
200
|
+
* `permission.evaluate` consume. Two copies of this question is the rot that would let the
|
|
201
|
+
* measured wire name (`shell`) or its V1 spelling (`bash`) be handled at one site only, which is
|
|
202
|
+
* silently the pre-fix behaviour: capture with no consume, or consume with no capture.
|
|
203
|
+
*/
|
|
204
|
+
export function decidesBash(tool: string): boolean {
|
|
205
|
+
return claudeToolName(tool) === "Bash";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The Claude PreToolUse envelope the guards parse. Returns `undefined` for any tool the guards
|
|
210
|
+
* cannot decide, so no process is spawned for a call no guard would look at. `tool_input` is
|
|
211
|
+
* passed through whole rather than pre-extracted: the destructive guard's documented behavior
|
|
212
|
+
* on an absent or non-string `command` is to **ask**, and pre-extracting here would silently
|
|
213
|
+
* turn that ask into an allow.
|
|
214
|
+
*/
|
|
215
|
+
export function guardEnvelope(tool: string, input: unknown, cwd: string): string | undefined {
|
|
216
|
+
if (!decidesBash(tool)) return undefined;
|
|
217
|
+
const name = claudeToolName(tool);
|
|
218
|
+
const toolInput = input !== null && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
219
|
+
return JSON.stringify({ hook_event_name: "PreToolUse", tool_name: name, tool_input: toolInput, cwd });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* What a `tool.execute.before` event said about the directory its command will start in.
|
|
224
|
+
*
|
|
225
|
+
* `absent` and `unusable` are deliberately different: the first is the normal case (no `workdir`
|
|
226
|
+
* was supplied, so the session directory is correct), the second means the operator asked for a
|
|
227
|
+
* directory this plugin cannot vouch for — and judging in the session directory instead is exactly
|
|
228
|
+
* the hole #10819 reports, one order of magnitude rarer.
|
|
229
|
+
*/
|
|
230
|
+
export type WorkdirCapture =
|
|
231
|
+
| { readonly kind: "absent" }
|
|
232
|
+
| { readonly kind: "usable"; readonly path: string }
|
|
233
|
+
| { readonly kind: "unusable"; readonly raw: string };
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Resolve the `workdir` a tool call carries. Total by construction: it never throws and never
|
|
237
|
+
* returns a path it has not confirmed is a directory.
|
|
238
|
+
*
|
|
239
|
+
* A relative value is resolved against `baseDir` (the session directory) — measured, not assumed:
|
|
240
|
+
* with the session moved to a worktree while the OpenCode process kept its launch cwd in the bare
|
|
241
|
+
* checkout, `workdir: "plugins/soleur"` ran in the worktree. `realpathSync` then yields the physical
|
|
242
|
+
* directory the shell's `chdir` reaches, so `path.resolve`'s lexical answer for a symlinked or
|
|
243
|
+
* `..`-containing value cannot disagree with where the command actually runs.
|
|
244
|
+
*/
|
|
245
|
+
export function resolveWorkdir(input: unknown, baseDir: string): WorkdirCapture {
|
|
246
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) return { kind: "absent" };
|
|
247
|
+
const raw = (input as Record<string, unknown>)["workdir"];
|
|
248
|
+
if (raw === undefined || raw === null) return { kind: "absent" };
|
|
249
|
+
// A present but non-string value is a *shape* violation, not "no workdir was given": treating it
|
|
250
|
+
// as absent would judge the call in the session directory without saying so, which is #10819's
|
|
251
|
+
// silence one shape over. `null`/absent stay the normal case because a runtime may send either.
|
|
252
|
+
if (typeof raw !== "string") return { kind: "unusable", raw: clip(String(raw)) };
|
|
253
|
+
if (raw === "") return { kind: "absent" };
|
|
254
|
+
try {
|
|
255
|
+
// `resolve` already returns an absolute argument unchanged, and `statSync` throws for a missing
|
|
256
|
+
// path, so neither `isAbsolute` nor `existsSync` earns a branch here.
|
|
257
|
+
const absolute = resolve(baseDir, raw);
|
|
258
|
+
if (!statSync(absolute).isDirectory()) return { kind: "unusable", raw: clip(raw) };
|
|
259
|
+
return { kind: "usable", path: realpathSync(absolute) };
|
|
260
|
+
} catch {
|
|
261
|
+
return { kind: "unusable", raw: clip(raw) };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Clip wire-controlled text to a length an operator can read, after stripping what would forge a
|
|
267
|
+
* record: `field()` removes the control characters, and the bound removes a megabyte of it.
|
|
268
|
+
*/
|
|
269
|
+
export function clip(value: string, max = 160): string {
|
|
270
|
+
const flat = value.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, "_");
|
|
271
|
+
return flat.length > max ? `${flat.slice(0, max)}…` : flat;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface GuardDecision {
|
|
275
|
+
guard: string;
|
|
276
|
+
decision: "deny" | "ask";
|
|
277
|
+
reason: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Read `hookSpecificOutput.permissionDecision` out of a guard's stdout. No decision → undefined. */
|
|
281
|
+
export function parseGuardOutput(stdout: string): { decision: "deny" | "ask"; reason: string } | undefined {
|
|
282
|
+
const specific = objectField(lastJsonObject(stdout), "hookSpecificOutput");
|
|
283
|
+
const decision = specific?.["permissionDecision"];
|
|
284
|
+
if (decision !== "deny" && decision !== "ask") return undefined;
|
|
285
|
+
const reason = specific?.["permissionDecisionReason"];
|
|
286
|
+
return { decision, reason: typeof reason === "string" ? reason : "" };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The guard id a failed guard *run* is reported under — never a guard that examined and declined. */
|
|
290
|
+
export const GUARD_RUN_FAILURE = "guard-did-not-run";
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Run the Bash guards in order for a tool call; the first decision wins. `undefined` means
|
|
294
|
+
* every guard **ran and declined**, and the call proceeds unchanged.
|
|
295
|
+
*
|
|
296
|
+
* A run that did not produce a decision is not the same event. Both guard scripts print a
|
|
297
|
+
* decision or nothing and exit 0 in every path, so a non-zero status (missing or moved checkout,
|
|
298
|
+
* an unresolved `__SOLEUR_PLUGIN_ROOT__` → 127, a spawn error, the 12 s timeout → `null`) means
|
|
299
|
+
* the guard never spoke. That is a **deny**, with the dead path in the reason: the alternative is
|
|
300
|
+
* a silent fail-open of the credential and destructive-command guards — a support failure dressed
|
|
301
|
+
* as a feature, and the exact class this adapter's spec names as its load-bearing risk. Measured:
|
|
302
|
+
* `decideBash("shell", {command:"rm -rf /"}, …)` returned `undefined` for a `{status:127}` runner
|
|
303
|
+
* before this branch (10-seat review, 2026-10-19).
|
|
304
|
+
*/
|
|
305
|
+
export function decideBash(
|
|
306
|
+
tool: string,
|
|
307
|
+
input: unknown,
|
|
308
|
+
cwd: string,
|
|
309
|
+
runner: ScriptRunner = runScript,
|
|
310
|
+
pluginRoot: string = PLUGIN_ROOT,
|
|
311
|
+
): GuardDecision | undefined {
|
|
312
|
+
const envelope = guardEnvelope(tool, input, cwd);
|
|
313
|
+
if (envelope === undefined) return undefined;
|
|
314
|
+
for (const guard of BASH_GUARDS) {
|
|
315
|
+
const script = join(pluginRoot, guard.script);
|
|
316
|
+
const run = runner(script, envelope, cwd);
|
|
317
|
+
const parsed = parseGuardOutput(run.stdout);
|
|
318
|
+
if (parsed !== undefined) return { guard: guard.id, ...parsed };
|
|
319
|
+
if (run.status !== 0) {
|
|
320
|
+
// A directory that cannot be entered and a guard that could not start share `status: null`, so
|
|
321
|
+
// the error code decides which sentence the operator reads. Only a *directory* fault gets the
|
|
322
|
+
// directory sentence: `ETIMEDOUT`/`ENOBUFS` are the guard's own limits, and `ENOENT` with an
|
|
323
|
+
// existing cwd is a missing interpreter or script — telling that operator to create a
|
|
324
|
+
// directory would send them to the wrong fix.
|
|
325
|
+
if (run.error !== undefined && (run.error === "EACCES" || run.error === "ENOTDIR" || !existsSync(cwd))) {
|
|
326
|
+
return {
|
|
327
|
+
guard: GUARD_RUN_FAILURE,
|
|
328
|
+
decision: "deny",
|
|
329
|
+
reason: `the guard could not be started in \`${clip(cwd)}\` (${run.error}): ${script}. The working directory this call asked for is not enterable, so the command was NOT judged and NOT run. Create that directory and re-issue, or drop the \`workdir\` to be judged in the session directory.`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const how =
|
|
333
|
+
run.status === null
|
|
334
|
+
? run.error === undefined
|
|
335
|
+
? "timed out or could not be spawned"
|
|
336
|
+
: `could not be spawned (${run.error})`
|
|
337
|
+
: `exited ${run.status}`;
|
|
338
|
+
return {
|
|
339
|
+
guard: GUARD_RUN_FAILURE,
|
|
340
|
+
decision: "deny",
|
|
341
|
+
reason: `the guard did not run (${how}): ${script}. The checkout the adapter points at may have moved or been removed — re-run \`bun "${INSTALLED_ROOT}/scripts/install-opencode-adapter.ts" --check\` from the plugin checkout. The command was NOT run, because an unrunnable guard cannot vouch for it.`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The command a `permission.evaluate` event describes.
|
|
350
|
+
*
|
|
351
|
+
* Measured shape (§12.2): a shell call arrives as `action: "shell"`,
|
|
352
|
+
* `resources: ["echo hello-from-probe"]` — one string that IS the command. Joining rather than
|
|
353
|
+
* indexing is deliberate: it cannot lose text if a shell call ever arrives with more than one
|
|
354
|
+
* resource, and it is identical to indexing on the measured shape.
|
|
355
|
+
*/
|
|
356
|
+
export function permissionCommand(resources: readonly unknown[]): string {
|
|
357
|
+
return resources.filter((resource): resource is string => typeof resource === "string").join("\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Decide a `permission.evaluate` event. Non-shell actions cost nothing: the tool-name gate
|
|
362
|
+
* inside `guardEnvelope` returns before any process is spawned.
|
|
363
|
+
*/
|
|
364
|
+
export function decidePermission(
|
|
365
|
+
action: unknown,
|
|
366
|
+
resources: readonly unknown[],
|
|
367
|
+
cwd: string,
|
|
368
|
+
runner: ScriptRunner = runScript,
|
|
369
|
+
pluginRoot: string = PLUGIN_ROOT,
|
|
370
|
+
): GuardDecision | undefined {
|
|
371
|
+
if (typeof action !== "string") return undefined;
|
|
372
|
+
return decideBash(action, { command: permissionCommand(resources) }, cwd, runner, pluginRoot);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The message the decision carries. The guard's own reason is already the operator-facing text
|
|
377
|
+
* (`This command was NOT run. …` / `Stop and tell the person …`), so it is passed through whole
|
|
378
|
+
* and only prefixed with which guard spoke — never rewritten, because the reason contains the
|
|
379
|
+
* escalation instructions a person needs.
|
|
380
|
+
*/
|
|
381
|
+
export function decisionMessage(decision: GuardDecision): string {
|
|
382
|
+
const head = `Soleur's ${decision.guard}:`;
|
|
383
|
+
return decision.reason === "" ? head : `${head} ${decision.reason}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** ADR-094: the corpus's frontmatter is stripped before injection. */
|
|
387
|
+
export function stripFrontmatter(text: string): string {
|
|
388
|
+
return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** A regular file that is not a symlink — the rules loader's prompt-injection defense, kept. */
|
|
392
|
+
function isRealFile(path: string): boolean {
|
|
393
|
+
try {
|
|
394
|
+
return lstatSync(path).isFile();
|
|
395
|
+
} catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function additionalContext(stdout: string): string | undefined {
|
|
401
|
+
const value = objectField(lastJsonObject(stdout), "hookSpecificOutput")?.["additionalContext"];
|
|
402
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The rule corpus for a session, reuse-first.
|
|
407
|
+
*
|
|
408
|
+
* Where the repository carries the canonical loader (`.claude/hooks/session-rules-loader.sh` —
|
|
409
|
+
* ADR-151's frontmatter strip, over-strip guard, symlink rejection, manifest and stamps), it is
|
|
410
|
+
* invoked with a SessionStart envelope and its `additionalContext` is used verbatim. Otherwise
|
|
411
|
+
* the corpus is read directly, keeping the two properties that must never be missing: a symlink
|
|
412
|
+
* is refused, and frontmatter is stripped. Everything the loader adds on top of that (the
|
|
413
|
+
* manifest, the tmpfs alarm block, the SOC 2 session-context snapshot) is not reproduced, and
|
|
414
|
+
* the disposition ledger says so.
|
|
415
|
+
*/
|
|
416
|
+
export function rulesInjection(cwd: string, runner: ScriptRunner = runScript): string | undefined {
|
|
417
|
+
const loader = join(cwd, ".claude", "hooks", "session-rules-loader.sh");
|
|
418
|
+
if (isRealFile(loader)) {
|
|
419
|
+
const envelope = JSON.stringify({ hook_event_name: "SessionStart", source: "startup", cwd });
|
|
420
|
+
const injected = additionalContext(runner(loader, envelope, cwd).stdout);
|
|
421
|
+
if (injected !== undefined) return injected;
|
|
422
|
+
}
|
|
423
|
+
const corpus = join(cwd, "AGENTS.rules.md");
|
|
424
|
+
if (isRealFile(corpus)) return stripFrontmatter(readFileSync(corpus, "utf8"));
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The post-compaction re-read directive. `compaction-state.sh` cannot be reused: it counts
|
|
430
|
+
* `compact_boundary` records in a Claude Code transcript, which this harness has no equivalent
|
|
431
|
+
* of. The directive is reproduced; the window bookkeeping is not (disposition: `downgraded`).
|
|
432
|
+
*/
|
|
433
|
+
export function compactionDirective(): string {
|
|
434
|
+
return [
|
|
435
|
+
"[soleur] A checkpoint summary has replaced part of this conversation.",
|
|
436
|
+
"Re-read the rule corpus (`AGENTS.rules.md`, or the copy injected at session start) before",
|
|
437
|
+
"continuing: a summary compresses rule bodies, and a rule you cannot quote is a rule you",
|
|
438
|
+
"cannot follow.",
|
|
439
|
+
].join(" ");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Optional observation target. Unset means the completion observer writes nothing. */
|
|
443
|
+
const EVENT_LOG = process.env["SOLEUR_OPENCODE_EVENT_LOG"];
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The default writer. A failed append is announced once and then shrugged off: an unwritable log
|
|
447
|
+
* path (a parent directory that does not exist is the common shape of that mistake) must not make
|
|
448
|
+
* `setup` reject, because a rejected setup leaves **no hook bound at all** — no guards, no capture,
|
|
449
|
+
* nothing — to protect a diagnostic channel.
|
|
450
|
+
*/
|
|
451
|
+
const defaultWriter = (() => {
|
|
452
|
+
let broken = false;
|
|
453
|
+
return (line: string): void => {
|
|
454
|
+
if (EVENT_LOG === undefined) return;
|
|
455
|
+
try {
|
|
456
|
+
appendFileSync(EVENT_LOG, `${line}\n`);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (broken) return;
|
|
459
|
+
broken = true;
|
|
460
|
+
console.error(
|
|
461
|
+
`[soleur] the event log \`${clip(String(EVENT_LOG))}\` is not writable (${clip(String(error))}); continuing without the durable record. The log's parent directory must exist.`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
})();
|
|
466
|
+
|
|
467
|
+
export interface PermissionEvaluateEvent {
|
|
468
|
+
readonly action?: unknown;
|
|
469
|
+
readonly resources?: readonly unknown[];
|
|
470
|
+
readonly agent?: unknown;
|
|
471
|
+
/**
|
|
472
|
+
* Where the decision came from. Measured (§12.2): a `shell` call carries
|
|
473
|
+
* `{type: "tool", messageID, id}` and that `id` equals the `tool.execute.before` event's `id`,
|
|
474
|
+
* which is the only channel through which the per-call `workdir` can reach this event.
|
|
475
|
+
*/
|
|
476
|
+
readonly source?: { readonly type?: unknown; readonly id?: unknown; readonly messageID?: unknown };
|
|
477
|
+
effect: "allow" | "ask" | "deny";
|
|
478
|
+
message?: string;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** A `tool.execute.before` event. Measured keys: `tool, sessionID, agent, messageID, id, input`. */
|
|
482
|
+
export interface ToolBeforeEvent {
|
|
483
|
+
readonly tool?: unknown;
|
|
484
|
+
readonly id?: unknown;
|
|
485
|
+
readonly messageID?: unknown;
|
|
486
|
+
readonly input?: unknown;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export interface SetupContext {
|
|
490
|
+
location?: { directory?: string };
|
|
491
|
+
permission: {
|
|
492
|
+
hook(
|
|
493
|
+
name: "evaluate",
|
|
494
|
+
handler: (event: PermissionEvaluateEvent) => Promise<void> | void,
|
|
495
|
+
): Promise<unknown>;
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
* The capture surface (#10819). Optional because a runtime that does not offer it must still
|
|
499
|
+
* load; absence, and a registration the runtime rejects, are both announced at setup rather than
|
|
500
|
+
* ignored, because every decision then silently reverts to the session directory.
|
|
501
|
+
*/
|
|
502
|
+
tool?: {
|
|
503
|
+
hook(
|
|
504
|
+
name: "execute.before",
|
|
505
|
+
handler: (event: ToolBeforeEvent) => Promise<void> | void,
|
|
506
|
+
): Promise<unknown>;
|
|
507
|
+
};
|
|
508
|
+
session: {
|
|
509
|
+
hook(
|
|
510
|
+
name: string,
|
|
511
|
+
handler: (event: Record<string, unknown>) => Promise<void> | void,
|
|
512
|
+
options?: { providerID?: string },
|
|
513
|
+
): Promise<unknown>;
|
|
514
|
+
};
|
|
515
|
+
event: { subscribe(options?: { signal?: AbortSignal }): AsyncIterable<{ type?: string; data?: unknown }> };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Session-scoped injection state: inject once, then again after a compaction. */
|
|
519
|
+
interface SessionState {
|
|
520
|
+
injected: boolean;
|
|
521
|
+
compacted: boolean;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Cap on remembered sessions. A long-lived `opencode serve` sees one session per conversation and
|
|
526
|
+
* no session-end event was measured to hook (§6.6), so the map is bounded here and the oldest
|
|
527
|
+
* entry is evicted first — `Map` preserves insertion order, so this is a two-line LRU.
|
|
528
|
+
*/
|
|
529
|
+
const MAX_SESSIONS = 500;
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Cap on captured tool calls. One entry per Bash call, keyed by `(messageID, id)`; a long-lived
|
|
533
|
+
* `opencode serve` never tells the plugin a call is finished, so the map is bounded here.
|
|
534
|
+
*
|
|
535
|
+
* Eviction is **silent**, unlike `remember`'s. A capture that is lost is announced where it could
|
|
536
|
+
* change a verdict — at the decision, as `workdir-uncorrelated cause=no-capture` — so an eviction
|
|
537
|
+
* notice would be a second report of the same fact, emitted for calls whose evaluation never
|
|
538
|
+
* arrived at all (cancelled, auto-allowed by the ruleset). Loudness belongs at the decision.
|
|
539
|
+
*/
|
|
540
|
+
const MAX_CALLS = 500;
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The key a call is captured and consumed under.
|
|
544
|
+
*
|
|
545
|
+
* `id` alone was the obvious choice and the wrong one: the measured id (`call_00_s3uv…`) reads as a
|
|
546
|
+
* per-message counter, and one plugin instance is shared by every session an `opencode serve`
|
|
547
|
+
* process hosts. `messageID` is measured on both events, so the pair makes a colliding suffix
|
|
548
|
+
* harmless. Both ids are **required**: defaulting a missing one to `""` would collapse the key back
|
|
549
|
+
* to the bare counter and hand one call's workdir to another, so an absent id is uncorrelated
|
|
550
|
+
* (loud, session directory) rather than correlated on half a key.
|
|
551
|
+
*/
|
|
552
|
+
function callKey(messageID: string, id: string): string {
|
|
553
|
+
return `${messageID}\u0000${id}`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Build `setup`. The hooks are returned bound to injected collaborators so the unit tests can
|
|
558
|
+
* drive them with a fake context and a fake script runner.
|
|
559
|
+
*/
|
|
560
|
+
export function createSetup(
|
|
561
|
+
runner: ScriptRunner = runScript,
|
|
562
|
+
pluginRoot: string = PLUGIN_ROOT,
|
|
563
|
+
write: (line: string) => void = defaultWriter,
|
|
564
|
+
report: (line: string) => void = (line) => console.error(line),
|
|
565
|
+
): (ctx: SetupContext) => Promise<void> {
|
|
566
|
+
const sessions = new Map<string, SessionState>();
|
|
567
|
+
const calls = new Map<string, WorkdirCapture>();
|
|
568
|
+
|
|
569
|
+
/** Wire-controlled text before it goes into a TSV line: no C0/C1, no line/paragraph separators. */
|
|
570
|
+
const field = (value: unknown): string => String(value).replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, "_");
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Announce a degraded correlation on both channels. The log file is off by default
|
|
574
|
+
* (`SOLEUR_OPENCODE_EVENT_LOG` unset), so a log-only claim of loudness is false in the default
|
|
575
|
+
* configuration; stderr visibility in the TUI is not measured, so neither channel alone is
|
|
576
|
+
* enough. The sentence is for the operator, the TSV token for the record.
|
|
577
|
+
*/
|
|
578
|
+
const announce = (token: string, detail: string, sentence: string): void => {
|
|
579
|
+
report(`[soleur] ${sentence}`);
|
|
580
|
+
// `detail` is fielded here as well as at the call sites: this is the single line assembler, so a
|
|
581
|
+
// future caller that passes a raw path cannot break the column count.
|
|
582
|
+
write(`soleur-opencode\t${token}\t${field(detail)}`);
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
/** Record a capture, keeping the map bounded. Write-once keys: no recency re-ordering. */
|
|
586
|
+
const recordCall = (key: string, capture: WorkdirCapture): void => {
|
|
587
|
+
calls.set(key, capture);
|
|
588
|
+
while (calls.size > MAX_CALLS) {
|
|
589
|
+
const oldest = calls.keys().next();
|
|
590
|
+
if (oldest.done === true) break;
|
|
591
|
+
calls.delete(oldest.value);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
/** Remember a session's state, keeping the map bounded and the most recent entry last. */
|
|
596
|
+
const remember = (id: string, state: SessionState): void => {
|
|
597
|
+
sessions.delete(id);
|
|
598
|
+
sessions.set(id, state);
|
|
599
|
+
while (sessions.size > MAX_SESSIONS) {
|
|
600
|
+
const oldest = sessions.keys().next();
|
|
601
|
+
if (oldest.done === true) break;
|
|
602
|
+
sessions.delete(oldest.value);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
return async function setup(ctx: SetupContext): Promise<void> {
|
|
607
|
+
const sessionCwd = ctx.location?.directory ?? process.cwd();
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* The one directory a `shell` decision is judged against, or the escalation to make instead.
|
|
611
|
+
*
|
|
612
|
+
* `ask` is returned only when a workdir **was** captured and cannot be used. Falling back to the
|
|
613
|
+
* session directory there is defect #10819 applied to the one case where the operator
|
|
614
|
+
* explicitly asked for a different directory — and `effect = "ask"` is a measured channel
|
|
615
|
+
* (§12.2), so the plugin can say "I cannot check this" instead of guessing.
|
|
616
|
+
*/
|
|
617
|
+
const decisionCwd = (
|
|
618
|
+
event: PermissionEvaluateEvent,
|
|
619
|
+
): { kind: "decide"; cwd: string } | { kind: "unusable"; raw: string; message: string } => {
|
|
620
|
+
const source = event.source;
|
|
621
|
+
const id = source?.id;
|
|
622
|
+
const idText = typeof id === "string" && id !== "" ? field(id) : "absent";
|
|
623
|
+
// `source.type` is checked as well as the ids: a rule or config source whose id happened to
|
|
624
|
+
// collide with a call id would otherwise apply a stale workdir and log success. A missing
|
|
625
|
+
// message id is uncorrelatable rather than defaulted — with `""` on both sides a per-message
|
|
626
|
+
// counter would collide across concurrent messages and hand call A's workdir to call B.
|
|
627
|
+
const cause =
|
|
628
|
+
source?.type !== "tool"
|
|
629
|
+
? "source-not-tool"
|
|
630
|
+
: typeof id !== "string" || id === ""
|
|
631
|
+
? "id-absent"
|
|
632
|
+
: typeof source.messageID !== "string"
|
|
633
|
+
? "message-id-absent"
|
|
634
|
+
: undefined;
|
|
635
|
+
if (cause !== undefined) {
|
|
636
|
+
announce(
|
|
637
|
+
"workdir-uncorrelated",
|
|
638
|
+
`id=${idText}\tcause=${cause}`,
|
|
639
|
+
`a shell call's permission event does not carry the ids the capture is keyed by (${cause}), so any per-call workdir cannot be correlated — this command is judged against the session directory instead. If this repeats, the runtime changed the event shape; check the OpenCode version.`,
|
|
640
|
+
);
|
|
641
|
+
return { kind: "decide", cwd: sessionCwd };
|
|
642
|
+
}
|
|
643
|
+
const capture = calls.get(callKey(source.messageID as string, id as string));
|
|
644
|
+
if (capture === undefined) {
|
|
645
|
+
announce(
|
|
646
|
+
"workdir-uncorrelated",
|
|
647
|
+
`id=${idText}\tcause=no-capture`,
|
|
648
|
+
`the tool hook recorded nothing for shell call ${idText}, so any workdir it carried is unknown — this command is judged against the session directory instead. The tool hook did not fire; check the OpenCode version, then re-run \`install-opencode-adapter.ts --check\`.`,
|
|
649
|
+
);
|
|
650
|
+
return { kind: "decide", cwd: sessionCwd };
|
|
651
|
+
}
|
|
652
|
+
if (capture.kind === "absent") return { kind: "decide", cwd: sessionCwd };
|
|
653
|
+
if (capture.kind === "usable") {
|
|
654
|
+
write(`soleur-opencode\tworkdir-correlated\tcwd=${field(capture.path)}`);
|
|
655
|
+
return { kind: "decide", cwd: capture.path };
|
|
656
|
+
}
|
|
657
|
+
announce(
|
|
658
|
+
"workdir-unusable",
|
|
659
|
+
`cwd=${field(capture.raw)}`,
|
|
660
|
+
`this shell call asked to run in \`${capture.raw}\`, which is not a directory this plugin can use. The guards were run against the session directory instead and declined, so the command is not checked against the directory it asked for.`,
|
|
661
|
+
);
|
|
662
|
+
return {
|
|
663
|
+
kind: "unusable",
|
|
664
|
+
raw: capture.raw,
|
|
665
|
+
message: `Soleur: this shell call asked for \`${capture.raw}\`, which is not a usable directory, so it could not be checked against the directory it asked for — the guards ran against the session directory and declined. Re-issue it without \`workdir\` to be checked here, or create that directory first.`,
|
|
666
|
+
};
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/** Deliver a guard decision onto the event, never relaxing an arriving `deny` into an `ask`. */
|
|
670
|
+
const applyDecision = (event: PermissionEvaluateEvent, decision: GuardDecision | undefined): void => {
|
|
671
|
+
if (decision === undefined) return;
|
|
672
|
+
// Only `allow` arrivals were measured (§12.2); relaxing an unmeasured deny is the
|
|
673
|
+
// silent-enforcement-loss direction.
|
|
674
|
+
if (event.effect === "deny" && decision.decision === "ask") return;
|
|
675
|
+
event.effect = decision.decision;
|
|
676
|
+
event.message = decisionMessage(decision);
|
|
677
|
+
// A guard that could not run is an operator-visible fault, so it is announced even when the
|
|
678
|
+
// observation channel was never armed.
|
|
679
|
+
if (decision.guard === GUARD_RUN_FAILURE) {
|
|
680
|
+
report(`[soleur] ${decision.reason}`);
|
|
681
|
+
write(`soleur-opencode\tguard-run-failure\teffect=deny\t${field(decision.reason)}`);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
await ctx.permission.hook("evaluate", (event) => {
|
|
686
|
+
const action = event.action;
|
|
687
|
+
// ONE predicate for both sides (#10819). A non-Bash action returns before the map is touched:
|
|
688
|
+
// the measured `external_directory` event fires FIRST with the same call id, and consuming it
|
|
689
|
+
// there would lose the workdir before the decision that needs it.
|
|
690
|
+
if (typeof action !== "string" || !decidesBash(action)) return;
|
|
691
|
+
const target = decisionCwd(event);
|
|
692
|
+
if (target.kind === "decide") {
|
|
693
|
+
applyDecision(event, decidePermission(action, event.resources ?? [], target.cwd, runner, pluginRoot));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// A captured workdir that cannot be used does NOT skip the guards: their cwd-independent
|
|
697
|
+
// classes (a home wipe, an infra destroy, a credential-bearing snapshot, a forced push to the
|
|
698
|
+
// default branch) are the ones that must survive, and running them costs one prompt at most.
|
|
699
|
+
// Only when they decline does the unresolvable directory become the decision — and an
|
|
700
|
+
// arriving `deny` is still never relaxed into that prompt.
|
|
701
|
+
applyDecision(event, decidePermission(action, event.resources ?? [], sessionCwd, runner, pluginRoot));
|
|
702
|
+
if (event.effect !== "allow") return;
|
|
703
|
+
event.effect = "ask";
|
|
704
|
+
event.message = target.message;
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Capture-only (#10819): publish `input.workdir` for the decision above. Bound **after** the
|
|
709
|
+
* decision surface, so a runtime that rejects this registration cannot leave the guards
|
|
710
|
+
* unbound; wrapped, because optional chaining covers an absent `tool` and not a rejecting
|
|
711
|
+
* `hook()`. Never decides, never throws — a throw here would preempt the real ask behind it.
|
|
712
|
+
*/
|
|
713
|
+
const capture = (event: ToolBeforeEvent): void => {
|
|
714
|
+
try {
|
|
715
|
+
const tool = event.tool;
|
|
716
|
+
if (typeof tool !== "string" || !decidesBash(tool)) return;
|
|
717
|
+
const id = event.id;
|
|
718
|
+
const hasId = typeof id === "string" && id !== "";
|
|
719
|
+
if (!hasId || typeof event.messageID !== "string") {
|
|
720
|
+
announce(
|
|
721
|
+
"workdir-uncapturable",
|
|
722
|
+
`id=${hasId ? field(id) : "absent"}\treason=${hasId ? "message-id-absent" : "id-absent"}`,
|
|
723
|
+
"a shell call reached the tool hook without the ids its permission event is keyed by, so its workdir cannot be published — the decision that follows is judged against the session directory. Check the OpenCode version.",
|
|
724
|
+
);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
recordCall(callKey(event.messageID, id), resolveWorkdir(event.input, sessionCwd));
|
|
728
|
+
} catch (error) {
|
|
729
|
+
// Containment for the "never throw" promise: with total helpers this is unreachable, and it
|
|
730
|
+
// must stay that way rather than becoming the one path that preempts the real ask.
|
|
731
|
+
announce(
|
|
732
|
+
"workdir-capture-error",
|
|
733
|
+
`reason=${field(String(error))}`,
|
|
734
|
+
`the workdir capture failed (${clip(String(error))}); this shell call is judged against the session directory and the guards still run.`,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const noToolSurface = (reason: string): void => {
|
|
740
|
+
announce(
|
|
741
|
+
"no-tool-hook-surface",
|
|
742
|
+
`workdir-unavailable\treason=${field(reason)}`,
|
|
743
|
+
`this runtime offers no usable \`tool.execute.before\` surface (${clip(reason)}), so a shell call's \`workdir\` cannot be captured — every command is judged against the session directory, as before this fix. The permission guards remain bound.`,
|
|
744
|
+
);
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
if (ctx.tool === undefined) {
|
|
748
|
+
noToolSurface("the surface is absent");
|
|
749
|
+
} else {
|
|
750
|
+
try {
|
|
751
|
+
await ctx.tool.hook("execute.before", capture);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
noToolSurface(String(error));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
await ctx.session.hook("context", (event) => {
|
|
758
|
+
const cwd = typeof event["cwd"] === "string" ? event["cwd"] : sessionCwd;
|
|
759
|
+
const rawSessionID = event["sessionID"];
|
|
760
|
+
const sessionID = typeof rawSessionID === "string" ? rawSessionID : undefined;
|
|
761
|
+
// A missing or non-string id gets no shared bucket: one session's `injected` must never
|
|
762
|
+
// silence another's. Such an event injects and is not memoized.
|
|
763
|
+
const state =
|
|
764
|
+
sessionID === undefined ? { injected: false, compacted: false } : (sessions.get(sessionID) ?? { injected: false, compacted: false });
|
|
765
|
+
if (state.injected && !state.compacted) return;
|
|
766
|
+
const system = event["system"];
|
|
767
|
+
if (!Array.isArray(system)) {
|
|
768
|
+
write(`soleur-opencode\tcontext\tno-system-array\tsession=${field(sessionID)}`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// The compaction hook's own `system` reaches only the summarization call, so the re-read
|
|
772
|
+
// directive has to ride the next real request — which is this one (measurement §12.3).
|
|
773
|
+
if (state.compacted) system.push({ type: "text", text: compactionDirective() });
|
|
774
|
+
const rules = rulesInjection(cwd, runner);
|
|
775
|
+
if (rules !== undefined) system.push({ type: "text", text: rules });
|
|
776
|
+
// One line per injection decision: whether the corpus landed must be answerable from the
|
|
777
|
+
// log, so a hook that stops firing — or a corpus that moved — is visible rather than
|
|
778
|
+
// indistinguishable from working.
|
|
779
|
+
const outcome = rules === undefined ? "no-corpus" : `injected:${rules.length}`;
|
|
780
|
+
write(`soleur-opencode\tcontext\t${outcome}\tsession=${field(sessionID)}\tcwd=${field(cwd)}`);
|
|
781
|
+
if (sessionID !== undefined) remember(sessionID, { injected: true, compacted: false });
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
await ctx.session.hook("compaction", (event) => {
|
|
785
|
+
const rawSessionID = event["sessionID"];
|
|
786
|
+
if (typeof rawSessionID !== "string") {
|
|
787
|
+
write(`soleur-opencode\tcompaction\tunkeyed\treread-not-armed`);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const state = sessions.get(rawSessionID) ?? { injected: false, compacted: false };
|
|
791
|
+
remember(rawSessionID, { ...state, compacted: true });
|
|
792
|
+
write(`soleur-opencode\tcompaction\treread-armed\tsession=${field(rawSessionID)}`);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Observation only, and it must stay that way: §6.6 measured no return channel, so an idle
|
|
796
|
+
// signal can be noticed and acted on by nothing. Off unless SOLEUR_OPENCODE_EVENT_LOG names
|
|
797
|
+
// a path.
|
|
798
|
+
void (async () => {
|
|
799
|
+
try {
|
|
800
|
+
for await (const event of ctx.event.subscribe()) {
|
|
801
|
+
const type = typeof event?.type === "string" ? event.type : "unknown";
|
|
802
|
+
if (type.startsWith("session.")) write(`soleur-opencode\t${field(type)}\tadvisory-completion-marker-not-enforceable`);
|
|
803
|
+
}
|
|
804
|
+
} catch (error) {
|
|
805
|
+
write(`soleur-opencode\tevent-stream-error\t${field(String(error))}`);
|
|
806
|
+
}
|
|
807
|
+
})();
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* The module's default export — a static object, because the loader reads `id` and `setup` off
|
|
813
|
+
* the default export directly.
|
|
814
|
+
*/
|
|
815
|
+
export default {
|
|
816
|
+
id: "soleur",
|
|
817
|
+
setup: createSetup(),
|
|
818
|
+
};
|