@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,906 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* harness-discovery-smoke — prove Codex, Devin and OpenCode DISCOVER the skill set
|
|
4
|
+
* their manifests declare, by installing the plugin hermetically from this checkout and
|
|
5
|
+
* asking the vendor CLI what it registered (ADR-245; the unfiled item 6 of #8390's
|
|
6
|
+
* bundle).
|
|
7
|
+
*
|
|
8
|
+
* WHAT THIS PROVES, AND WHAT IT DOES NOT.
|
|
9
|
+
* - It proves REGISTRATION: the name is in the harness's skill list. It does not
|
|
10
|
+
* prove INVOCABILITY (ADR-236: `codex exec` ignores `disable-model-invocation`,
|
|
11
|
+
* and `devin skills list` reports `[user]` vs `[user,model]`), and it does not
|
|
12
|
+
* prove WHICH copy was loaded — for `go`/`help`/`sync` the shim body differs
|
|
13
|
+
* from the canonical skill, and a single listing cannot distinguish them.
|
|
14
|
+
* - It proves nothing about UNIQUENESS. A newly added `codex/skills/plan/` is
|
|
15
|
+
* legitimately k=2 and passes here by construction; cross-root collision policy
|
|
16
|
+
* stays with `components.test.ts` (ADR-224 decision 5, ACKED_CROSS_ROOT_DUPES).
|
|
17
|
+
*
|
|
18
|
+
* OPENCODE IS A DIFFERENT SHAPE, and the differences are each forced by a measurement
|
|
19
|
+
* (§11.0): ids are BARE directory basenames with no namespace, so `plan` names both this
|
|
20
|
+
* plugin's skill and the operator's own — the arm keys on the absolute `path`, not the id.
|
|
21
|
+
* The listing also requires a live session: `GET /api/skill` answers an empty array until
|
|
22
|
+
* one exists, so the arm opens one rather than reading a listing that would be a false
|
|
23
|
+
* alarm about a correct install.
|
|
24
|
+
*
|
|
25
|
+
* The pure functions below are exported and unit-tested offline against synthesized
|
|
26
|
+
* fixtures (`plugins/soleur/test/harness-discovery-smoke.test.ts`); the CLI-driving
|
|
27
|
+
* half is the same code under a real binary. NOTE: importing this module binds a
|
|
28
|
+
* per-process scratch session (`ensureScratchSession()` at load, below) — importers
|
|
29
|
+
* of the pure functions get the environment mutation too; it is a no-op nested under
|
|
30
|
+
* an already-bound root.
|
|
31
|
+
*
|
|
32
|
+
* `scripts/codex-plugin-smoke.mjs` is deliberately UNTOUCHED: it is the operator-local
|
|
33
|
+
* check of an already-installed plugin and its hooks, and needs `~/.codex` state this
|
|
34
|
+
* gate refuses to depend on.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { execFileSync, spawn, spawnSync } from "child_process";
|
|
38
|
+
import { randomBytes } from "crypto";
|
|
39
|
+
import {
|
|
40
|
+
closeSync,
|
|
41
|
+
existsSync,
|
|
42
|
+
mkdirSync,
|
|
43
|
+
mkdtempSync,
|
|
44
|
+
openSync,
|
|
45
|
+
readFileSync,
|
|
46
|
+
readdirSync,
|
|
47
|
+
realpathSync,
|
|
48
|
+
rmSync,
|
|
49
|
+
writeFileSync,
|
|
50
|
+
} from "fs";
|
|
51
|
+
import { tmpdir } from "os";
|
|
52
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
53
|
+
import { ensureScratchSession } from "../test/lib/scratch-session";
|
|
54
|
+
import { gitCleanEnv } from "../test/lib/git-clean-env";
|
|
55
|
+
import { PLUGIN_ROOT_TOKEN } from "./sync-opencode-adapter";
|
|
56
|
+
|
|
57
|
+
// Bind a per-process scratch session root (#7004, ADR-250) BEFORE anything
|
|
58
|
+
// allocates: this script runs via `bun <file>` (ci.yml), where the bunfig
|
|
59
|
+
// [test].preload does NOT run, so this statement IS the adoption — scratchDir()
|
|
60
|
+
// below then lands every harness-discovery-* home/repo under an owned
|
|
61
|
+
// soleur-run.<pid>.* root the reapers can attribute when a run is SIGKILLed
|
|
62
|
+
// (clean exits were already covered by SCRATCH[]/cleanupScratch()). A nested
|
|
63
|
+
// run under a runner inherits the parent's root and never deletes it.
|
|
64
|
+
ensureScratchSession();
|
|
65
|
+
|
|
66
|
+
export const REPO_ROOT = resolve(import.meta.dir, "../../..");
|
|
67
|
+
const PLUGIN_ROOT = resolve(REPO_ROOT, "plugins/soleur");
|
|
68
|
+
|
|
69
|
+
/** Per-call wall-clock cap. A vendor CLI that hangs must not hold the CI job open. */
|
|
70
|
+
export const CLI_TIMEOUT_MS = 90_000;
|
|
71
|
+
|
|
72
|
+
export type Harness = "codex" | "devin" | "opencode";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Scratch directories this process created, removed on the way out.
|
|
76
|
+
*
|
|
77
|
+
* `mkdtempSync` has no owner otherwise: a CI runner is thrown away so nothing shows there, but
|
|
78
|
+
* the same script run locally (which is how the parsers get debugged) leaves a fresh `CODEX_HOME`
|
|
79
|
+
* and a scratch git repo in `$TMPDIR` on every invocation. Registered rather than removed inline
|
|
80
|
+
* because every arm below can return early, including the throwing ones.
|
|
81
|
+
*/
|
|
82
|
+
const SCRATCH: string[] = [];
|
|
83
|
+
function scratchDir(prefix: string): string {
|
|
84
|
+
const d = mkdtempSync(join(tmpdir(), prefix));
|
|
85
|
+
SCRATCH.push(d);
|
|
86
|
+
return d;
|
|
87
|
+
}
|
|
88
|
+
function cleanupScratch(): void {
|
|
89
|
+
for (const d of SCRATCH.splice(0)) {
|
|
90
|
+
try {
|
|
91
|
+
rmSync(d, { recursive: true, force: true });
|
|
92
|
+
} catch {
|
|
93
|
+
// Best effort: a leaked scratch dir must never change this gate's verdict.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface Verdict {
|
|
99
|
+
missing: string[];
|
|
100
|
+
extra: string[];
|
|
101
|
+
badMultiplicity: string[];
|
|
102
|
+
/** Which multiplicity mode the run exhibited, once inferred. */
|
|
103
|
+
mode: "dedup" | "additive" | "single" | "indeterminate";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The expectation, derived from the manifest's own `skills` roots.
|
|
108
|
+
*
|
|
109
|
+
* Keyed on the DIRECTORY BASENAME, never on frontmatter `name:`: a skill that loses
|
|
110
|
+
* its `name:` must still appear in the expected set, so the CLI omitting it reads as
|
|
111
|
+
* MISSING rather than as a shrunken expectation that quietly agrees.
|
|
112
|
+
*
|
|
113
|
+
* The value is the number of declared roots containing that name — its `k`. The
|
|
114
|
+
* Codex `go`/`help`/`sync` duplication is therefore DERIVED from the manifest rather
|
|
115
|
+
* than acked by hand, so the allowance expires on its own when #8236 deletes a root.
|
|
116
|
+
*/
|
|
117
|
+
export function expectedSkills(manifestPath: string, pluginRoot = PLUGIN_ROOT): Map<string, number> {
|
|
118
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { skills?: string[] };
|
|
119
|
+
const roots = manifest.skills ?? [];
|
|
120
|
+
if (roots.length === 0) throw new Error(`${manifestPath} declares no skills roots`);
|
|
121
|
+
const counts = new Map<string, number>();
|
|
122
|
+
for (const root of roots) {
|
|
123
|
+
const dir = resolve(pluginRoot, root);
|
|
124
|
+
// THROW, never `continue`. A typo'd or deleted root silently contributing
|
|
125
|
+
// zero is the "shrunken expectation that quietly agrees" this function's
|
|
126
|
+
// header refuses one level down (at the skill-NAME level): the manifest
|
|
127
|
+
// declares the root, so its absence is a defect in the thing under test,
|
|
128
|
+
// not a reason to expect less of it. Measured consequence of the old form:
|
|
129
|
+
// `"skills": ["./skilz", …]` collapsed the expectation to the 3 shim stems,
|
|
130
|
+
// all 3 were discovered, and the gate exited 0 on a 99-skill loss.
|
|
131
|
+
if (!existsSync(dir)) {
|
|
132
|
+
throw new Error(`${manifestPath}: declared skills root '${root}' does not exist at ${dir}`);
|
|
133
|
+
}
|
|
134
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
135
|
+
if (!entry.isDirectory()) continue;
|
|
136
|
+
if (!existsSync(join(dir, entry.name, "SKILL.md"))) continue;
|
|
137
|
+
const name = `soleur:${entry.name}`;
|
|
138
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Roots that exist but hold no skill. Same argument as the throw above, one
|
|
142
|
+
// level out: an empty expectation makes `verdict()` trivially clean and the
|
|
143
|
+
// whole gate exits 0 having compared nothing against nothing.
|
|
144
|
+
if (counts.size === 0) {
|
|
145
|
+
throw new Error(`${manifestPath}: declared roots ${JSON.stringify(roots)} contain no SKILL.md`);
|
|
146
|
+
}
|
|
147
|
+
return counts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Canonicalise a path for comparison: symlinked scratch roots (`/var` → `/private/var`
|
|
152
|
+
* on macOS, a linked worktree anywhere) make the server's reported `path` and this
|
|
153
|
+
* process's `PLUGIN_ROOT` differ as STRINGS while denoting the same file.
|
|
154
|
+
*/
|
|
155
|
+
function canonical(p: string): string {
|
|
156
|
+
try {
|
|
157
|
+
return realpathSync(p);
|
|
158
|
+
} catch {
|
|
159
|
+
return resolve(p);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A path relative to the plugin root, with `/` separators, or `undefined` when it lies
|
|
165
|
+
* outside. Returning `undefined` rather than a `../…` string is what EXCLUDES global
|
|
166
|
+
* leakage: an operator's `~/.claude/skills/plan/SKILL.md` and the harness's own
|
|
167
|
+
* `/builtin/opencode.md` both carry ids this plugin also uses, so counting anything the
|
|
168
|
+
* declaration did not place under the plugin root would report them as registrations.
|
|
169
|
+
*/
|
|
170
|
+
function pluginRelative(pluginRoot: string, p: string): string | undefined {
|
|
171
|
+
const rel = relative(canonical(pluginRoot), canonical(p));
|
|
172
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return undefined;
|
|
173
|
+
return rel.split(sep).join("/");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The expectation for the OpenCode arm, derived from the generated fragment's own
|
|
178
|
+
* `skills` roots (measurement §11.0). Keyed on the plugin-relative `path`, because the
|
|
179
|
+
* server's ids are bare basenames that collide across roots (§2.3).
|
|
180
|
+
*
|
|
181
|
+
* The `__SOLEUR_PLUGIN_ROOT__` sentinel is resolved here with the same substitution the
|
|
182
|
+
* installer performs (D6): the committed fragment is machine-independent, and the arm
|
|
183
|
+
* must judge the bytes a user would actually install.
|
|
184
|
+
*/
|
|
185
|
+
export function expectedOpenCodeSkillFiles(manifestPath: string, pluginRoot = PLUGIN_ROOT): Map<string, number> {
|
|
186
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { skills?: string[] };
|
|
187
|
+
const roots = manifest.skills ?? [];
|
|
188
|
+
if (roots.length === 0) throw new Error(`${manifestPath} declares no skills roots`);
|
|
189
|
+
const counts = new Map<string, number>();
|
|
190
|
+
for (const root of roots) {
|
|
191
|
+
const dir = resolve(pluginRoot, root.split(PLUGIN_ROOT_TOKEN).join(pluginRoot));
|
|
192
|
+
// THROW, never `continue` — the same argument as `expectedSkills()` above: a
|
|
193
|
+
// declared root the installer would have written is an assertion, and a typo'd
|
|
194
|
+
// one silently shrinking the expectation is how a 103-skill loss reads green.
|
|
195
|
+
if (!existsSync(dir)) {
|
|
196
|
+
throw new Error(`${manifestPath}: declared skills root '${root}' does not exist at ${dir}`);
|
|
197
|
+
}
|
|
198
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
199
|
+
if (!entry.isDirectory()) continue;
|
|
200
|
+
const file = join(dir, entry.name, "SKILL.md");
|
|
201
|
+
if (!existsSync(file)) continue;
|
|
202
|
+
const rel = pluginRelative(pluginRoot, file);
|
|
203
|
+
if (rel) counts.set(rel, 1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (counts.size === 0) {
|
|
207
|
+
throw new Error(`${manifestPath}: declared roots ${JSON.stringify(roots)} contain no SKILL.md`);
|
|
208
|
+
}
|
|
209
|
+
return counts;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* `GET /api/skill` returns `{location, data: [{id, name, description, path, content}]}`
|
|
214
|
+
* (measurement §11.4), one record per registration — and `content` is the skill's full
|
|
215
|
+
* text, so the payload runs to megabytes and must be read from a FILE, never a pipe.
|
|
216
|
+
*
|
|
217
|
+
* Keyed on `path`, not `id`: ids are bare basenames with no namespace, so the same id
|
|
218
|
+
* is carried by this plugin's skill and by any operator-level copy. Counting is by
|
|
219
|
+
* relative path, so a duplicate registration of the SAME file still shows up.
|
|
220
|
+
*
|
|
221
|
+
* Unreadable input yields an EMPTY MAP rather than a throw: whether the bytes were a
|
|
222
|
+
* listing at all is `isStructurallyParseable()`'s question, and folding the two together
|
|
223
|
+
* is what would let a dead server grade as a set mismatch.
|
|
224
|
+
*/
|
|
225
|
+
export function parseOpenCodeSkillList(raw: string, pluginRoot = PLUGIN_ROOT): Map<string, number> {
|
|
226
|
+
const counts = new Map<string, number>();
|
|
227
|
+
let payload: { data?: unknown };
|
|
228
|
+
try {
|
|
229
|
+
payload = JSON.parse(raw) as { data?: unknown };
|
|
230
|
+
} catch {
|
|
231
|
+
return counts;
|
|
232
|
+
}
|
|
233
|
+
if (!Array.isArray(payload?.data)) return counts;
|
|
234
|
+
for (const entry of payload.data as Array<{ path?: unknown }>) {
|
|
235
|
+
if (typeof entry?.path !== "string") continue;
|
|
236
|
+
const rel = pluginRelative(pluginRoot, entry.path);
|
|
237
|
+
if (!rel) continue;
|
|
238
|
+
counts.set(rel, (counts.get(rel) ?? 0) + 1);
|
|
239
|
+
}
|
|
240
|
+
return counts;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* `codex debug prompt-input` emits the skills list inside an ESCAPED JSON STRING —
|
|
245
|
+
* `\n`, not real newlines — so a line-anchored regex over the raw bytes returns zero.
|
|
246
|
+
* Decoding first is the whole reason this is a function rather than a grep.
|
|
247
|
+
*/
|
|
248
|
+
export function parseCodexPromptInput(raw: string): Map<string, number> {
|
|
249
|
+
let text = raw;
|
|
250
|
+
const decoded = raw.replace(/\\n/g, "\n");
|
|
251
|
+
if (decoded !== raw) text = decoded;
|
|
252
|
+
const counts = new Map<string, number>();
|
|
253
|
+
for (const m of text.matchAll(/^\s*-\s+(soleur:[a-z0-9-]+):/gm)) {
|
|
254
|
+
counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
|
|
255
|
+
}
|
|
256
|
+
return counts;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** `devin skills list` prints ` /soleur:<name> …` one per registration. */
|
|
260
|
+
export function parseDevinSkillsList(raw: string): Map<string, number> {
|
|
261
|
+
const counts = new Map<string, number>();
|
|
262
|
+
for (const m of raw.matchAll(/^\s+\/(soleur:[a-z0-9-]+)(?:\s|$)/gm)) {
|
|
263
|
+
counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
|
|
264
|
+
}
|
|
265
|
+
return counts;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Did the listing carry the structure a parse depends on, at all? */
|
|
269
|
+
export function isStructurallyParseable(harness: Harness, raw: string, discovered: Map<string, number>): boolean {
|
|
270
|
+
if (discovered.size > 0) return true;
|
|
271
|
+
if (harness === "codex") return /<skills_instructions>/.test(raw);
|
|
272
|
+
if (harness === "opencode") {
|
|
273
|
+
// The marker is the payload's `data` array. An empty one is a READING, not an
|
|
274
|
+
// outage: a correct install answers `{location, data: []}` before a session
|
|
275
|
+
// exists (measurement §11.1), and a total loss must grade exit 1 naming the
|
|
276
|
+
// missing skills rather than `unparseable-output` (exit 3, UNRESOLVED) — the
|
|
277
|
+
// same AP-021 inversion the Devin arm below refuses.
|
|
278
|
+
try {
|
|
279
|
+
const payload = JSON.parse(raw) as { data?: unknown };
|
|
280
|
+
return Array.isArray(payload?.data);
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Devin has a structural marker too, and using it is what keeps the two arms
|
|
286
|
+
// honest. Without it, "the CLI answered and listed NOTHING" — a total loss of
|
|
287
|
+
// all 102 registrations, the worst regression this gate exists to catch —
|
|
288
|
+
// graded `unparseable-output` (exit 3, UNRESOLVED) on Devin while the
|
|
289
|
+
// identical Codex regression graded exit 1 with the names listed. That is the
|
|
290
|
+
// AP-021 inversion the exit taxonomy below forbids in the other direction,
|
|
291
|
+
// and it would have been the triage signal for #8574's soak.
|
|
292
|
+
return /^\s*Available skills:/m.test(raw);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* ONE MODE PER HARNESS RUN.
|
|
297
|
+
*
|
|
298
|
+
* A name found in k declared roots may be listed once (the harness dedups) or k times
|
|
299
|
+
* (it is additive) — but not a MIXTURE. Checking "1 or k" per name independently would
|
|
300
|
+
* pass a listing with `go` twice and `help` once, which is exactly the loader ambiguity
|
|
301
|
+
* ADR-224 decision 5 exists to detect. So the mode is INFERRED across every multi-root
|
|
302
|
+
* name, and a mixture goes to badMultiplicity.
|
|
303
|
+
*
|
|
304
|
+
* Measured 2026-09-23: Codex 0.156.1 is additive (go/help/sync twice, 105 entries for
|
|
305
|
+
* 102 names); Devin 3000.11.1 dedups (102 entries, each once).
|
|
306
|
+
*/
|
|
307
|
+
export function verdict(expected: Map<string, number>, discovered: Map<string, number>): Verdict {
|
|
308
|
+
const missing: string[] = [];
|
|
309
|
+
const extra: string[] = [];
|
|
310
|
+
const badMultiplicity: string[] = [];
|
|
311
|
+
|
|
312
|
+
for (const name of discovered.keys()) if (!expected.has(name)) extra.push(name);
|
|
313
|
+
|
|
314
|
+
const multiRoot = [...expected.entries()].filter(([, k]) => k >= 2);
|
|
315
|
+
let mode: Verdict["mode"] = "indeterminate";
|
|
316
|
+
if (multiRoot.length === 0) {
|
|
317
|
+
// No name is declared by more than one root, so there is no cross-root
|
|
318
|
+
// multiplicity to infer and `indeterminate` would read as a defect. This is
|
|
319
|
+
// the OpenCode shape: one canonical tree, no shim roots.
|
|
320
|
+
mode = "single";
|
|
321
|
+
} else {
|
|
322
|
+
// `n === 0` is MISSING, and is reported as such below. Feeding it into the
|
|
323
|
+
// mode inference too makes an ordinary omission arrive as "3 missing, 0
|
|
324
|
+
// extra, 3 bad-multiplicity" — a bogus multiplicity story layered over the
|
|
325
|
+
// real cause, which is the operator's first read of a red run.
|
|
326
|
+
const observed = multiRoot
|
|
327
|
+
.map(([name, k]) => ({ name, k, n: discovered.get(name) ?? 0 }))
|
|
328
|
+
.filter((o) => o.n > 0);
|
|
329
|
+
const allOne = observed.every((o) => o.n === 1);
|
|
330
|
+
const allK = observed.every((o) => o.n === o.k);
|
|
331
|
+
if (allOne) mode = "dedup";
|
|
332
|
+
else if (allK) mode = "additive";
|
|
333
|
+
else {
|
|
334
|
+
mode = "indeterminate";
|
|
335
|
+
for (const o of observed) {
|
|
336
|
+
if (o.n !== 1 && o.n !== o.k) {
|
|
337
|
+
badMultiplicity.push(`${o.name}: listed ${o.n} times, expected 1 (dedup) or ${o.k} (additive)`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (badMultiplicity.length === 0) {
|
|
341
|
+
badMultiplicity.push(
|
|
342
|
+
`mixed multiplicity mode across multi-root names: ${observed
|
|
343
|
+
.map((o) => `${o.name}=${o.n}/k${o.k}`)
|
|
344
|
+
.join(", ")} — a harness must dedup consistently or be additive consistently`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const [name, k] of expected) {
|
|
351
|
+
const n = discovered.get(name) ?? 0;
|
|
352
|
+
if (n === 0) {
|
|
353
|
+
missing.push(name);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const allowed = mode === "dedup" ? [1] : mode === "additive" ? [k] : [1, k];
|
|
357
|
+
if (!allowed.includes(n)) {
|
|
358
|
+
badMultiplicity.push(`${name}: listed ${n} times, expected ${allowed.join(" or ")}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { missing, extra, badMultiplicity, mode };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export interface ExitInput {
|
|
366
|
+
cliPresent: boolean;
|
|
367
|
+
installFailed?: boolean;
|
|
368
|
+
/** The probe command itself failed (non-zero, or killed at CLI_TIMEOUT_MS). */
|
|
369
|
+
probeFailed?: boolean;
|
|
370
|
+
version?: string;
|
|
371
|
+
pin?: string;
|
|
372
|
+
parseable: boolean;
|
|
373
|
+
verdict?: Verdict;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface ExitResult {
|
|
377
|
+
code: 0 | 1 | 3;
|
|
378
|
+
reason: string;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The exit decision, PURE and exported — the two rows carrying the whole "never exit 0
|
|
383
|
+
* on an empty or unparsed listing" contract are decided in the CLI-driving path
|
|
384
|
+
* otherwise, where no unit test can reach them.
|
|
385
|
+
*
|
|
386
|
+
* ADR-177 taxonomy. Exit 3 is UNRESOLVED and is reserved for a listing that is
|
|
387
|
+
* STRUCTURALLY unreadable — no `<skills_instructions>` marker, or zero `soleur:` lines.
|
|
388
|
+
* A listing that PARSED but is missing names is a real partial loss and exits 1 naming
|
|
389
|
+
* them: mapping "discovered < 100" to exit 3 would report a 60-skill regression as
|
|
390
|
+
* "could not check", which names a cause the gate did not measure (AP-021).
|
|
391
|
+
*/
|
|
392
|
+
export function resolveExit(input: ExitInput): ExitResult {
|
|
393
|
+
if (!input.cliPresent) return { code: 3, reason: "cli-missing" };
|
|
394
|
+
// "Could not read the version" is NOT "the version is fine". `cliVersion()`
|
|
395
|
+
// returns undefined whenever `--version` exits non-zero or prints no semver,
|
|
396
|
+
// and the old `pin && version && …` form skipped the comparison entirely in
|
|
397
|
+
// exactly that case — so `--pin` failed OPEN on an unreadable binary, which
|
|
398
|
+
// is the one mapping the ADR-177 taxonomy exists to forbid.
|
|
399
|
+
if (input.pin && !input.version) return { code: 3, reason: "version-unknown" };
|
|
400
|
+
if (input.pin && input.version && input.version !== input.pin) {
|
|
401
|
+
return { code: 3, reason: `version-mismatch:${input.version}!=${input.pin}` };
|
|
402
|
+
}
|
|
403
|
+
if (input.installFailed) return { code: 3, reason: "install-failed" };
|
|
404
|
+
// A probe that died (timeout, crash, non-zero) yields PARTIAL output, and a
|
|
405
|
+
// partial listing parses. Without this, a CLI that printed 40 of 102 names
|
|
406
|
+
// and then hung reported "set-mismatch: 62 missing" — an authoritative claim
|
|
407
|
+
// of a regression that did not happen, from a run that could not measure one.
|
|
408
|
+
if (input.probeFailed) return { code: 3, reason: "probe-failed" };
|
|
409
|
+
if (!input.parseable) return { code: 3, reason: "unparseable-output" };
|
|
410
|
+
const v = input.verdict;
|
|
411
|
+
if (!v) return { code: 3, reason: "unparseable-output" };
|
|
412
|
+
const problems = v.missing.length + v.extra.length + v.badMultiplicity.length;
|
|
413
|
+
if (problems > 0) {
|
|
414
|
+
return {
|
|
415
|
+
code: 1,
|
|
416
|
+
reason: `set-mismatch: ${v.missing.length} missing, ${v.extra.length} extra, ${v.badMultiplicity.length} bad-multiplicity`,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return { code: 0, reason: `ok (mode=${v.mode})` };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Neutralise text before it reaches a GitHub Actions log.
|
|
424
|
+
*
|
|
425
|
+
* WHY THIS IS NOT PARANOIA. GitHub parses a WORKFLOW COMMAND from any log line whose first
|
|
426
|
+
* non-whitespace characters are `::` — `::error::`, `::add-mask::`, `::set-output` and, worst,
|
|
427
|
+
* `::stop-commands::`. Everything this script prints on a red path is derived from two places a
|
|
428
|
+
* pull request can write to:
|
|
429
|
+
*
|
|
430
|
+
* - the vendor CLI's raw output, which ECHOES skill names, and
|
|
431
|
+
* - `verdict()`'s `missing` / `extra` / `badMultiplicity` arrays, whose members are built from
|
|
432
|
+
* `readdirSync` over `plugins/soleur/skills/` — i.e. from DIRECTORY NAMES in the checkout.
|
|
433
|
+
*
|
|
434
|
+
* A directory name may contain `:`, and on a fork PR the checkout is the fork's. So a fork could
|
|
435
|
+
* add `skills/<name-containing-a-newline-and-::error::…>/SKILL.md` and have this job emit
|
|
436
|
+
* annotations, mask arbitrary strings out of the log, or issue `::stop-commands::` and silence
|
|
437
|
+
* every real annotation after it. The job is advisory and cannot gate a merge, but a log that can
|
|
438
|
+
* be authored by the thing under test is not evidence.
|
|
439
|
+
*
|
|
440
|
+
* TWO LAYERS, because either alone has a hole:
|
|
441
|
+
*
|
|
442
|
+
* 1. Per-line neutralisation. A line that would start a command gets one leading space, which
|
|
443
|
+
* GitHub does NOT trim back into a command. Applied after normalising `\r`, since a lone CR
|
|
444
|
+
* also begins a new line on a terminal.
|
|
445
|
+
* 2. A `::stop-commands::<token>` fence around the whole block, with a 128-bit random token.
|
|
446
|
+
* Layer 1 is what makes the fence safe: without it, untrusted text containing the resume
|
|
447
|
+
* token would end the fence early, and with it no untrusted line can be a command at all.
|
|
448
|
+
*
|
|
449
|
+
* C0 control characters other than `\n` and `\t` are dropped outright — they carry no
|
|
450
|
+
* information here and are how a name hides what it really is in a terminal.
|
|
451
|
+
*/
|
|
452
|
+
export function sanitizeForLog(text: string): string {
|
|
453
|
+
return text
|
|
454
|
+
.replace(/\r\n?/g, "\n")
|
|
455
|
+
// eslint-disable-next-line no-control-regex -- the C0 set is the subject, not a typo (cq-regex-unicode-separators-escape-only)
|
|
456
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
|
|
457
|
+
.split("\n")
|
|
458
|
+
.map((line) => (/^\s*::/.test(line) ? ` ${line}` : line))
|
|
459
|
+
.join("\n");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Write untrusted text to stderr inside a random-token workflow-command fence. */
|
|
463
|
+
function writeUntrusted(text: string): void {
|
|
464
|
+
const token = randomBytes(16).toString("hex");
|
|
465
|
+
process.stderr.write(`::stop-commands::${token}\n`);
|
|
466
|
+
process.stderr.write(sanitizeForLog(text));
|
|
467
|
+
if (!text.endsWith("\n")) process.stderr.write("\n");
|
|
468
|
+
process.stderr.write(`::${token}::\n`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function run(cmd: string, args: string[], env: Record<string, string>, cwd = REPO_ROOT): { ok: boolean; out: string } {
|
|
472
|
+
try {
|
|
473
|
+
const out = execFileSync(cmd, args, {
|
|
474
|
+
cwd,
|
|
475
|
+
env,
|
|
476
|
+
encoding: "utf8",
|
|
477
|
+
timeout: CLI_TIMEOUT_MS,
|
|
478
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
479
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
480
|
+
});
|
|
481
|
+
return { ok: true, out };
|
|
482
|
+
} catch (e) {
|
|
483
|
+
const err = e as { stdout?: string; stderr?: string; message?: string };
|
|
484
|
+
return { ok: false, out: `${err.stdout ?? ""}\n${err.stderr ?? ""}\n${err.message ?? ""}` };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function have(bin: string): boolean {
|
|
489
|
+
try {
|
|
490
|
+
execFileSync("command", ["-v", bin], { shell: "/bin/bash", stdio: "ignore" });
|
|
491
|
+
return true;
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function cliVersion(bin: string, env: Record<string, string> = { PATH: process.env.PATH ?? "" }): string | undefined {
|
|
498
|
+
const r = run(bin, ["--version"], env);
|
|
499
|
+
if (!r.ok) return undefined;
|
|
500
|
+
const m = r.out.match(/\d+\.\d+\.\d+/);
|
|
501
|
+
return m ? m[0] : undefined;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function driveCodex(pin?: string): { input: ExitInput; raw: string } {
|
|
505
|
+
if (!have("codex")) return { input: { cliPresent: false, parseable: false }, raw: "" };
|
|
506
|
+
const home = scratchDir("harness-discovery-codex-");
|
|
507
|
+
const env = { PATH: process.env.PATH ?? "", HOME: home, CODEX_HOME: home };
|
|
508
|
+
const version = cliVersion("codex");
|
|
509
|
+
if (pin && version && version !== pin) {
|
|
510
|
+
return { input: { cliPresent: true, parseable: false, version, pin }, raw: "" };
|
|
511
|
+
}
|
|
512
|
+
const add = run("codex", ["plugin", "marketplace", "add", REPO_ROOT], env);
|
|
513
|
+
const install = run("codex", ["plugin", "add", "soleur@soleur"], env);
|
|
514
|
+
if (!add.ok || !install.ok) {
|
|
515
|
+
return { input: { cliPresent: true, installFailed: true, parseable: false, version, pin }, raw: `${add.out}\n${install.out}` };
|
|
516
|
+
}
|
|
517
|
+
const probe = run("codex", ["debug", "prompt-input"], env);
|
|
518
|
+
const discovered = parseCodexPromptInput(probe.out);
|
|
519
|
+
const parseable = isStructurallyParseable("codex", probe.out, discovered);
|
|
520
|
+
const expected = expectedSkills(resolve(PLUGIN_ROOT, ".codex-plugin/plugin.json"));
|
|
521
|
+
const usable = probe.ok && parseable;
|
|
522
|
+
return {
|
|
523
|
+
input: {
|
|
524
|
+
cliPresent: true,
|
|
525
|
+
probeFailed: !probe.ok,
|
|
526
|
+
parseable,
|
|
527
|
+
version,
|
|
528
|
+
pin,
|
|
529
|
+
verdict: usable ? verdict(expected, discovered) : undefined,
|
|
530
|
+
},
|
|
531
|
+
raw: probe.out,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function driveDevin(pin?: string): { input: ExitInput; raw: string } {
|
|
536
|
+
if (!have("devin")) return { input: { cliPresent: false, parseable: false }, raw: "" };
|
|
537
|
+
const home = scratchDir("harness-discovery-devin-");
|
|
538
|
+
const scratch = scratchDir("harness-discovery-devin-repo-");
|
|
539
|
+
const env = {
|
|
540
|
+
PATH: process.env.PATH ?? "",
|
|
541
|
+
HOME: home,
|
|
542
|
+
XDG_CONFIG_HOME: join(home, "config"),
|
|
543
|
+
XDG_DATA_HOME: join(home, "data"),
|
|
544
|
+
};
|
|
545
|
+
const version = cliVersion("devin");
|
|
546
|
+
if (pin && version && version !== pin) {
|
|
547
|
+
return { input: { cliPresent: true, parseable: false, version, pin }, raw: "" };
|
|
548
|
+
}
|
|
549
|
+
const init = run("git", ["init", "-q", scratch], env, scratch);
|
|
550
|
+
if (!init.ok) {
|
|
551
|
+
// Otherwise the scratch dir is not a repo, Devin reads no config, and the
|
|
552
|
+
// empty listing grades `unparseable-output` — naming a cause we did not
|
|
553
|
+
// measure when the real one is right here.
|
|
554
|
+
return { input: { cliPresent: true, installFailed: true, parseable: false, version, pin }, raw: init.out };
|
|
555
|
+
}
|
|
556
|
+
mkdirSync(join(scratch, ".devin"), { recursive: true });
|
|
557
|
+
writeFileSync(
|
|
558
|
+
join(scratch, ".devin/config.json"),
|
|
559
|
+
// The repo-scoped local source: `devin plugins install` requires auth, this does not.
|
|
560
|
+
JSON.stringify({ requiredPlugins: [{ source: "local", path: PLUGIN_ROOT }] }, null, 2),
|
|
561
|
+
);
|
|
562
|
+
const probe = run("devin", ["skills", "list"], env, scratch);
|
|
563
|
+
const discovered = parseDevinSkillsList(probe.out);
|
|
564
|
+
const parseable = isStructurallyParseable("devin", probe.out, discovered);
|
|
565
|
+
const expected = expectedSkills(resolve(PLUGIN_ROOT, ".devin-plugin/plugin.json"));
|
|
566
|
+
const usable = probe.ok && parseable;
|
|
567
|
+
return {
|
|
568
|
+
input: {
|
|
569
|
+
cliPresent: true,
|
|
570
|
+
probeFailed: !probe.ok,
|
|
571
|
+
parseable,
|
|
572
|
+
version,
|
|
573
|
+
pin,
|
|
574
|
+
verdict: usable ? verdict(expected, discovered) : undefined,
|
|
575
|
+
},
|
|
576
|
+
raw: probe.out,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Synchronous sleep, so the server-poll loop keeps this file's all-sync shape. */
|
|
581
|
+
function sleepMs(ms: number): void {
|
|
582
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** How long a spawned `opencode serve` has to announce its port and password. */
|
|
586
|
+
export const SERVER_BOOT_TIMEOUT_MS = 60_000;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* How long the location has to finish SCANNING its skill roots before the reading is
|
|
590
|
+
* taken. Measured §11.6: the scan is asynchronous and the first read is always empty;
|
|
591
|
+
* a hermetic install settles to all 103 canonical skills in **~2 s**, and the poll exits
|
|
592
|
+
* the moment the expected population is reached. The cap is therefore a ceiling on a
|
|
593
|
+
* failure, not a per-run cost.
|
|
594
|
+
*/
|
|
595
|
+
export const SKILL_SETTLE_TIMEOUT_MS = 120_000;
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The absolute path to a binary.
|
|
599
|
+
*
|
|
600
|
+
* `opencode` on PATH is almost always a version-manager shim, and a shim resolves its
|
|
601
|
+
* install through `$HOME` — which this arm redirects for hermeticity, and which makes the
|
|
602
|
+
* shim try to REINSTALL (measured: the run hangs, then dies with `Disk quota exceeded`).
|
|
603
|
+
* `readlink -f` follows the shim chain to the real launcher, which needs no HOME at all.
|
|
604
|
+
*/
|
|
605
|
+
export function resolveBin(bin: string): string | undefined {
|
|
606
|
+
const r = run(
|
|
607
|
+
"bash",
|
|
608
|
+
["-c", 'p=$(command -v "$1") || exit 1; r=$(readlink -f "$p" 2>/dev/null) || r="$p"; [ -n "$r" ] || r="$p"; printf "%s" "$r"', "bash", bin],
|
|
609
|
+
{ PATH: process.env.PATH ?? "" },
|
|
610
|
+
);
|
|
611
|
+
const out = r.out.trim();
|
|
612
|
+
return r.ok && out !== "" ? out : undefined;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Run a command with its stdout going straight to a FILE.
|
|
617
|
+
*
|
|
618
|
+
* `run()`'s pipe is NOT safe for this payload. `execFileSync` silently truncates a large
|
|
619
|
+
* stdout — observed at ~800 kB and ~946 kB, different each run — and still reports
|
|
620
|
+
* SUCCESS, so the JSON arrives with an unclosed string and `JSON.parse` fails, which
|
|
621
|
+
* grades a correct install UNRESOLVED (measurement §11.6). The same call through a file
|
|
622
|
+
* descriptor yields all 3,069,017 bytes, valid. Hence the file, and hence not `run()`.
|
|
623
|
+
*/
|
|
624
|
+
function runToFile(
|
|
625
|
+
cmd: string,
|
|
626
|
+
args: string[],
|
|
627
|
+
env: Record<string, string>,
|
|
628
|
+
cwd: string,
|
|
629
|
+
outPath: string,
|
|
630
|
+
): { ok: boolean; err: string } {
|
|
631
|
+
const fd = openSync(outPath, "w");
|
|
632
|
+
try {
|
|
633
|
+
const r = spawnSync(cmd, args, {
|
|
634
|
+
cwd,
|
|
635
|
+
env,
|
|
636
|
+
stdio: ["ignore", fd, "pipe"],
|
|
637
|
+
timeout: CLI_TIMEOUT_MS,
|
|
638
|
+
encoding: "utf8",
|
|
639
|
+
});
|
|
640
|
+
return { ok: r.status === 0, err: r.stderr ?? "" };
|
|
641
|
+
} finally {
|
|
642
|
+
closeSync(fd);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Drive a real `opencode serve` and read `GET /api/skill` from it.
|
|
648
|
+
*
|
|
649
|
+
* Every step is forced by the measured recipe (measurement §11.0/§11.6):
|
|
650
|
+
*
|
|
651
|
+
* - `serve` is a LONG-RUNNING process, so this is the one arm that is not a single
|
|
652
|
+
* `execFileSync`. It is spawned detached into a log file and polled; the log is also
|
|
653
|
+
* where the port and password come from.
|
|
654
|
+
* - `--port 0` lets the OS pick, and the banner names the chosen port, so two CI jobs
|
|
655
|
+
* cannot race for a fixed one.
|
|
656
|
+
* - Every `/api/*` route is `UnauthorizedError` without that password, and `opencode
|
|
657
|
+
* api` calls `GET /api/info` first.
|
|
658
|
+
* - The listing is built by an ASYNCHRONOUS scan of the location's skill roots, so the
|
|
659
|
+
* first read is always empty and a single read reports a correct install as a total
|
|
660
|
+
* loss. The poll below is the whole reason this is not a single call.
|
|
661
|
+
* - The listing is read from a FILE, never from `run()`'s pipe — see `runToFile`.
|
|
662
|
+
*
|
|
663
|
+
* Deliberately NOT part of the recipe: `POST /api/session` (measured unnecessary — an
|
|
664
|
+
* idle boot settles) and `POST /api/location/reload` (measured to CLEAR the listing
|
|
665
|
+
* before rebuilding it, which would make the poll race its own reset).
|
|
666
|
+
*/
|
|
667
|
+
/**
|
|
668
|
+
* Is a child pid still alive? The `error`/`exit` events cannot fire while the boot loop blocks
|
|
669
|
+
* the thread, so liveness is asked synchronously instead of awaited: `kill(pid, 0)` performs no
|
|
670
|
+
* signal and throws `ESRCH` for a process that is gone.
|
|
671
|
+
*/
|
|
672
|
+
function processAlive(pid: number | undefined): boolean {
|
|
673
|
+
if (pid === undefined) return false;
|
|
674
|
+
try {
|
|
675
|
+
process.kill(pid, 0);
|
|
676
|
+
return true;
|
|
677
|
+
} catch {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function openCodeSkillProbe(
|
|
683
|
+
bin: string,
|
|
684
|
+
project: string,
|
|
685
|
+
env: Record<string, string>,
|
|
686
|
+
expected: Map<string, number>,
|
|
687
|
+
): { ok: boolean; raw: string } {
|
|
688
|
+
const logPath = join(project, "serve.log");
|
|
689
|
+
const fd = openSync(logPath, "w");
|
|
690
|
+
const child = spawn(bin, ["serve", "--port", "0", "--hostname", "127.0.0.1"], {
|
|
691
|
+
cwd: project,
|
|
692
|
+
env,
|
|
693
|
+
stdio: ["ignore", fd, fd],
|
|
694
|
+
detached: true,
|
|
695
|
+
});
|
|
696
|
+
let spawnError: Error | undefined;
|
|
697
|
+
child.on("error", (e: Error) => {
|
|
698
|
+
spawnError = e;
|
|
699
|
+
});
|
|
700
|
+
child.unref();
|
|
701
|
+
closeSync(fd);
|
|
702
|
+
|
|
703
|
+
const readLog = (): string => {
|
|
704
|
+
try {
|
|
705
|
+
return readFileSync(logPath, "utf8");
|
|
706
|
+
} catch {
|
|
707
|
+
return "";
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
let url: string | undefined;
|
|
713
|
+
let password: string | undefined;
|
|
714
|
+
const bootDeadline = Date.now() + SERVER_BOOT_TIMEOUT_MS;
|
|
715
|
+
while (Date.now() < bootDeadline && !(url && password)) {
|
|
716
|
+
const text = readLog();
|
|
717
|
+
url ??= text.match(/server listening on (http:\/\/[^\s]+)/)?.[1];
|
|
718
|
+
password ??= text.match(/server password (\S+)/)?.[1];
|
|
719
|
+
if (url && password) break;
|
|
720
|
+
sleepMs(250);
|
|
721
|
+
}
|
|
722
|
+
if (!url || !password) {
|
|
723
|
+
// `spawnError` is set by the child's `error` event, and no event handler can run while
|
|
724
|
+
// this loop blocks the thread in `Atomics.wait` (`sleepMs`) — so the check is HERE, after
|
|
725
|
+
// the loop, where it is reachable. A child that never spawned (no pid) or already
|
|
726
|
+
// exited is a DEAD PROBE (UNRESOLVED), not a slow boot: grading it as "did not announce
|
|
727
|
+
// itself" would report a correct install as a set mismatch.
|
|
728
|
+
if (spawnError !== undefined || !processAlive(child.pid)) {
|
|
729
|
+
const why = spawnError?.message ?? `process ${String(child.pid)} is not running`;
|
|
730
|
+
return { ok: false, raw: `server failed to start: ${why}\n${readLog().slice(0, 4096)}` };
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
ok: false,
|
|
734
|
+
raw: `server did not announce itself within ${SERVER_BOOT_TIMEOUT_MS}ms:\n${readLog().slice(0, 4096)}`,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const apiEnv = { ...env, OPENCODE_PASSWORD: password };
|
|
739
|
+
const outPath = join(project, "skills.json");
|
|
740
|
+
const deadline = Date.now() + SKILL_SETTLE_TIMEOUT_MS;
|
|
741
|
+
let last = "";
|
|
742
|
+
let failures = 0;
|
|
743
|
+
for (;;) {
|
|
744
|
+
const listing = runToFile(bin, ["api", "--server", url, "GET", "/api/skill"], apiEnv, project, outPath);
|
|
745
|
+
let text = "";
|
|
746
|
+
try {
|
|
747
|
+
text = readFileSync(outPath, "utf8");
|
|
748
|
+
} catch {
|
|
749
|
+
text = "";
|
|
750
|
+
}
|
|
751
|
+
if (listing.ok) {
|
|
752
|
+
failures = 0;
|
|
753
|
+
last = text;
|
|
754
|
+
if (parseOpenCodeSkillList(last).size >= expected.size) break;
|
|
755
|
+
} else {
|
|
756
|
+
last = `listing failed (status != 0): ${listing.err}\n--- partial payload ---\n${text.slice(0, 2048)}`;
|
|
757
|
+
// A transport that keeps failing is a dead probe, not an empty listing: five
|
|
758
|
+
// consecutive failures end the arm as UNRESOLVED rather than as a set mismatch.
|
|
759
|
+
if (++failures >= 5) return { ok: false, raw: last };
|
|
760
|
+
}
|
|
761
|
+
if (Date.now() >= deadline) break;
|
|
762
|
+
sleepMs(1000);
|
|
763
|
+
}
|
|
764
|
+
return { ok: true, raw: last };
|
|
765
|
+
} finally {
|
|
766
|
+
// Detached, so the child leads its own process group: signalling the group also
|
|
767
|
+
// reaps anything the server forked, and leaves no listener behind on a red run.
|
|
768
|
+
if (child.pid) {
|
|
769
|
+
try {
|
|
770
|
+
process.kill(-child.pid, "SIGTERM");
|
|
771
|
+
} catch {
|
|
772
|
+
try {
|
|
773
|
+
child.kill("SIGTERM");
|
|
774
|
+
} catch {
|
|
775
|
+
// Already gone — nothing to clean up.
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function driveOpenCode(pin?: string): { input: ExitInput; raw: string } {
|
|
783
|
+
if (!have("opencode")) return { input: { cliPresent: false, parseable: false }, raw: "" };
|
|
784
|
+
const home = scratchDir("harness-discovery-opencode-");
|
|
785
|
+
const project = scratchDir("harness-discovery-opencode-repo-");
|
|
786
|
+
const configDir = join(home, ".config", "opencode");
|
|
787
|
+
// A fully hermetic HOME + XDG set: the operator's own config, and the `~/.claude/skills`
|
|
788
|
+
// / `~/.agents/skills` trees this harness also reads, are then out of reach — measured
|
|
789
|
+
// §11.4 to be the only control needed for the reading to be this plugin's 103 skills
|
|
790
|
+
// plus the harness's own 2 built-ins, with no "other" bucket at all. The absolute
|
|
791
|
+
// binary (`resolveBin`) is what makes a hermetic HOME safe: a PATH shim resolves its
|
|
792
|
+
// install THROUGH `$HOME` and reinstalls into the scratch dir without one.
|
|
793
|
+
const env = gitCleanEnv({
|
|
794
|
+
HOME: home,
|
|
795
|
+
PATH: process.env.PATH ?? "",
|
|
796
|
+
XDG_CONFIG_HOME: join(home, ".config"),
|
|
797
|
+
XDG_DATA_HOME: join(home, ".local", "share"),
|
|
798
|
+
XDG_STATE_HOME: join(home, ".local", "state"),
|
|
799
|
+
});
|
|
800
|
+
const bin = resolveBin("opencode") ?? "opencode";
|
|
801
|
+
const version = cliVersion(bin, env);
|
|
802
|
+
if (pin && version && version !== pin) {
|
|
803
|
+
return { input: { cliPresent: true, parseable: false, version, pin }, raw: "" };
|
|
804
|
+
}
|
|
805
|
+
const init = run("git", ["init", "-q", project], env, project);
|
|
806
|
+
if (!init.ok) {
|
|
807
|
+
// `opencode serve` opens a project, and the scratch dir must be a repo for that to
|
|
808
|
+
// mean anything. A failure here is install-shaped, not a parse one.
|
|
809
|
+
return { input: { cliPresent: true, installFailed: true, parseable: false, version, pin }, raw: init.out };
|
|
810
|
+
}
|
|
811
|
+
// The user-facing installer, not a hand-rolled copy of what it does: the arm must
|
|
812
|
+
// judge the bytes an operator actually gets, sentinel and all.
|
|
813
|
+
const install = run(
|
|
814
|
+
process.execPath,
|
|
815
|
+
[resolve(PLUGIN_ROOT, "scripts/install-opencode-adapter.ts"), "--target", configDir],
|
|
816
|
+
env,
|
|
817
|
+
);
|
|
818
|
+
if (!install.ok) {
|
|
819
|
+
return { input: { cliPresent: true, installFailed: true, parseable: false, version, pin }, raw: install.out };
|
|
820
|
+
}
|
|
821
|
+
const expected = expectedOpenCodeSkillFiles(resolve(PLUGIN_ROOT, "opencode/opencode.json"));
|
|
822
|
+
const probe = openCodeSkillProbe(bin, project, env, expected);
|
|
823
|
+
const discovered = parseOpenCodeSkillList(probe.raw);
|
|
824
|
+
const parseable = isStructurallyParseable("opencode", probe.raw, discovered);
|
|
825
|
+
const usable = probe.ok && parseable;
|
|
826
|
+
return {
|
|
827
|
+
input: {
|
|
828
|
+
cliPresent: true,
|
|
829
|
+
probeFailed: !probe.ok,
|
|
830
|
+
parseable,
|
|
831
|
+
version,
|
|
832
|
+
pin,
|
|
833
|
+
verdict: usable ? verdict(expected, discovered) : undefined,
|
|
834
|
+
},
|
|
835
|
+
raw: probe.raw,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const USAGE = "usage: harness-discovery-smoke.ts --harness codex|devin|opencode [--pin <version>]\n";
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Argument parsing, PURE and exported so the fail-open cases below are reachable from a unit test.
|
|
843
|
+
*
|
|
844
|
+
* Both of them were live, and both fail in the direction that reports a green:
|
|
845
|
+
*
|
|
846
|
+
* - `argv[argv.indexOf("--harness") + 1]` reads `argv[0]` when the flag is ABSENT, because
|
|
847
|
+
* `indexOf` returns -1 and -1 + 1 is 0. `harness-discovery-smoke.ts codex` therefore ran the
|
|
848
|
+
* Codex arm with no flag at all, and any other bare first argument silently selected nothing.
|
|
849
|
+
* - `--pin` with no value yields `undefined`, which is indistinguishable from "no pin given", so
|
|
850
|
+
* a typo'd flag SKIPS the version comparison entirely. A pin that silently does not apply is
|
|
851
|
+
* worse than no pin: the job still prints its reassuring pin in the log.
|
|
852
|
+
*
|
|
853
|
+
* A flag-shaped value (`--pin --harness`) is likewise a mistake, never a version.
|
|
854
|
+
*/
|
|
855
|
+
export function parseArgs(argv: readonly string[]): { harness: Harness; pin?: string } | { error: string } {
|
|
856
|
+
const hIdx = argv.indexOf("--harness");
|
|
857
|
+
if (hIdx === -1) return { error: "missing --harness" };
|
|
858
|
+
const harness = argv[hIdx + 1];
|
|
859
|
+
if (harness !== "codex" && harness !== "devin" && harness !== "opencode") {
|
|
860
|
+
return { error: `--harness must be codex, devin or opencode, got ${JSON.stringify(harness ?? null)}` };
|
|
861
|
+
}
|
|
862
|
+
const pIdx = argv.indexOf("--pin");
|
|
863
|
+
if (pIdx === -1) return { harness };
|
|
864
|
+
const pin = argv[pIdx + 1];
|
|
865
|
+
if (pin === undefined || pin.startsWith("--")) {
|
|
866
|
+
return { error: `--pin requires a version, got ${JSON.stringify(pin ?? null)}` };
|
|
867
|
+
}
|
|
868
|
+
return { harness, pin };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function main(): void {
|
|
872
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
873
|
+
if ("error" in parsed) {
|
|
874
|
+
process.stderr.write(`harness-discovery: ${parsed.error}\n${USAGE}`);
|
|
875
|
+
process.exit(2);
|
|
876
|
+
}
|
|
877
|
+
const { harness, pin } = parsed;
|
|
878
|
+
|
|
879
|
+
let result: ExitResult;
|
|
880
|
+
let input: ExitInput;
|
|
881
|
+
let raw: string;
|
|
882
|
+
try {
|
|
883
|
+
const arms = { codex: driveCodex, devin: driveDevin, opencode: driveOpenCode } as const;
|
|
884
|
+
({ input, raw } = arms[harness](pin));
|
|
885
|
+
result = resolveExit(input);
|
|
886
|
+
} finally {
|
|
887
|
+
// Before the writes below, and before any exit: `process.exit()` runs no `finally`.
|
|
888
|
+
cleanupScratch();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (input.verdict) {
|
|
892
|
+
// Untrusted: `missing`/`extra` members come from directory names in the checkout.
|
|
893
|
+
writeUntrusted(JSON.stringify({ harness, ...input.verdict }, null, 2));
|
|
894
|
+
}
|
|
895
|
+
// Trusted: every field here is produced by this file.
|
|
896
|
+
process.stderr.write(`harness-discovery[${harness}]: exit=${result.code} reason=${result.reason}\n`);
|
|
897
|
+
if (result.code !== 0) {
|
|
898
|
+
// The first 4 KB of raw vendor output, so a red job is diagnosable from the log
|
|
899
|
+
// itself and needs no artifact upload. Untrusted — see `sanitizeForLog`.
|
|
900
|
+
process.stderr.write(`--- raw ${harness} output (first 4 KB) ---\n`);
|
|
901
|
+
writeUntrusted(raw.slice(0, 4096));
|
|
902
|
+
}
|
|
903
|
+
process.exit(result.code);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (import.meta.main) main();
|