@soleur/opencode 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/agents.manifest.json +476 -0
- package/.claude-plugin/plugin.json +42 -0
- package/README.md +44 -2
- package/agents/community/.gitkeep +0 -0
- package/agents/engineering/cto.md +87 -0
- package/agents/engineering/design/ddd-architect.md +95 -0
- package/agents/engineering/discovery/agent-finder.md +214 -0
- package/agents/engineering/discovery/functional-discovery.md +182 -0
- package/agents/engineering/infra/infra-security.md +151 -0
- package/agents/engineering/infra/platform-strategist.md +109 -0
- package/agents/engineering/infra/terraform-architect.md +171 -0
- package/agents/engineering/prompt-engineer.md +37 -0
- package/agents/engineering/research/best-practices-researcher.md +115 -0
- package/agents/engineering/research/framework-docs-researcher.md +93 -0
- package/agents/engineering/research/git-history-analyzer.md +53 -0
- package/agents/engineering/research/learnings-researcher.md +285 -0
- package/agents/engineering/research/repo-research-analyst.md +118 -0
- package/agents/engineering/review/agent-native-reviewer.md +279 -0
- package/agents/engineering/review/architecture-strategist.md +61 -0
- package/agents/engineering/review/code-quality-analyst.md +92 -0
- package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
- package/agents/engineering/review/data-integrity-guardian.md +82 -0
- package/agents/engineering/review/data-migration-expert.md +98 -0
- package/agents/engineering/review/deployment-verification-agent.md +183 -0
- package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
- package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
- package/agents/engineering/review/legacy-code-expert.md +55 -0
- package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
- package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
- package/agents/engineering/review/performance-oracle.md +130 -0
- package/agents/engineering/review/security-sentinel.md +167 -0
- package/agents/engineering/review/semgrep-sast.md +36 -0
- package/agents/engineering/review/test-design-reviewer.md +150 -0
- package/agents/engineering/review/user-impact-reviewer.md +70 -0
- package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
- package/agents/finance/budget-analyst.md +29 -0
- package/agents/finance/cfo.md +44 -0
- package/agents/finance/financial-reporter.md +30 -0
- package/agents/finance/revenue-analyst.md +30 -0
- package/agents/legal/clo.md +115 -0
- package/agents/legal/legal-compliance-auditor.md +110 -0
- package/agents/legal/legal-document-generator.md +134 -0
- package/agents/marketing/analytics-analyst.md +25 -0
- package/agents/marketing/brand-architect.md +181 -0
- package/agents/marketing/cmo.md +76 -0
- package/agents/marketing/conversion-optimizer.md +19 -0
- package/agents/marketing/copywriter.md +20 -0
- package/agents/marketing/fact-checker.md +70 -0
- package/agents/marketing/growth-strategist.md +145 -0
- package/agents/marketing/paid-media-strategist.md +28 -0
- package/agents/marketing/pricing-strategist.md +17 -0
- package/agents/marketing/programmatic-seo-specialist.md +25 -0
- package/agents/marketing/retention-strategist.md +23 -0
- package/agents/marketing/seo-aeo-analyst.md +164 -0
- package/agents/operations/coo.md +50 -0
- package/agents/operations/ops-advisor.md +78 -0
- package/agents/operations/ops-provisioner.md +101 -0
- package/agents/operations/ops-research.md +52 -0
- package/agents/operations/service-automator.md +278 -0
- package/agents/product/business-validator.md +206 -0
- package/agents/product/competitive-intelligence.md +89 -0
- package/agents/product/cpo.md +63 -0
- package/agents/product/design/ux-design-lead.md +209 -0
- package/agents/product/spec-flow-analyzer.md +122 -0
- package/agents/sales/cro.md +44 -0
- package/agents/sales/deal-architect.md +29 -0
- package/agents/sales/outbound-strategist.md +29 -0
- package/agents/sales/pipeline-analyst.md +29 -0
- package/agents/support/cco.md +48 -0
- package/agents/support/community-manager.md +570 -0
- package/agents/support/ticket-triage.md +70 -0
- package/bin/soleur-opencode.js +29 -0
- package/commands/go.md +659 -0
- package/commands/help.md +273 -0
- package/commands/sync.md +977 -0
- package/commands/version.md +103 -0
- package/hooks/antigravity-hooks.json +84 -0
- package/hooks/antigravity-session-start.sh +15 -0
- package/hooks/bare-root-cwd-guard.sh +131 -0
- package/hooks/browser-snapshot-credential-guard.sh +200 -0
- package/hooks/codex-session-start.sh +9 -0
- package/hooks/compaction-state.sh +525 -0
- package/hooks/destructive-command-guard.sh +1387 -0
- package/hooks/devin-session-start.sh +90 -0
- package/hooks/ext-filter-detect.sh +171 -0
- package/hooks/hooks.json +133 -0
- package/hooks/lib/hook-tool-kind.sh +24 -0
- package/hooks/lib/log-rotation.sh +176 -0
- package/hooks/lib/shell-argv.pl +576 -0
- package/hooks/memory-backstop.sh +1114 -0
- package/hooks/operator-stage-approval.sh +391 -0
- package/hooks/stop-hook.sh +367 -0
- package/hooks/unkept-promise-hook.sh +383 -0
- package/hooks/version-banner.sh +127 -0
- package/hooks/welcome-hook.sh +50 -0
- package/install.js +108 -0
- package/lib/agent-registry.ts +154 -0
- package/lib/c4-canonical-cli.mjs +34 -0
- package/lib/c4-canonical.mjs +46 -0
- package/lib/c4-from-components.ts +554 -0
- package/lib/go-routing.ts +81 -0
- package/lib/grok-inspect-contract.ts +311 -0
- package/lib/harness-model-map.ts +101 -0
- package/lib/harness-parity.ts +726 -0
- package/lib/harness-tool-map.ts +180 -0
- package/lib/harness.ts +716 -0
- package/lib/heartbeat-live-reconcile.ts +1153 -0
- package/lib/heartbeat-manifest.ts +342 -0
- package/lib/kb-coverage.ts +252 -0
- package/lib/pr-merge-poll.ts +103 -0
- package/lib/workflow-fidelity.ts +388 -0
- package/opencode/INSTRUCTIONS.md +152 -0
- package/opencode/agents/engineering/cto.md +10 -0
- package/opencode/agents/engineering/design/ddd-architect.md +10 -0
- package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
- package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
- package/opencode/agents/engineering/infra/infra-security.md +10 -0
- package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
- package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
- package/opencode/agents/engineering/prompt-engineer.md +10 -0
- package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
- package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
- package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
- package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
- package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
- package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
- package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
- package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
- package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
- package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
- package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
- package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
- package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
- package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
- package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
- package/opencode/agents/engineering/review/performance-oracle.md +10 -0
- package/opencode/agents/engineering/review/security-sentinel.md +10 -0
- package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
- package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
- package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
- package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
- package/opencode/agents/finance/budget-analyst.md +10 -0
- package/opencode/agents/finance/cfo.md +10 -0
- package/opencode/agents/finance/financial-reporter.md +10 -0
- package/opencode/agents/finance/revenue-analyst.md +10 -0
- package/opencode/agents/legal/clo.md +10 -0
- package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
- package/opencode/agents/legal/legal-document-generator.md +10 -0
- package/opencode/agents/marketing/analytics-analyst.md +10 -0
- package/opencode/agents/marketing/brand-architect.md +10 -0
- package/opencode/agents/marketing/cmo.md +10 -0
- package/opencode/agents/marketing/conversion-optimizer.md +10 -0
- package/opencode/agents/marketing/copywriter.md +10 -0
- package/opencode/agents/marketing/fact-checker.md +10 -0
- package/opencode/agents/marketing/growth-strategist.md +10 -0
- package/opencode/agents/marketing/paid-media-strategist.md +10 -0
- package/opencode/agents/marketing/pricing-strategist.md +10 -0
- package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
- package/opencode/agents/marketing/retention-strategist.md +10 -0
- package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
- package/opencode/agents/operations/coo.md +10 -0
- package/opencode/agents/operations/ops-advisor.md +10 -0
- package/opencode/agents/operations/ops-provisioner.md +10 -0
- package/opencode/agents/operations/ops-research.md +10 -0
- package/opencode/agents/operations/service-automator.md +10 -0
- package/opencode/agents/product/business-validator.md +10 -0
- package/opencode/agents/product/competitive-intelligence.md +10 -0
- package/opencode/agents/product/cpo.md +10 -0
- package/opencode/agents/product/design/ux-design-lead.md +10 -0
- package/opencode/agents/product/spec-flow-analyzer.md +10 -0
- package/opencode/agents/sales/cro.md +10 -0
- package/opencode/agents/sales/deal-architect.md +10 -0
- package/opencode/agents/sales/outbound-strategist.md +10 -0
- package/opencode/agents/sales/pipeline-analyst.md +10 -0
- package/opencode/agents/support/cco.md +10 -0
- package/opencode/agents/support/community-manager.md +10 -0
- package/opencode/agents/support/ticket-triage.md +10 -0
- package/opencode/commands/go.md +10 -0
- package/opencode/commands/help.md +10 -0
- package/opencode/commands/sync.md +10 -0
- package/opencode/commands/version.md +11 -0
- package/opencode/opencode.json +8 -0
- package/opencode/plugin.ts +493 -0
- package/package.json +36 -4
- package/scripts/admin-merge-ready.sh +545 -0
- package/scripts/admin-merge-ready.test.sh +753 -0
- package/scripts/affected-tests.sh +908 -0
- package/scripts/affected-tests.test.sh +1797 -0
- package/scripts/alpha-metrics.sh +78 -0
- package/scripts/alpha-metrics.test.sh +115 -0
- package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
- package/scripts/audit-flag-flip.sh +107 -0
- package/scripts/check-red-on-main.sh +316 -0
- package/scripts/ci-head-verdict.sh +363 -0
- package/scripts/cloud-detect.sh +193 -0
- package/scripts/delegation-verify.sh +657 -0
- package/scripts/delegation-verify.test.sh +1424 -0
- package/scripts/deploy-arm.sh +475 -0
- package/scripts/domain-model-drift.sh +419 -0
- package/scripts/emit-decision.sh +144 -0
- package/scripts/emit-decision.test.sh +144 -0
- package/scripts/ext-filter-divergence-corpus.txt +15 -0
- package/scripts/ext-filter-divergence.sh +189 -0
- package/scripts/generate-c4-from-components.ts +442 -0
- package/scripts/grok-fidelity-gate.sh +68 -0
- package/scripts/grok-pre-push-gate.sh +232 -0
- package/scripts/harness-discovery-smoke.ts +906 -0
- package/scripts/harness-parity-census.ts +92 -0
- package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
- package/scripts/install-opencode-adapter.ts +520 -0
- package/scripts/lib/affected-tests/common.sh +365 -0
- package/scripts/lib/affected-tests/go.sh +157 -0
- package/scripts/lib/affected-tests/js.sh +188 -0
- package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
- package/scripts/lib/affected-tests/python.sh +185 -0
- package/scripts/lib/context-sanitize.sh +49 -0
- package/scripts/lib/domain-model-lib.sh +180 -0
- package/scripts/lib/ext-filter-grammar.tsv +34 -0
- package/scripts/lib/operator-script.sh +1236 -0
- package/scripts/lib/proc.sh +580 -0
- package/scripts/lib/session-state.sh +748 -0
- package/scripts/lib/tmp-classify.sh +843 -0
- package/scripts/lib/wrapper-boundary.sh +47 -0
- package/scripts/lint-ratchet.py +1547 -0
- package/scripts/monitor-pr-checks.sh +441 -0
- package/scripts/pipeline-tally.sh +756 -0
- package/scripts/pipeline-tally.test.sh +740 -0
- package/scripts/plugin-version.sh +528 -0
- package/scripts/precommit-guard.sh +113 -0
- package/scripts/precommit-guard.test.sh +254 -0
- package/scripts/reconcile-live-heartbeats.ts +705 -0
- package/scripts/render-c4-model.sh +217 -0
- package/scripts/resolve-git-root.sh +55 -0
- package/scripts/resolve-regenerable-conflicts.sh +363 -0
- package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
- package/scripts/ruff-requirements.txt +22 -0
- package/scripts/run-in-pid-namespace.sh +98 -0
- package/scripts/run-in-pid-namespace.test.sh +290 -0
- package/scripts/scan-ancestor-signal-helpers.py +556 -0
- package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
- package/scripts/setup-ralph-loop.sh +238 -0
- package/scripts/sync-cursor-name-map.ts +316 -0
- package/scripts/sync-grok-agent-compat.ts +188 -0
- package/scripts/sync-npm-package.ts +178 -0
- package/scripts/sync-opencode-adapter.ts +479 -0
- package/scripts/sync-pr-behind.sh +534 -0
- package/scripts/sync-pr-behind.test.sh +252 -0
- package/scripts/taste-profile-update.sh +180 -0
- package/scripts/taste-profile-update.test.sh +167 -0
- package/scripts/write-budget-marker.sh +23 -0
- package/scripts/write-kb-coverage.ts +113 -0
- package/skills/admin-ip-refresh/SKILL.md +81 -0
- package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
- package/skills/agent-browser/SKILL.md +698 -0
- package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
- package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
- package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
- package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
- package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
- package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
- package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
- package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
- package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
- package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
- package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
- package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
- package/skills/agent-native-architecture/SKILL.md +479 -0
- package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
- package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
- package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
- package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
- package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
- package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
- package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
- package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
- package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
- package/skills/agent-native-architecture/references/product-implications.md +478 -0
- package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
- package/skills/agent-native-architecture/references/self-modification.md +294 -0
- package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
- package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
- package/skills/agent-native-audit/SKILL.md +300 -0
- package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
- package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
- package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
- package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
- package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
- package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
- package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
- package/skills/architecture/SKILL.md +436 -0
- package/skills/architecture/references/adr-template.md +154 -0
- package/skills/architecture/references/likec4-reference.md +136 -0
- package/skills/architecture/references/nfr-reference.md +126 -0
- package/skills/archive-kb/SKILL.md +86 -0
- package/skills/archive-kb/scripts/archive-kb.sh +300 -0
- package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
- package/skills/atdd-developer/SKILL.md +65 -0
- package/skills/brainstorm/SKILL.md +701 -0
- package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
- package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
- package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
- package/skills/brainstorm/references/ui-surface-terms.md +42 -0
- package/skills/brainstorm-techniques/SKILL.md +231 -0
- package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
- package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
- package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
- package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
- package/skills/campaign-calendar/SKILL.md +145 -0
- package/skills/cf-token-scope/SKILL.md +164 -0
- package/skills/cf-token-scope/references/widen-playbook.md +114 -0
- package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
- package/skills/changelog/SKILL.md +142 -0
- package/skills/code-to-prd/SKILL.md +66 -0
- package/skills/code-to-prd/references/banner-template.md +12 -0
- package/skills/code-to-prd/references/prd-template.md +41 -0
- package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
- package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
- package/skills/code-to-prd/test/fixture/.env.example +7 -0
- package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
- package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
- package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
- package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
- package/skills/code-to-prd/test/fixture/package.json +6 -0
- package/skills/cohort-status/SKILL.md +62 -0
- package/skills/community/SKILL.md +210 -0
- package/skills/community/scripts/bsky-community.sh +644 -0
- package/skills/community/scripts/bsky-setup.sh +631 -0
- package/skills/community/scripts/community-router.sh +93 -0
- package/skills/community/scripts/discord-community.sh +487 -0
- package/skills/community/scripts/discord-setup.sh +784 -0
- package/skills/community/scripts/github-community.sh +706 -0
- package/skills/community/scripts/hn-community.sh +233 -0
- package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
- package/skills/community/scripts/linkedin-community.sh +750 -0
- package/skills/community/scripts/linkedin-setup.sh +758 -0
- package/skills/community/scripts/x-community.sh +890 -0
- package/skills/community/scripts/x-setup.sh +713 -0
- package/skills/community/test/community-argv.test.sh +2922 -0
- package/skills/community/test/github-community.test.sh +981 -0
- package/skills/competitive-analysis/SKILL.md +69 -0
- package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
- package/skills/compound/SKILL.md +673 -0
- package/skills/compound/scripts/token-efficiency-report.sh +334 -0
- package/skills/compound/test/phase-16.test.sh +379 -0
- package/skills/compound-capture/SKILL.md +791 -0
- package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
- package/skills/compound-capture/assets/resolution-template.md +116 -0
- package/skills/compound-capture/references/yaml-schema.md +67 -0
- package/skills/compound-capture/schema.yaml +181 -0
- package/skills/constraint-scaffold/SKILL.md +175 -0
- package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
- package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
- package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
- package/skills/constraint-scaffold/references/shared-runner.template +135 -0
- package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
- package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
- package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
- package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
- package/skills/constraint-scaffold/test/generator.test.sh +93 -0
- package/skills/constraint-scaffold/test/parity.test.sh +285 -0
- package/skills/content-writer/SKILL.md +242 -0
- package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
- package/skills/cron-delete/SKILL.md +56 -0
- package/skills/cron-list/SKILL.md +54 -0
- package/skills/deepen-plan/SKILL.md +937 -0
- package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
- package/skills/deploy/SKILL.md +115 -0
- package/skills/deploy/references/hetzner-setup.md +65 -0
- package/skills/deploy/scripts/deploy.sh +37 -0
- package/skills/deploy-docs/SKILL.md +95 -0
- package/skills/dhh-rails-style/SKILL.md +203 -0
- package/skills/dhh-rails-style/references/architecture.md +717 -0
- package/skills/dhh-rails-style/references/controllers.md +323 -0
- package/skills/dhh-rails-style/references/frontend.md +556 -0
- package/skills/dhh-rails-style/references/gems.md +316 -0
- package/skills/dhh-rails-style/references/models.md +388 -0
- package/skills/dhh-rails-style/references/testing.md +354 -0
- package/skills/discord-content/SKILL.md +126 -0
- package/skills/docs-site/SKILL.md +214 -0
- package/skills/drain-labeled-backlog/SKILL.md +203 -0
- package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
- package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
- package/skills/drain-prs/SKILL.md +142 -0
- package/skills/drain-prs/scripts/triage-prs.sh +192 -0
- package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
- package/skills/dspy-ruby/SKILL.md +634 -0
- package/skills/dspy-ruby/assets/config-template.rb +359 -0
- package/skills/dspy-ruby/assets/module-template.rb +326 -0
- package/skills/dspy-ruby/assets/signature-template.rb +143 -0
- package/skills/dspy-ruby/references/core-concepts.md +268 -0
- package/skills/dspy-ruby/references/optimization.md +623 -0
- package/skills/dspy-ruby/references/providers.md +335 -0
- package/skills/eval-harness/README.md +194 -0
- package/skills/eval-harness/SKILL.md +157 -0
- package/skills/eval-harness/enums/go-routes.json +13 -0
- package/skills/eval-harness/enums/incident-threshold.json +5 -0
- package/skills/eval-harness/enums/lane.json +5 -0
- package/skills/eval-harness/enums/tool-selection.json +20 -0
- package/skills/eval-harness/enums/triage-levels.json +5 -0
- package/skills/eval-harness/gated-skills.json +34 -0
- package/skills/eval-harness/models.generated.json +5 -0
- package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
- package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
- package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
- package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
- package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
- package/skills/eval-harness/prompts/go-baseline.txt +6 -0
- package/skills/eval-harness/prompts/go-skill.txt +21 -0
- package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
- package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
- package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
- package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
- package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
- package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
- package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
- package/skills/eval-harness/prompts/triage-skill.txt +10 -0
- package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
- package/skills/eval-harness/scripts/extract-block.cjs +87 -0
- package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
- package/skills/eval-harness/scripts/gen-models.sh +41 -0
- package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
- package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
- package/skills/eval-harness/scripts/parse-label.cjs +93 -0
- package/skills/eval-harness/scripts/verdict.cjs +132 -0
- package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
- package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
- package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
- package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
- package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
- package/skills/eval-harness/test/eval-gate.test.sh +132 -0
- package/skills/eval-harness/test/extract-block.test.sh +87 -0
- package/skills/eval-harness/test/gate-classification.test.sh +77 -0
- package/skills/eval-harness/test/gen-models.test.sh +75 -0
- package/skills/eval-harness/test/measure-classification.test.sh +74 -0
- package/skills/eval-harness/test/parse-label.test.sh +129 -0
- package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
- package/skills/eval-harness/test/verdict.test.sh +123 -0
- package/skills/every-style-editor/SKILL.md +142 -0
- package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
- package/skills/feature-tweet/SKILL.md +199 -0
- package/skills/feature-video/SKILL.md +412 -0
- package/skills/feature-video/scripts/check_deps.sh +281 -0
- package/skills/file-todos/SKILL.md +267 -0
- package/skills/file-todos/assets/todo-template.md +166 -0
- package/skills/fix-issue/SKILL.md +278 -0
- package/skills/fix-issue/references/agent-authored-exclusion.md +118 -0
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +96 -0
- package/skills/flag-bootstrap/SETUP.md +142 -0
- package/skills/flag-create/SKILL.md +145 -0
- package/skills/flag-create/scripts/create.sh +273 -0
- package/skills/flag-delete/SKILL.md +167 -0
- package/skills/flag-delete/scripts/delete.sh +288 -0
- package/skills/flag-list/SKILL.md +99 -0
- package/skills/flag-list/scripts/list.sh +275 -0
- package/skills/flag-set-role/SKILL.md +229 -0
- package/skills/flag-set-role/scripts/flip.sh +774 -0
- package/skills/frontend-anti-slop/SKILL.md +106 -0
- package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
- package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
- package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
- package/skills/frontend-design/SKILL.md +77 -0
- package/skills/gdpr-gate/NOTICE +136 -0
- package/skills/gdpr-gate/SKILL.md +341 -0
- package/skills/gdpr-gate/references/fields.md +160 -0
- package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
- package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
- package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
- package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
- package/skills/gdpr-gate/references/layers/frontend.md +249 -0
- package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
- package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
- package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
- package/skills/gdpr-gate/references/legal-consent.md +140 -0
- package/skills/gdpr-gate/references/non-negotiables.md +60 -0
- package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
- package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
- package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
- package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
- package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
- package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
- package/skills/gemini-imagegen/SKILL.md +283 -0
- package/skills/gemini-imagegen/requirements.txt +5 -0
- package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
- package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
- package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
- package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
- package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
- package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
- package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
- package/skills/git-worktree/SKILL.md +534 -0
- package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
- package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
- package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
- package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
- package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
- package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
- package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
- package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
- package/skills/go/SKILL.md +54 -0
- package/skills/growth/SKILL.md +164 -0
- package/skills/harvest-debt/SKILL.md +78 -0
- package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
- package/skills/heal-skill/SKILL.md +173 -0
- package/skills/help/SKILL.md +46 -0
- package/skills/incident/NOTICE +39 -0
- package/skills/incident/SKILL.md +395 -0
- package/skills/incident/scripts/dry-run.sh +448 -0
- package/skills/incident/scripts/redact-engine.py +412 -0
- package/skills/incident/scripts/redact-sentinel.sh +44 -0
- package/skills/incident/templates/pir.md +139 -0
- package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
- package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
- package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
- package/skills/incident/test/fixtures/positive-corpus.md +87 -0
- package/skills/incident/test/redact-sentinel.test.sh +1420 -0
- package/skills/invoice/SKILL.md +206 -0
- package/skills/kb-glossary/SKILL.md +71 -0
- package/skills/kb-glossary/references/glossary-format.md +111 -0
- package/skills/kb-glossary/references/rejected-request-register.md +115 -0
- package/skills/kb-search/SKILL.md +276 -0
- package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
- package/skills/legal-audit/SKILL.md +161 -0
- package/skills/legal-generate/NOTICE +242 -0
- package/skills/legal-generate/SKILL.md +244 -0
- package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
- package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
- package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
- package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
- package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
- package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
- package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
- package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
- package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
- package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
- package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
- package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
- package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
- package/skills/linear-fetch/SKILL.md +247 -0
- package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
- package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
- package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
- package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
- package/skills/linear-fetch/test/parity.test.sh +112 -0
- package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
- package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
- package/skills/merge-pr/SKILL.md +668 -0
- package/skills/model-launch-review/SKILL.md +255 -0
- package/skills/model-launch-review/scripts/audit-models.sh +501 -0
- package/skills/one-shot/SKILL.md +316 -0
- package/skills/one-shot/references/token-discipline.md +74 -0
- package/skills/operator-bootstrap/SKILL.md +412 -0
- package/skills/operator-bootstrap/template.sh +439 -0
- package/skills/operator-digest/SKILL.md +287 -0
- package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
- package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
- package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
- package/skills/operator-rephrase/SKILL.md +98 -0
- package/skills/pencil-setup/SKILL.md +201 -0
- package/skills/pencil-setup/scripts/check_deps.sh +556 -0
- package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
- package/skills/pencil-setup/scripts/package-lock.json +1172 -0
- package/skills/pencil-setup/scripts/package.json +14 -0
- package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
- package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
- package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
- package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
- package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
- package/skills/plan/SKILL.md +1140 -0
- package/skills/plan/references/differential-migration-testing.md +185 -0
- package/skills/plan/references/plan-community-discovery.md +42 -0
- package/skills/plan/references/plan-founder-check.md +137 -0
- package/skills/plan/references/plan-functional-overlap.md +25 -0
- package/skills/plan/references/plan-host-fanout.md +51 -0
- package/skills/plan/references/plan-issue-templates.md +779 -0
- package/skills/plan/references/plan-network-outage-checklist.md +131 -0
- package/skills/plan/references/plan-scope-check.md +126 -0
- package/skills/plan/references/plan-sharp-edges.md +325 -0
- package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
- package/skills/plan/references/webapp-cwv-observability.md +143 -0
- package/skills/plan-review/SKILL.md +91 -0
- package/skills/plan-review/lib/named-panel.mjs +58 -0
- package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
- package/skills/postmerge/SKILL.md +642 -0
- package/skills/postmerge/references/deploy-status-debugging.md +138 -0
- package/skills/postmerge/references/graceful-degradation.md +20 -0
- package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
- package/skills/preflight/SKILL.md +1553 -0
- package/skills/preflight/references/check-13-founder-check.md +286 -0
- package/skills/preflight/scripts/founder-check.py +1640 -0
- package/skills/preflight/scripts/parse-form-a.awk +107 -0
- package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
- package/skills/product-roadmap/SKILL.md +357 -0
- package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
- package/skills/provision-cloudflare/SKILL.md +67 -0
- package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
- package/skills/provision-doppler/SKILL.md +58 -0
- package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
- package/skills/provision-github/SKILL.md +59 -0
- package/skills/provision-github/scripts/provision-github.sh +300 -0
- package/skills/provision-hetzner/SKILL.md +76 -0
- package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
- package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
- package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
- package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
- package/skills/qa/SKILL.md +263 -0
- package/skills/qa/references/browser-preflight.md +22 -0
- package/skills/questionnaire-generate/SKILL.md +328 -0
- package/skills/questionnaire-generate/references/questionnaire.template +60 -0
- package/skills/rclone/SKILL.md +162 -0
- package/skills/rclone/scripts/check_setup.sh +63 -0
- package/skills/release-announce/SKILL.md +65 -0
- package/skills/release-docs/SKILL.md +92 -0
- package/skills/reproduce-bug/SKILL.md +358 -0
- package/skills/resolve-debt/SKILL.md +85 -0
- package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
- package/skills/resolve-parallel/SKILL.md +45 -0
- package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
- package/skills/resolve-pr-parallel/SKILL.md +68 -0
- package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
- package/skills/resolve-todo-parallel/SKILL.md +57 -0
- package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
- package/skills/review/SKILL.md +1621 -0
- package/skills/review/references/anti-slop-scanner-hook.md +40 -0
- package/skills/review/references/defect-classes-migrations.md +9 -0
- package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
- package/skills/review/references/review-e2e-testing.md +116 -0
- package/skills/review/references/review-todo-structure.md +165 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +95 -0
- package/skills/review/references/semgrep-custom-rules.yaml +174 -0
- package/skills/review/references/ultrathink-phases.md +98 -0
- package/skills/review/references/wfs.md +6 -0
- package/skills/review/scripts/emit-review-trailer.sh +499 -0
- package/skills/review/scripts/ensure-semgrep.sh +99 -0
- package/skills/review/scripts/fix-round-seats.sh +197 -0
- package/skills/review/test/emit-review-trailer.test.sh +387 -0
- package/skills/review/test/ensure-semgrep.test.sh +111 -0
- package/skills/review/test/fix-round-seats.test.sh +237 -0
- package/skills/review/workflows/README.md +147 -0
- package/skills/review/workflows/review.workflow.js +667 -0
- package/skills/schedule/SKILL.md +796 -0
- package/skills/seo-aeo/SKILL.md +139 -0
- package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
- package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
- package/skills/ship/SKILL.md +3022 -0
- package/skills/ship/references/adr-ordinal-collision.md +47 -0
- package/skills/ship/references/affected-gate-contract.md +115 -0
- package/skills/ship/references/ci-run-diagnosis.md +29 -0
- package/skills/ship/references/ci-workflow-authoring.md +68 -0
- package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
- package/skills/ship/references/cto-ruling-armed-push.md +5 -0
- package/skills/ship/references/decision-challenges-render.md +71 -0
- package/skills/ship/references/detached-battery-verdict.md +13 -0
- package/skills/ship/references/feature-tweet-draft.md +42 -0
- package/skills/ship/references/followthrough-issue-body-template.md +47 -0
- package/skills/ship/references/followthrough-stub-template.sh +92 -0
- package/skills/ship/references/merge-queue-dequeue.md +39 -0
- package/skills/ship/references/operator-holds.md +149 -0
- package/skills/ship/references/pipeline-tally-render.md +43 -0
- package/skills/ship/references/pr-overlap.md +153 -0
- package/skills/ship/references/queue-mode.md +7 -0
- package/skills/ship/references/ready-run-wait.md +36 -0
- package/skills/ship/references/red-on-main-quarantine.md +110 -0
- package/skills/ship/references/register-update-pr-pattern.md +88 -0
- package/skills/ship/references/settle-then-admin-merge.md +130 -0
- package/skills/ship/references/supabase-column-probe.md +13 -0
- package/skills/ship/scripts/auto-close-scan.sh +76 -0
- package/skills/ship/scripts/battery-owed.sh +403 -0
- package/skills/ship/scripts/hold-gate-eval.sh +410 -0
- package/skills/ship/scripts/net-issue-flow.sh +665 -0
- package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
- package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
- package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
- package/skills/skill-creator/SKILL.md +299 -0
- package/skills/skill-creator/references/api-security.md +248 -0
- package/skills/skill-creator/references/authoring-levers.md +110 -0
- package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
- package/skills/skill-creator/references/best-practices.md +431 -0
- package/skills/skill-creator/references/common-patterns.md +639 -0
- package/skills/skill-creator/references/core-principles.md +193 -0
- package/skills/skill-creator/references/executable-code.md +188 -0
- package/skills/skill-creator/references/iteration-and-testing.md +509 -0
- package/skills/skill-creator/references/official-spec.md +193 -0
- package/skills/skill-creator/references/recommended-structure.md +180 -0
- package/skills/skill-creator/references/skill-structure.md +396 -0
- package/skills/skill-creator/references/using-scripts.md +122 -0
- package/skills/skill-creator/references/using-templates.md +120 -0
- package/skills/skill-creator/references/workflows-and-validation.md +541 -0
- package/skills/skill-creator/scripts/init_skill.py +322 -0
- package/skills/skill-creator/scripts/package_skill.py +110 -0
- package/skills/skill-creator/scripts/quick_validate.py +64 -0
- package/skills/skill-creator/templates/router-skill.md +73 -0
- package/skills/skill-creator/templates/simple-skill.md +34 -0
- package/skills/skill-creator/workflows/add-reference.md +96 -0
- package/skills/skill-creator/workflows/add-script.md +104 -0
- package/skills/skill-creator/workflows/add-template.md +83 -0
- package/skills/skill-creator/workflows/add-workflow.md +128 -0
- package/skills/skill-creator/workflows/audit-skill.md +159 -0
- package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
- package/skills/skill-creator/workflows/create-new-skill.md +207 -0
- package/skills/skill-creator/workflows/get-guidance.md +134 -0
- package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
- package/skills/skill-creator/workflows/verify-skill.md +226 -0
- package/skills/skill-security-scan/SKILL.md +145 -0
- package/skills/skill-security-scan/references/disclaimer.md +21 -0
- package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
- package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
- package/skills/skill-security-scan/references/override-mechanism.md +171 -0
- package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
- package/skills/skill-security-scan/references/regex-patterns.md +136 -0
- package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
- package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
- package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
- package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
- package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
- package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
- package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
- package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
- package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
- package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
- package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
- package/skills/skill-security-scan/scripts/lib.sh +197 -0
- package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
- package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
- package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
- package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
- package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
- package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
- package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
- package/skills/social-distribute/SKILL.md +485 -0
- package/skills/spec-templates/SKILL.md +274 -0
- package/skills/sync/SKILL.md +49 -0
- package/skills/test-browser/SKILL.md +464 -0
- package/skills/test-browser/references/feature-map.md +161 -0
- package/skills/test-fix-loop/SKILL.md +159 -0
- package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
- package/skills/triage/SKILL.md +402 -0
- package/skills/trigger-cron/SKILL.md +136 -0
- package/skills/trigger-cron/scripts/trigger.sh +198 -0
- package/skills/user-set-role/SKILL.md +147 -0
- package/skills/user-set-role/scripts/set-role.sh +218 -0
- package/skills/user-story-writer/SKILL.md +77 -0
- package/skills/ux-audit/SKILL.md +251 -0
- package/skills/ux-audit/references/finding.schema.json +62 -0
- package/skills/ux-audit/references/route-list.yaml +95 -0
- package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
- package/skills/ux-audit/scripts/bot-signin.ts +135 -0
- package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
- package/skills/work/SKILL.md +1479 -0
- package/skills/work/references/end-of-work-resume.md +7 -0
- package/skills/work/references/hook-test-main-cwd.md +24 -0
- package/skills/work/references/key-principles.md +83 -0
- package/skills/work/references/property-testing.md +122 -0
- package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
- package/skills/work/references/work-agent-teams.md +97 -0
- package/skills/work/references/work-lifecycle-parallel.md +147 -0
- package/skills/work/references/work-lockfile-bumps.md +97 -0
- package/skills/work/references/work-scratch-sandboxes.md +212 -0
- package/skills/work/references/work-subagent-fanout.md +61 -0
- package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
- package/skills/xcode-test/SKILL.md +360 -0
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# operator-script.sh — shared primitives for Soleur-generated operator scripts.
|
|
3
|
+
#
|
|
4
|
+
# SOURCED, NEVER EXECUTED. The shebang is the line-1 convention of every sibling
|
|
5
|
+
# in this directory (`proc.sh`, `session-state.sh`, `domain-model-lib.sh`) and
|
|
6
|
+
# is what lets shellcheck infer the dialect; the file carries no exec bit. There
|
|
7
|
+
# is ONE distribution mode (the generated script `source`s this file), so there
|
|
8
|
+
# is no inlined copy to drift from and no `STAGES` byte identity marker to pin.
|
|
9
|
+
# Plan 2026-09-18-feat-ship-operator-bootstrap-wizard-merge-danger, revision R6.
|
|
10
|
+
#
|
|
11
|
+
# Home per ADR-178 §1: a shared bash primitive consumed by shipped plugin code
|
|
12
|
+
# lives inside `plugins/soleur/`, alongside `proc.sh`, `session-state.sh` and
|
|
13
|
+
# `domain-model-lib.sh`.
|
|
14
|
+
#
|
|
15
|
+
# API CONTRACT: this file exports `SOLEUR_OP_LIB_API=1`. Every consumer asserts
|
|
16
|
+
# `[[ ${SOLEUR_OP_LIB_API:-0} -eq 1 ]]` right after its `source` line and exits
|
|
17
|
+
# 64 with `SOLEUR_BOOTSTRAP_LIB_INCOMPATIBLE need=1 got=<n>` otherwise. EQUALITY,
|
|
18
|
+
# not `-ge`: the library auto-updates with the plugin while a generated script
|
|
19
|
+
# is frozen in the founder's repository, so "library newer than script" is the
|
|
20
|
+
# only incompatibility that can occur — and a `-ge` gate admits exactly that
|
|
21
|
+
# case, then dies mid-stage on a renamed helper (measured by the review lead).
|
|
22
|
+
# Bump the number only on a change that breaks an existing consumer (a renamed
|
|
23
|
+
# helper, a changed positional contract), never on an additive one.
|
|
24
|
+
#
|
|
25
|
+
# PORTABILITY: bash 3.2 (the /bin/bash macOS ships), the floor `proc.sh` in this
|
|
26
|
+
# directory declares. So: no negative array subscripts (`${a[-1]}` is 4.3+), no
|
|
27
|
+
# `${var^^}` (4.0+), no `mapfile`, no `declare -A`. `readlink -f` may be absent
|
|
28
|
+
# on older macOS — every use falls back to the unresolved path.
|
|
29
|
+
#
|
|
30
|
+
# Sourced (NEVER executed) by:
|
|
31
|
+
# - plugins/soleur/skills/operator-bootstrap/template.sh
|
|
32
|
+
# the template every generated <feature>/bootstrap.sh is authored from
|
|
33
|
+
# - plugins/soleur/skills/provision-hetzner/scripts/provision-hetzner.sh
|
|
34
|
+
# the proving consumer (plan D11)
|
|
35
|
+
# - every script the `soleur:operator-bootstrap` skill generates
|
|
36
|
+
#
|
|
37
|
+
# SOURCING PRECONDITIONS — the caller satisfies all of these BEFORE the
|
|
38
|
+
# `source` line (the shape of apps/cla-evidence/scripts/_cf-admin-token.sh):
|
|
39
|
+
# 1. `set -euo pipefail` is already in effect. This file sets no shell options
|
|
40
|
+
# of its own except `umask`, because a sourced library that changes the
|
|
41
|
+
# caller's error handling changes the caller's control flow.
|
|
42
|
+
# 2. The caller carries its OWN xtrace-refusal prologue ABOVE the `source`
|
|
43
|
+
# line. It is DUPLICATED there, never moved in here: `PROLOGUE_MAX_CMDS = 0`
|
|
44
|
+
# in scripts/lint-shell-trace-credential-refusal.py makes a `source` line
|
|
45
|
+
# itself a counted command, and `find_preamble` only ever scans a file's
|
|
46
|
+
# OWN lines — a caller sourcing a fully compliant library still fails Rule
|
|
47
|
+
# A (measured, revision R26). The caller does NOT strip SSL_CERT_FILE /
|
|
48
|
+
# SSL_CERT_DIR / CURL_CA_BUNDLE: `gh` and `hcloud` are Go clients that read
|
|
49
|
+
# those for their root CA pool, and a founder behind a TLS-inspecting proxy
|
|
50
|
+
# needs them (the linter requires only the xtrace refusal — measured).
|
|
51
|
+
# 3. `grep`, `sed`, `date`, `dirname`, `mkdir`, `mv`, `chmod`, `mktemp`, `rm`,
|
|
52
|
+
# `readlink`, `printenv`, `od`, `id`, `stat`, `cut`, `tr` and one of
|
|
53
|
+
# `sha256sum` / `shasum` are on PATH — the binaries this file actually
|
|
54
|
+
# calls, and only those. `gh` is required only by the two GitHub helpers.
|
|
55
|
+
#
|
|
56
|
+
# LIBRARY INVARIANT (revision R26) — THIS FILE NEVER EXPANDS A SECRET-SHAPED
|
|
57
|
+
# VARIABLE NAME. The credential linter's `^scripts/lib/` exclusion is
|
|
58
|
+
# repo-root-anchored, so a library under `plugins/soleur/scripts/lib/` IS
|
|
59
|
+
# scanned. If this file ever expanded a `*_TOKEN` / `*_KEY` / `*_SECRET` /
|
|
60
|
+
# `*_PASSWORD` / `*_PAT` name — or used `${!name}` indirection — it would come
|
|
61
|
+
# into scope and need its own `exit 78`, which on a `source` terminates the
|
|
62
|
+
# CALLER. So: secret values arrive as POSITIONAL PARAMETERS bound to neutral
|
|
63
|
+
# local names (`value`), skip variables are read with `printenv` rather than
|
|
64
|
+
# `${!name}`, and every expansion of a caller-named credential stays in the
|
|
65
|
+
# caller.
|
|
66
|
+
#
|
|
67
|
+
# EXIT CODES a generated script may return (revision R42 — the repo has no
|
|
68
|
+
# central table and code 3 is already overloaded, so the table lives HERE ONLY;
|
|
69
|
+
# operator-bootstrap/SKILL.md and template.sh point at this section rather than
|
|
70
|
+
# restating it):
|
|
71
|
+
#
|
|
72
|
+
# 0 success.
|
|
73
|
+
# 1 usage error; a refused call — e.g. a secret-shaped name handed to the
|
|
74
|
+
# GitHub *variable* helper, which writes on argv (remedy: fix the call); or
|
|
75
|
+
# the operator DECLINED a barrier or a destructive-write ack (the
|
|
76
|
+
# `SOLEUR_BOOTSTRAP_ABORTED stage=<kind>` marker names which).
|
|
77
|
+
# 3 DPA-gate rejection (tenant provisioning scripts only). CONFLICT, stated
|
|
78
|
+
# rather than renumbered: apps/cla-evidence/scripts/sentinel-pr.sh returns
|
|
79
|
+
# 3 for "missing tool on PATH" and "not inside a git repository" — a class
|
|
80
|
+
# apps/cla-evidence/infra/bootstrap.sh returns 64 for. Do NOT re-derive 3
|
|
81
|
+
# as "missing tool".
|
|
82
|
+
# 64 missing input — a required binary, a required environment variable, a
|
|
83
|
+
# missing `--stage`, or a prompt that cannot be answered because stdin is
|
|
84
|
+
# not a TTY. Remedy: the sentence under the marker names it. For a
|
|
85
|
+
# class-1/class-3 prompt, set the named variable. The legacy class-2
|
|
86
|
+
# destructive-write ack (`soleur_op_ack_or_die`, scripts not yet moved to
|
|
87
|
+
# the staged contract) has NO variable to set and needs a typed `yes` at a
|
|
88
|
+
# terminal; a v2 generated script never calls it.
|
|
89
|
+
# 75 approval required or not valid for a staged WRITE (EX_TEMPFAIL, ADR-264):
|
|
90
|
+
# no approval was presented, the receipt was expired / used / for another
|
|
91
|
+
# command, or the plan changed between the plan and the apply. Nothing was
|
|
92
|
+
# written. The marker's `reason=` and the sentence under it say what to do;
|
|
93
|
+
# the agent re-plans, re-issues the command, or stops and reports.
|
|
94
|
+
# 78 refusing to run under shell tracing while holding a live credential
|
|
95
|
+
# (#7797). Remedy: re-run without `-x`.
|
|
96
|
+
#
|
|
97
|
+
# Exit 1 has a FOURTH meaning under the caller's `set -e`: a `soleur_op_gh_*`
|
|
98
|
+
# helper that `return 1`s (write failed, verify failed, unsafe name) ends the
|
|
99
|
+
# caller with status 1 unless the call is guarded. The marker line above the
|
|
100
|
+
# exit says which.
|
|
101
|
+
#
|
|
102
|
+
# STDOUT MARKERS, not stderr. Agent runtimes surface stdout and swallow stderr,
|
|
103
|
+
# so a stderr-only refusal is invisible on the one surface that matters
|
|
104
|
+
# (provision-doppler.sh records the same reason at its own prologue). On a
|
|
105
|
+
# hosted agent surface (a cloud session, a CI step) these markers are the ONLY
|
|
106
|
+
# durable signal besides the ledger: grep for `SOLEUR_BOOTSTRAP_` in the step
|
|
107
|
+
# log. Every refusal is the marker PLUS one plain sentence for the founder.
|
|
108
|
+
# SOLEUR_BOOTSTRAP_INPUT_REQUIRED var=<NAME> tty=0 → exit 64
|
|
109
|
+
# SOLEUR_BOOTSTRAP_MISSING_BINARY bin=<bin> → exit 64
|
|
110
|
+
# SOLEUR_BOOTSTRAP_ABORTED stage=<barrier|ack> → operator declined, exit 1
|
|
111
|
+
# SOLEUR_BOOTSTRAP_BAD_ARG key=<KEY> [reason=<r>] → refused .env write, return 1
|
|
112
|
+
# helper=<h> reason=<r> (mktemp failed)
|
|
113
|
+
# SOLEUR_BOOTSTRAP_ENV_READ_FAILED path=<path> → grep exit 2 on the .env; no mv, return 1
|
|
114
|
+
# SOLEUR_BOOTSTRAP_ENV_KEYCOUNT_DROP before=<n> after=<m> path=<path>
|
|
115
|
+
# → filter lost keys; no mv, return 1
|
|
116
|
+
# SOLEUR_BOOTSTRAP_UNSAFE_VARIABLE name=<NAME> reason=secret-shaped-name-on-argv
|
|
117
|
+
# → refused argv write, return 1
|
|
118
|
+
# SOLEUR_BOOTSTRAP_SECRET_WRITE_FAILED name=<NAME> repo=<repo> → return 1
|
|
119
|
+
# SOLEUR_BOOTSTRAP_SECRET_VERIFY_FAILED name=<NAME> repo=<repo> → return 1 (write may have
|
|
120
|
+
# landed; also sets SOLEUR_OP_WRITE_MAY_HAVE_LANDED=1 for the trap)
|
|
121
|
+
# SOLEUR_BOOTSTRAP_VARIABLE_WRITE_FAILED name=<NAME> repo=<repo> → return 1
|
|
122
|
+
# SOLEUR_BOOTSTRAP_LEDGER_WRITE_FAILED path=<path> → non-fatal; the run
|
|
123
|
+
# continues, the record is lost
|
|
124
|
+
# SOLEUR_BOOTSTRAP_LIB_MISSING path=<last-rejected> → exit 64
|
|
125
|
+
# SOLEUR_BOOTSTRAP_LIB_INCOMPATIBLE need=1 got=<n> → exit 64
|
|
126
|
+
# SOLEUR_BOOTSTRAP_ENV_NOT_IGNORED path=<path> → exit 64 (template.sh)
|
|
127
|
+
#
|
|
128
|
+
# STAGED CONTRACT markers (ADR-264; every v2 generated script: --list, --stage
|
|
129
|
+
# <name>, --stage <name> --apply --plan-digest <d> [--rotate-token]). One line
|
|
130
|
+
# each, names and booleans only — never a value and never a hash of one:
|
|
131
|
+
# SOLEUR_BOOTSTRAP_STAGE_DECL stage=<s> class=<read|write> (--list)
|
|
132
|
+
# SOLEUR_BOOTSTRAP_PLAN stage=<s> operations=<n> digest=<hex>
|
|
133
|
+
# SOLEUR_BOOTSTRAP_PLAN_OP stage=<s> op=<name> target=<name>
|
|
134
|
+
# SOLEUR_BOOTSTRAP_IMPACT / _ROLLBACK stage=<s> text=<one plain sentence>
|
|
135
|
+
# SOLEUR_BOOTSTRAP_APPLY_COMMAND stage=<s> approval_digest=<hex> + the exact command
|
|
136
|
+
# SOLEUR_BOOTSTRAP_APPROVAL_REQUIRED stage=<s> approval_digest=<hex> surface=<s> → exit 75
|
|
137
|
+
# SOLEUR_BOOTSTRAP_APPROVAL_INVALID stage=<s> reason=<no-record|expired|digest-mismatch|
|
|
138
|
+
# consumed|algo|perms|format> → exit 75
|
|
139
|
+
# SOLEUR_BOOTSTRAP_PLAN_DRIFT stage=<s> → exit 75, receipt burned
|
|
140
|
+
# SOLEUR_BOOTSTRAP_PRECONDITION_FAILED stage=<s> need=<stage> → exit 1 (settles as refused;
|
|
141
|
+
# run the `need` stage, then this one again)
|
|
142
|
+
# SOLEUR_BOOTSTRAP_PATH_UNSUPPORTED stage=<s> → exit 64 (script path has a ')
|
|
143
|
+
# SOLEUR_BOOTSTRAP_STAGE_REQUIRED (no marker args) → exit 64 (no --stage: no run-all mode)
|
|
144
|
+
# SOLEUR_BOOTSTRAP_UNKNOWN_STAGE stage=<s> → exit 64 (not in the stage table)
|
|
145
|
+
# SOLEUR_BOOTSTRAP_PLAN_DIGEST_REQUIRED stage=<s> → exit 64 (--apply without the digest)
|
|
146
|
+
# SOLEUR_BOOTSTRAP_STAGE_OK stage=<s> changed=<0|1> [approval=<harness-receipt|tty-ack>]
|
|
147
|
+
# SOLEUR_BOOTSTRAP_STAGE_FAILED stage=<s> rc=<n>
|
|
148
|
+
# SOLEUR_BOOTSTRAP_LIB_INCOMPATIBLE need=stage-1 got=<n> → exit 64 (consumer)
|
|
149
|
+
|
|
150
|
+
# SOLEUR_BOOTSTRAP_LIB_MISSING, _LIB_INCOMPATIBLE and _ENV_NOT_IGNORED are
|
|
151
|
+
# emitted by the CONSUMER, not by this file — a library that is not there cannot
|
|
152
|
+
# announce itself. The canonical shape of that resolution (env override →
|
|
153
|
+
# CLAUDE_PLUGIN_ROOT → generation-time baked path → hard exit 64, never a stub;
|
|
154
|
+
# ADR-178 Context §1 records what fail-closed stubs did to `cleanup-merged`) is
|
|
155
|
+
# the "library resolution" section of
|
|
156
|
+
# plugins/soleur/skills/operator-bootstrap/template.sh.
|
|
157
|
+
#
|
|
158
|
+
# <!-- Inspired by mattpocock/skills/skills/engineering/wizard/ (MIT, Copyright (c) 2026 Matt Pocock). -->
|
|
159
|
+
# What is adopted is the SHAPE — a wizard that walks named stages, one journey
|
|
160
|
+
# per stage, with the operator told what will happen before it happens. The code
|
|
161
|
+
# is extracted from in-repo prior art, not ported: five of the six primitives
|
|
162
|
+
# already existed here (apps/cla-evidence/infra/bootstrap.sh for stage progress,
|
|
163
|
+
# preflight and the closing summary; community/scripts/{x,bsky,discord}-setup.sh
|
|
164
|
+
# for the exact-key `.env` upsert; operator-digest/scripts/
|
|
165
|
+
# provision-operator-digest-repo.sh for the stdin-only secret write and the
|
|
166
|
+
# separate argv variable write). Only cross-platform URL opening is new.
|
|
167
|
+
|
|
168
|
+
# Guard against double-source within a single shell.
|
|
169
|
+
if [[ "${_SOLEUR_OPERATOR_SCRIPT_LOADED:-}" == "1" ]]; then
|
|
170
|
+
return 0 2>/dev/null || true
|
|
171
|
+
fi
|
|
172
|
+
_SOLEUR_OPERATOR_SCRIPT_LOADED=1
|
|
173
|
+
|
|
174
|
+
# The class-2 ack's in-process result (#8486, ADR-249). Cleared at load so an
|
|
175
|
+
# inherited or exported value can never stand in for an ack that did not happen
|
|
176
|
+
# here; soleur_op_ack_or_die assigns it (never exports it) after a typed `yes`,
|
|
177
|
+
# and plugins/soleur/scripts/audit-flag-flip.sh refuses to append without it.
|
|
178
|
+
unset SOLEUR_OP_ACKED
|
|
179
|
+
|
|
180
|
+
# The API contract every consumer asserts after its `source` line (header §API).
|
|
181
|
+
export SOLEUR_OP_LIB_API=1
|
|
182
|
+
|
|
183
|
+
# The generated `.env` holds live credentials on the founder's own disk, and a
|
|
184
|
+
# default umask writes it 0644 into a directory that may sit under a cloud-sync
|
|
185
|
+
# client. 077 here, and `chmod 600` asserted after every upsert.
|
|
186
|
+
umask 077
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Approval receipts — the algorithm shared by the approval HOOK and this library
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
#
|
|
192
|
+
# The hook (plugins/soleur/hooks/operator-stage-approval.sh) SOURCES this file for
|
|
193
|
+
# `soleur_approval_*`, so MINT and VERIFY share ONE digest algorithm in ONE file
|
|
194
|
+
# and one diff moves both. (An earlier draft kept this in a sibling file; every
|
|
195
|
+
# harness that copies the library to a sandbox then lost it. One file is the
|
|
196
|
+
# distribution contract the rest of this header already states.) The independent
|
|
197
|
+
# anchor is the known-answer vector set in plugins/soleur/test/operator-script.test.sh,
|
|
198
|
+
# computed with python3 hashlib.
|
|
199
|
+
#
|
|
200
|
+
# WHAT A RECEIPT IS. When the human approves the exact command the agent is about
|
|
201
|
+
# to run, the harness hook has already minted a one-time record on the human's
|
|
202
|
+
# machine, and the nonce that unlocks it rides INSIDE the approved command line
|
|
203
|
+
# (an environment prefix the hook rewrote in). The script recomputes the digest of
|
|
204
|
+
# its own invocation, finds the record named sha256(nonce), and consumes it
|
|
205
|
+
# (rename) BEFORE the first write. An environment variable or a flag the agent sets
|
|
206
|
+
# by itself is therefore never an approval: the nonce is a LOOKUP KEY into a record
|
|
207
|
+
# the hook wrote, and any value with no matching record is exit 75.
|
|
208
|
+
#
|
|
209
|
+
# HONEST THRESHOLD (ADR-264, binding): the record is a file the same OS user can
|
|
210
|
+
# write. This resists an instruction-following agent that follows skill text and
|
|
211
|
+
# exit markers; it does NOT resist a hijacked agent that deliberately forges a
|
|
212
|
+
# record. Closing that needs a principal the agent cannot write as (credential
|
|
213
|
+
# custody, ADR-249 step 2). Never describe this mechanism as stronger.
|
|
214
|
+
#
|
|
215
|
+
# A failure in this section is a `return`, never an `exit`: the consumer owns the
|
|
216
|
+
# exit code (a library exit kills the caller).
|
|
217
|
+
|
|
218
|
+
# The record format version. A record or digest carrying any other value is
|
|
219
|
+
# refused with reason=algo, never as a generic failure (version skew is a
|
|
220
|
+
# distinct, reportable condition).
|
|
221
|
+
SOLEUR_APPROVAL_ALGO=1
|
|
222
|
+
# A record is valid for five minutes. Long enough for a human to read the prompt,
|
|
223
|
+
# short enough that a record the human denied is dead soon. A new mint for the same
|
|
224
|
+
# session also removes that session's earlier unconsumed records (soleur_approval_mint).
|
|
225
|
+
SOLEUR_APPROVAL_TTL_SECONDS=300
|
|
226
|
+
|
|
227
|
+
# The presented nonce is taken OUT of the environment the moment this file is loaded
|
|
228
|
+
# and held in a plain (never exported) shell variable. A child process the script
|
|
229
|
+
# starts later (doppler, gh, curl, a stage helper) therefore never inherits it, and
|
|
230
|
+
# the only function that ever reads it is soleur_approval_consume / _burn. The hook
|
|
231
|
+
# sources this file too and presents no nonce, so it holds the empty string there.
|
|
232
|
+
unset _SOLEUR_APPROVAL_NONCE_HELD
|
|
233
|
+
_SOLEUR_APPROVAL_NONCE_HELD="${SOLEUR_APPROVAL_NONCE:-}"
|
|
234
|
+
unset SOLEUR_APPROVAL_NONCE
|
|
235
|
+
|
|
236
|
+
# soleur_approval_sha256 — stdin to a lowercase hex digest on stdout.
|
|
237
|
+
soleur_approval_sha256() {
|
|
238
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
239
|
+
sha256sum | cut -d' ' -f1
|
|
240
|
+
else
|
|
241
|
+
shasum -a 256 | cut -d' ' -f1
|
|
242
|
+
fi
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# soleur_approval_dir — where records live: outside the worktree, per user.
|
|
246
|
+
soleur_approval_dir() {
|
|
247
|
+
printf '%s' "${XDG_STATE_HOME:-${HOME:-/nonexistent}/.local/state}/soleur/approvals"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# soleur_approval_realpath <path> — the resolved absolute path of an existing
|
|
251
|
+
# file, falling back to the unresolved absolute path (readlink -f may be absent
|
|
252
|
+
# on older macOS).
|
|
253
|
+
soleur_approval_realpath() {
|
|
254
|
+
local p="$1" d
|
|
255
|
+
readlink -f -- "$p" 2>/dev/null && return 0
|
|
256
|
+
d="$(cd "$(dirname "$p")" 2>/dev/null && pwd -P)" || { printf '%s' "$p"; return 0; }
|
|
257
|
+
printf '%s/%s' "$d" "$(basename "$p")"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# soleur_approval_content_hash <file> — sha256 of the file's bytes, or the fixed
|
|
261
|
+
# word `unreadable`. A receipt is bound to the script's CONTENT as well as its
|
|
262
|
+
# path: a script edited between the human's approval and the run no longer matches.
|
|
263
|
+
soleur_approval_content_hash() {
|
|
264
|
+
local h
|
|
265
|
+
h="$(soleur_approval_sha256 < "$1" 2>/dev/null)" || h=""
|
|
266
|
+
[[ "$h" =~ ^[0-9a-f]{64}$ ]] && printf '%s' "$h" || printf 'unreadable'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# soleur_approval_binding_digest <script-realpath> <stage> <arg>...
|
|
270
|
+
# The digest an approval is bound to: the resolved script, its content hash, the
|
|
271
|
+
# stage and every argument EXACTLY as the shell handed it over. Every variable
|
|
272
|
+
# field is written as `<byte-length>:<value>` so no value, whatever characters it
|
|
273
|
+
# carries (a newline included), can read as the next field. The hook computes it
|
|
274
|
+
# from the command string it parsed; the script recomputes it from "$@". The two
|
|
275
|
+
# are equal only for the same invocation of the same script bytes.
|
|
276
|
+
soleur_approval_binding_digest() {
|
|
277
|
+
local real="$1" stage="$2" a content
|
|
278
|
+
shift 2
|
|
279
|
+
content="$(soleur_approval_content_hash "$real")"
|
|
280
|
+
{
|
|
281
|
+
local LC_ALL=C
|
|
282
|
+
printf 'algo=%s\n' "$SOLEUR_APPROVAL_ALGO"
|
|
283
|
+
printf 'script=%d:%s\n' "${#real}" "$real"
|
|
284
|
+
printf 'content=%s\n' "$content"
|
|
285
|
+
printf 'stage=%d:%s\n' "${#stage}" "$stage"
|
|
286
|
+
for a in "$@"; do printf 'arg=%d:%s\n' "${#a}" "$a"; done
|
|
287
|
+
} | soleur_approval_sha256
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# soleur_approval_nonce_ok <value> — a 32-hex-digit (128-bit) nonce.
|
|
291
|
+
soleur_approval_nonce_ok() {
|
|
292
|
+
[[ "${1:-}" =~ ^[0-9a-f]{32}$ ]]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# soleur_approval_new_nonce — 128 random bits as 32 hex digits.
|
|
296
|
+
soleur_approval_new_nonce() {
|
|
297
|
+
od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n'
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# _soleur_approval_stat <path> — "<uid> <octal-mode>" or empty.
|
|
301
|
+
_soleur_approval_stat() {
|
|
302
|
+
stat -c '%u %a' -- "$1" 2>/dev/null || stat -f '%u %Lp' -- "$1" 2>/dev/null || true
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# _soleur_approval_dir_ok <dir> — a real directory (never a symlink), owned by
|
|
306
|
+
# this euid, mode 0700.
|
|
307
|
+
_soleur_approval_dir_ok() {
|
|
308
|
+
local d="$1" st
|
|
309
|
+
[[ -d "$d" && ! -L "$d" ]] || return 1
|
|
310
|
+
st="$(_soleur_approval_stat "$d")"
|
|
311
|
+
[[ "$st" == "$(id -u) 700" ]]
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# _soleur_approval_file_ok <file> — a regular file (never a symlink), owned by
|
|
315
|
+
# this euid, mode 0600.
|
|
316
|
+
_soleur_approval_file_ok() {
|
|
317
|
+
local f="$1" st
|
|
318
|
+
[[ -f "$f" && ! -L "$f" ]] || return 1
|
|
319
|
+
st="$(_soleur_approval_stat "$f")"
|
|
320
|
+
[[ "$st" == "$(id -u) 600" ]]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# _soleur_approval_housekeeping <dir> <session>
|
|
324
|
+
# Runs on every mint. (1) Removes every record, consumed or not, older than an
|
|
325
|
+
# hour: the directory never grows without bound and a stale record is not left
|
|
326
|
+
# for a later session to find. (2) Removes this session's earlier UNCONSUMED
|
|
327
|
+
# records: only the newest receipt of a session is live, so a prompt the person
|
|
328
|
+
# declined cannot be revived by a later one. Best effort; a failure here never
|
|
329
|
+
# blocks a mint.
|
|
330
|
+
_soleur_approval_housekeeping() {
|
|
331
|
+
local dir="$1" session="$2"
|
|
332
|
+
# find, not a glob: the approval hook runs with pathname expansion off (`set -f`).
|
|
333
|
+
find "$dir" -maxdepth 1 -type f -mmin +60 -delete 2>/dev/null || true
|
|
334
|
+
find "$dir" -maxdepth 1 -type f ! -name '*.consumed' ! -name '.mint.*' \
|
|
335
|
+
-exec grep -aqx "session=${session}" {} \; -delete 2>/dev/null || true
|
|
336
|
+
return 0
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# soleur_approval_mint <binding-digest> <session-id>
|
|
340
|
+
# Writes one record and prints the nonce on stdout (the ONLY place it is ever
|
|
341
|
+
# printed; the caller embeds it in the rewritten command and nowhere else).
|
|
342
|
+
# The record is named sha256(nonce) so a directory listing never reveals a live
|
|
343
|
+
# nonce. Returns 1 (prints nothing) if the directory cannot be made safe.
|
|
344
|
+
soleur_approval_mint() {
|
|
345
|
+
local digest="$1" session="${2:-unknown}" dir nonce name tmp expires
|
|
346
|
+
dir="$(soleur_approval_dir)"
|
|
347
|
+
if [[ ! -e "$dir" ]]; then
|
|
348
|
+
(umask 077; mkdir -p "$dir") 2>/dev/null || return 1
|
|
349
|
+
chmod 700 "$dir" 2>/dev/null || return 1
|
|
350
|
+
fi
|
|
351
|
+
_soleur_approval_dir_ok "$dir" || return 1
|
|
352
|
+
nonce="$(soleur_approval_new_nonce)"
|
|
353
|
+
soleur_approval_nonce_ok "$nonce" || return 1
|
|
354
|
+
name="$(printf '%s' "$nonce" | soleur_approval_sha256)"
|
|
355
|
+
expires=$(( $(date +%s) + SOLEUR_APPROVAL_TTL_SECONDS ))
|
|
356
|
+
_soleur_approval_housekeeping "$dir" "${session//[^A-Za-z0-9._-]/_}"
|
|
357
|
+
tmp="${dir}/.mint.$$.${name:0:12}"
|
|
358
|
+
(
|
|
359
|
+
umask 077
|
|
360
|
+
printf 'algo=%s\ndigest=%s\nexpires=%s\nsession=%s\n' \
|
|
361
|
+
"$SOLEUR_APPROVAL_ALGO" "$digest" "$expires" "${session//[^A-Za-z0-9._-]/_}" > "$tmp"
|
|
362
|
+
) 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
363
|
+
chmod 600 "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
364
|
+
mv -- "$tmp" "${dir}/${name}" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
365
|
+
printf '%s' "$nonce"
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# SOLEUR_APPROVAL_REASON
|
|
369
|
+
# The consume functions report WHY through this variable (never stdout: stdout
|
|
370
|
+
# of a library function is often captured). One of: no-record expired
|
|
371
|
+
# digest-mismatch consumed algo perms format. Empty means "no nonce was
|
|
372
|
+
# presented" (a different outcome: approval REQUIRED, not INVALID).
|
|
373
|
+
SOLEUR_APPROVAL_REASON=""
|
|
374
|
+
SOLEUR_APPROVAL_NONCE_SHA12=""
|
|
375
|
+
|
|
376
|
+
# soleur_approval_consume <binding-digest>
|
|
377
|
+
# 0 — a valid unexpired record for exactly this digest existed and has been
|
|
378
|
+
# renamed to <name>.consumed (single use; the rename IS the consumption and
|
|
379
|
+
# happens BEFORE the caller's first write).
|
|
380
|
+
# 1 — nothing was consumed; SOLEUR_APPROVAL_REASON says why ("" = no nonce).
|
|
381
|
+
soleur_approval_consume() {
|
|
382
|
+
local digest="$1" nonce dir name rec algo rdigest expires now
|
|
383
|
+
SOLEUR_APPROVAL_REASON=""
|
|
384
|
+
SOLEUR_APPROVAL_NONCE_SHA12=""
|
|
385
|
+
nonce="${_SOLEUR_APPROVAL_NONCE_HELD:-}"
|
|
386
|
+
[[ -n "$nonce" ]] || return 1
|
|
387
|
+
if ! soleur_approval_nonce_ok "$nonce"; then SOLEUR_APPROVAL_REASON=format; return 1; fi
|
|
388
|
+
dir="$(soleur_approval_dir)"
|
|
389
|
+
# No directory at all means the hook never minted anything here: no record. A
|
|
390
|
+
# directory that exists but is a symlink, foreign-owned or not 0700 is a tamper
|
|
391
|
+
# class: perms.
|
|
392
|
+
[[ -e "$dir" || -L "$dir" ]] || { SOLEUR_APPROVAL_REASON=no-record; return 1; }
|
|
393
|
+
_soleur_approval_dir_ok "$dir" || { SOLEUR_APPROVAL_REASON=perms; return 1; }
|
|
394
|
+
name="$(printf '%s' "$nonce" | soleur_approval_sha256)"
|
|
395
|
+
SOLEUR_APPROVAL_NONCE_SHA12="${name:0:12}"
|
|
396
|
+
rec="${dir}/${name}"
|
|
397
|
+
if [[ ! -e "$rec" && ! -L "$rec" ]]; then
|
|
398
|
+
if [[ -e "${rec}.consumed" ]]; then SOLEUR_APPROVAL_REASON=consumed; else SOLEUR_APPROVAL_REASON=no-record; fi
|
|
399
|
+
return 1
|
|
400
|
+
fi
|
|
401
|
+
_soleur_approval_file_ok "$rec" || { SOLEUR_APPROVAL_REASON=perms; return 1; }
|
|
402
|
+
# Field extraction without a `read` loop (the prompt census in Guard 4 treats every
|
|
403
|
+
# `read` in this file as a prompt, and these are not).
|
|
404
|
+
algo="$(grep -a '^algo=' "$rec" | head -n 1 | cut -d= -f2-)"
|
|
405
|
+
rdigest="$(grep -a '^digest=' "$rec" | head -n 1 | cut -d= -f2-)"
|
|
406
|
+
expires="$(grep -a '^expires=' "$rec" | head -n 1 | cut -d= -f2-)"
|
|
407
|
+
[[ "$algo" == "$SOLEUR_APPROVAL_ALGO" ]] || { SOLEUR_APPROVAL_REASON=algo; return 1; }
|
|
408
|
+
[[ "$rdigest" =~ ^[0-9a-f]{64}$ && "$expires" =~ ^[0-9]+$ ]] || { SOLEUR_APPROVAL_REASON=format; return 1; }
|
|
409
|
+
[[ "$rdigest" == "$digest" ]] || { SOLEUR_APPROVAL_REASON=digest-mismatch; return 1; }
|
|
410
|
+
now="$(date +%s)"
|
|
411
|
+
(( expires > now )) || { SOLEUR_APPROVAL_REASON=expired; return 1; }
|
|
412
|
+
# Single use. A lost rename race (two applies, one record) fails here and the
|
|
413
|
+
# loser writes nothing.
|
|
414
|
+
if ! mv -- "$rec" "${rec}.consumed" 2>/dev/null; then
|
|
415
|
+
# The record is gone: another run won the rename. Still there: the rename itself
|
|
416
|
+
# failed (permissions, a full disk) and that is a different, reportable condition.
|
|
417
|
+
if [[ -e "$rec" ]]; then SOLEUR_APPROVAL_REASON=perms; else SOLEUR_APPROVAL_REASON=consumed; fi
|
|
418
|
+
return 1
|
|
419
|
+
fi
|
|
420
|
+
_SOLEUR_APPROVAL_NONCE_HELD=""
|
|
421
|
+
return 0
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# soleur_approval_burn
|
|
425
|
+
# Consume whatever record the presented nonce points at, valid or not: a plan
|
|
426
|
+
# that drifted between plan and apply must not leave a live receipt behind.
|
|
427
|
+
soleur_approval_burn() {
|
|
428
|
+
local nonce dir name rec
|
|
429
|
+
nonce="${_SOLEUR_APPROVAL_NONCE_HELD:-}"
|
|
430
|
+
soleur_approval_nonce_ok "$nonce" || return 0
|
|
431
|
+
dir="$(soleur_approval_dir)"
|
|
432
|
+
_soleur_approval_dir_ok "$dir" || return 0
|
|
433
|
+
name="$(printf '%s' "$nonce" | soleur_approval_sha256)"
|
|
434
|
+
rec="${dir}/${name}"
|
|
435
|
+
[[ -f "$rec" && ! -L "$rec" ]] || return 0
|
|
436
|
+
mv -- "$rec" "${rec}.consumed" 2>/dev/null || true
|
|
437
|
+
_SOLEUR_APPROVAL_NONCE_HELD=""
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
# Output helpers (shape from apps/cla-evidence/infra/bootstrap.sh)
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
SOLEUR_OP_GREEN='\033[32m'
|
|
445
|
+
SOLEUR_OP_RED='\033[31m'
|
|
446
|
+
SOLEUR_OP_YELLOW='\033[33m'
|
|
447
|
+
SOLEUR_OP_NC='\033[0m'
|
|
448
|
+
|
|
449
|
+
soleur_op_red() { printf '%b%s%b\n' "$SOLEUR_OP_RED" "$*" "$SOLEUR_OP_NC" >&2; }
|
|
450
|
+
soleur_op_green() { printf '%b%s%b\n' "$SOLEUR_OP_GREEN" "$*" "$SOLEUR_OP_NC"; }
|
|
451
|
+
soleur_op_yellow() { printf '%b%s%b\n' "$SOLEUR_OP_YELLOW" "$*" "$SOLEUR_OP_NC"; }
|
|
452
|
+
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
# Preflight
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
# soleur_op_require_bins <bin>...
|
|
458
|
+
# Exit 64 naming the first missing binary. The ladder (environment → Doppler →
|
|
459
|
+
# MCP/CLI/REST) runs BEFORE any prompt; a value outside the two interactive
|
|
460
|
+
# carve-outs that is missing is a hard failure with a named remedy, never a
|
|
461
|
+
# prompt.
|
|
462
|
+
# STDOUT marker plus one plain sentence: agent runtimes surface stdout and
|
|
463
|
+
# swallow stderr, and a founder reads the sentence, not the marker.
|
|
464
|
+
soleur_op_require_bins() {
|
|
465
|
+
local bin
|
|
466
|
+
for bin in "$@"; do
|
|
467
|
+
command -v "$bin" >/dev/null 2>&1 || {
|
|
468
|
+
printf 'SOLEUR_BOOTSTRAP_MISSING_BINARY bin=%s\n' "$bin"
|
|
469
|
+
printf 'Install %s first, then run again.\n' "$bin"
|
|
470
|
+
exit 64
|
|
471
|
+
}
|
|
472
|
+
done
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
# ---------------------------------------------------------------------------
|
|
476
|
+
# Run ledger — one JSON line per stage, BEFORE and AFTER. Names, never values.
|
|
477
|
+
# ---------------------------------------------------------------------------
|
|
478
|
+
#
|
|
479
|
+
# Layer 7 (cli-stdout-artifact): stdout alone is an explicit P1 rejection because
|
|
480
|
+
# it does not survive the session, so the synchronous markers above are paired
|
|
481
|
+
# with this durable artifact. ONE convention: the ledger sits BESIDE the script
|
|
482
|
+
# that writes it, as `bootstrap-runs.jsonl` — for a generated script that is
|
|
483
|
+
# `knowledge-base/project/specs/feat-<name>/`, tracked in the FOUNDER's
|
|
484
|
+
# repository, which is where the layer's "committed to the customer's own
|
|
485
|
+
# repository" condition is met. Nothing is transmitted and no alert target
|
|
486
|
+
# exists — the surface is the founder's own machine, and routing it to Soleur
|
|
487
|
+
# infrastructure would be a data-controller event, not an observability
|
|
488
|
+
# improvement.
|
|
489
|
+
#
|
|
490
|
+
# A write that fails (disk full, unwritable directory) is NON-FATAL — a
|
|
491
|
+
# provisioning stage must not abort because its audit line could not be
|
|
492
|
+
# appended — but it is never silent: `SOLEUR_BOOTSTRAP_LEDGER_WRITE_FAILED
|
|
493
|
+
# path=<path>` goes to stdout so the founder knows the artifact is incomplete.
|
|
494
|
+
|
|
495
|
+
# `$$` until soleur_op_ledger_init assigns the timestamped id: a helper that
|
|
496
|
+
# writes a ledger line before init (the `--reset` path does) must still carry a
|
|
497
|
+
# non-empty run_id, or the artifact has lines no run can be attributed to.
|
|
498
|
+
SOLEUR_OP_RUN_ID="$$"
|
|
499
|
+
SOLEUR_OP_TOTAL_STAGES=0
|
|
500
|
+
# Resolved ONCE (first use or soleur_op_ledger_init) and cached: the default is
|
|
501
|
+
# derived from the bottom of BASH_SOURCE, which may be relative, and a `cd` inside a stage
|
|
502
|
+
# would otherwise split one run's lines across two files.
|
|
503
|
+
SOLEUR_OP_LEDGER_FILE=""
|
|
504
|
+
|
|
505
|
+
# Default: beside the MAIN script (the bottom of the source stack), never a
|
|
506
|
+
# cwd-relative dotdir. `SOLEUR_BOOTSTRAP_LEDGER` overrides.
|
|
507
|
+
soleur_op_ledger_path() {
|
|
508
|
+
local main_script main_dir
|
|
509
|
+
if [[ -n "$SOLEUR_OP_LEDGER_FILE" ]]; then
|
|
510
|
+
printf '%s' "$SOLEUR_OP_LEDGER_FILE"
|
|
511
|
+
return 0
|
|
512
|
+
fi
|
|
513
|
+
if [[ -n "${SOLEUR_BOOTSTRAP_LEDGER:-}" ]]; then
|
|
514
|
+
SOLEUR_OP_LEDGER_FILE="$SOLEUR_BOOTSTRAP_LEDGER"
|
|
515
|
+
else
|
|
516
|
+
# `${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}`, not `[-1]`: negative subscripts
|
|
517
|
+
# are bash 4.3+ and the 3.2 that macOS ships dies here with "bad array
|
|
518
|
+
# subscript" — fatal under set -e, stderr only, no marker.
|
|
519
|
+
main_script="${BASH_SOURCE[${#BASH_SOURCE[@]}-1]:-}"
|
|
520
|
+
# Sourced with no main script (an interactive shell, `bash -c`): the bottom of
|
|
521
|
+
# the stack is this file, and "beside the library" is not a ledger home.
|
|
522
|
+
if [[ -z "$main_script" || "$main_script" == "${BASH_SOURCE[0]}" ]]; then
|
|
523
|
+
SOLEUR_OP_LEDGER_FILE="$(pwd)/bootstrap-runs.jsonl"
|
|
524
|
+
else
|
|
525
|
+
main_dir="$(cd "$(dirname "$main_script")" 2>/dev/null && pwd)" || main_dir="$(dirname "$main_script")"
|
|
526
|
+
SOLEUR_OP_LEDGER_FILE="${main_dir}/bootstrap-runs.jsonl"
|
|
527
|
+
fi
|
|
528
|
+
fi
|
|
529
|
+
printf '%s' "$SOLEUR_OP_LEDGER_FILE"
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
soleur_op_now() { date -u '+%Y-%m-%dT%H:%M:%SZ'; }
|
|
533
|
+
|
|
534
|
+
# JSON string escaping for the field set below (names and small integers by
|
|
535
|
+
# construction): newline and carriage return become a space FIRST, because the
|
|
536
|
+
# sed that follows is line-oriented and would otherwise pass a second line
|
|
537
|
+
# through unescaped; U+2028/U+2029 are stripped (JSON allows them raw, but a
|
|
538
|
+
# JS-hosted reader treats them as line terminators); backslash and double quote
|
|
539
|
+
# are escaped; remaining control characters collapse to a space.
|
|
540
|
+
soleur_op_json_escape() {
|
|
541
|
+
local s="$1"
|
|
542
|
+
s="${s//$'\n'/ }"
|
|
543
|
+
s="${s//$'\r'/ }"
|
|
544
|
+
s="${s//$'\xe2\x80\xa8'/}"
|
|
545
|
+
s="${s//$'\xe2\x80\xa9'/}"
|
|
546
|
+
printf '%s' "$s" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/[[:cntrl:]]/ /g'
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# soleur_op_json_int <value>
|
|
550
|
+
# A bare JSON number, or 0 when the argument is not an integer — an unquoted
|
|
551
|
+
# non-number would make the whole line unparseable.
|
|
552
|
+
soleur_op_json_int() {
|
|
553
|
+
if [[ "${1:-}" =~ ^-?[0-9]+$ ]]; then printf '%s' "$1"; else printf '0'; fi
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
soleur_op_ledger_write() {
|
|
557
|
+
local path line
|
|
558
|
+
path="$(soleur_op_ledger_path)"
|
|
559
|
+
line="$1"
|
|
560
|
+
mkdir -p "$(dirname "$path")" 2>/dev/null || true
|
|
561
|
+
if ! printf '%s\n' "$line" >> "$path" 2>/dev/null; then
|
|
562
|
+
printf 'SOLEUR_BOOTSTRAP_LEDGER_WRITE_FAILED path=%s\n' "$path"
|
|
563
|
+
fi
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
# soleur_op_ledger_init <total-stages> <script-name>
|
|
567
|
+
soleur_op_ledger_init() {
|
|
568
|
+
SOLEUR_OP_TOTAL_STAGES="$(soleur_op_json_int "${1:-}")"
|
|
569
|
+
SOLEUR_OP_RUN_ID="$(date -u '+%Y%m%dT%H%M%SZ')-$$"
|
|
570
|
+
soleur_op_ledger_path >/dev/null
|
|
571
|
+
soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"run_begin","total_stages":%s,"script":"%s"}' \
|
|
572
|
+
"$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$SOLEUR_OP_TOTAL_STAGES" "$(soleur_op_json_escape "${2:-unknown}")")"
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
# soleur_op_ledger_note <kind> <name> <detail>
|
|
576
|
+
# `name` is a KEY NAME or a SECRET NAME. `detail` is metadata — never a value.
|
|
577
|
+
# Guard 3 drives a mutation that swaps a value into `detail` and asserts the
|
|
578
|
+
# suite reddens, because "names never values" is otherwise a comment.
|
|
579
|
+
soleur_op_ledger_note() {
|
|
580
|
+
soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","kind":"%s","name":"%s","detail":"%s"}' \
|
|
581
|
+
"$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" \
|
|
582
|
+
"$(soleur_op_json_escape "$1")" "$(soleur_op_json_escape "$2")" "$(soleur_op_json_escape "${3:-}")")"
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# soleur_op_stage_begin <index> <name>
|
|
586
|
+
soleur_op_stage_begin() {
|
|
587
|
+
printf '\n→ [%s/%s] %s\n' "$1" "$SOLEUR_OP_TOTAL_STAGES" "$2"
|
|
588
|
+
soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"stage","phase":"begin","stage_index":%s,"total_stages":%s,"stage_name":"%s","outcome":"pending","exit_code":null}' \
|
|
589
|
+
"$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$(soleur_op_json_int "$1")" "$(soleur_op_json_int "$SOLEUR_OP_TOTAL_STAGES")" "$(soleur_op_json_escape "$2")")"
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
# soleur_op_stage_end <index> <name> <outcome> <exit-code>
|
|
593
|
+
soleur_op_stage_end() {
|
|
594
|
+
soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"stage","phase":"settle","stage_index":%s,"total_stages":%s,"stage_name":"%s","outcome":"%s","exit_code":%s}' \
|
|
595
|
+
"$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$(soleur_op_json_int "$1")" "$(soleur_op_json_int "$SOLEUR_OP_TOTAL_STAGES")" \
|
|
596
|
+
"$(soleur_op_json_escape "$2")" "$(soleur_op_json_escape "$3")" "$(soleur_op_json_int "${4:-0}")")"
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
# There is deliberately NO resume index. Every stage opens with an "already
|
|
600
|
+
# satisfied?" precondition (operator-bootstrap/SKILL.md §2), so re-running from
|
|
601
|
+
# stage 1 IS resume; a start-stage variable would be a second mechanism for the
|
|
602
|
+
# same property, and one the precondition already makes redundant.
|
|
603
|
+
|
|
604
|
+
# ---------------------------------------------------------------------------
|
|
605
|
+
# Prompts — R8's THREE carve-out classes
|
|
606
|
+
# ---------------------------------------------------------------------------
|
|
607
|
+
#
|
|
608
|
+
# class 1 NON-SECRET ladder value (a region, an account id, a repo slug)
|
|
609
|
+
# → named skip variable; no TTY + unset ⇒ exit 64 naming it.
|
|
610
|
+
# Credential ENTRY stays in the CALLER with its own `read -rs`
|
|
611
|
+
# behind the same gate — see provision-hetzner.sh. This helper
|
|
612
|
+
# echoes its input and must never take a secret.
|
|
613
|
+
# class 2 per-command destructive-write acknowledgement
|
|
614
|
+
# → NO SKIP VARIABLE AT ALL. No TTY ⇒ exit 64 unconditionally.
|
|
615
|
+
# An environment variable set once is exactly the "prior approval
|
|
616
|
+
# extending to a new command" hr-menu-option-ack-not-prod-write-auth
|
|
617
|
+
# forbids, so automation must not be able to supply this.
|
|
618
|
+
# class 3 out-of-band completion barrier ("Token created? Type 'yes'")
|
|
619
|
+
# → named skip variable, AND the caller MUST follow it with an
|
|
620
|
+
# independent verification of the thing attested. A barrier that is
|
|
621
|
+
# skippable and unverified attests nothing.
|
|
622
|
+
#
|
|
623
|
+
# A fourth class requires an ADR amendment (ADR-228).
|
|
624
|
+
|
|
625
|
+
# --- MUTATION ANCHOR: start of prompt helpers ---
|
|
626
|
+
|
|
627
|
+
# soleur_op_skip_value <VAR-NAME>
|
|
628
|
+
# `printenv`, deliberately, NOT `${!name}`: indirect expansion would put this
|
|
629
|
+
# file into the credential linter's scope (library invariant, above).
|
|
630
|
+
soleur_op_skip_value() {
|
|
631
|
+
printenv "$1" 2>/dev/null || true
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
# soleur_op_run_halt <reason> <name>
|
|
635
|
+
# The ledger's TERMINAL line for a run that stops before its stages settle:
|
|
636
|
+
# `reason` is `input_required` or `aborted`, `name` the variable or the stage
|
|
637
|
+
# kind. Without it the ledger could only ever say "ok" (review P2-12).
|
|
638
|
+
soleur_op_run_halt() {
|
|
639
|
+
soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"run_halt","reason":"%s","var":"%s"}' \
|
|
640
|
+
"$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$(soleur_op_json_escape "$1")" "$(soleur_op_json_escape "$2")")"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# soleur_op_input_required <VAR-NAME-or-reason> [ack]
|
|
644
|
+
# The no-TTY refusal. Emitted BEFORE any read, never after. Today's behaviour
|
|
645
|
+
# in provision-hetzner.sh is fail-closed but MUTE — EOF read into an empty ACK
|
|
646
|
+
# and `exit 1` with "Aborted." — the right outcome with an unattributed cause.
|
|
647
|
+
# The marker is for the agent; the sentence after it is for the founder. The
|
|
648
|
+
# second argument `ack` selects the class-2 sentence: there is no variable to
|
|
649
|
+
# set, so the only remedy is a person at a terminal.
|
|
650
|
+
soleur_op_input_required() {
|
|
651
|
+
printf 'SOLEUR_BOOTSTRAP_INPUT_REQUIRED var=%s tty=0\n' "$1"
|
|
652
|
+
if [[ "${2:-}" == "ack" ]]; then
|
|
653
|
+
# Legacy class-2 ack only (a script not yet moved to the staged contract):
|
|
654
|
+
# the approval is a typed yes at a terminal and nothing set by an agent can
|
|
655
|
+
# stand in for it. A v2 generated script never reaches this line.
|
|
656
|
+
printf 'This older script can only be approved by a person typing yes at a terminal; no setting can answer it for you. Regenerate it with the operator-bootstrap skill to get the staged form, where the approval happens at the Claude Code prompt on the exact command.\n'
|
|
657
|
+
else
|
|
658
|
+
printf 'This step needs one value. Set %s and run again; the agent can set it for you if you give it the value (a non-secret one) or point it at where the value already lives.\n' "$1"
|
|
659
|
+
fi
|
|
660
|
+
soleur_op_run_halt input_required "$1"
|
|
661
|
+
exit 64
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# soleur_op_aborted <kind>
|
|
665
|
+
# The operator declined. Marker, plain sentence, ledger line, exit 1.
|
|
666
|
+
soleur_op_aborted() {
|
|
667
|
+
printf 'SOLEUR_BOOTSTRAP_ABORTED stage=%s\n' "$1"
|
|
668
|
+
printf 'Stopped. Nothing was created.\n'
|
|
669
|
+
soleur_op_run_halt aborted "$1"
|
|
670
|
+
exit 1
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# soleur_op_value <SKIP-VAR> <prompt> <out-var-name> [class 1]
|
|
674
|
+
# Writes the answer into the named variable with `printf -v` rather than to
|
|
675
|
+
# stdout. Deliberate: a `$(...)` capture would run the helper in a SUBSHELL,
|
|
676
|
+
# where `exit 64` kills only the subshell and the script sails on past a
|
|
677
|
+
# prompt it never answered.
|
|
678
|
+
soleur_op_value() {
|
|
679
|
+
local var_name="$1" prompt_text="$2" out_name="$3" skip reply
|
|
680
|
+
skip="$(soleur_op_skip_value "$var_name")"
|
|
681
|
+
if [[ -n "$skip" ]]; then
|
|
682
|
+
printf -v "$out_name" '%s' "$skip"
|
|
683
|
+
return 0
|
|
684
|
+
fi
|
|
685
|
+
[[ -t 0 ]] || soleur_op_input_required "$var_name"
|
|
686
|
+
read -r -p "$prompt_text" reply
|
|
687
|
+
printf -v "$out_name" '%s' "$reply"
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
# soleur_op_barrier <SKIP-VAR> <prompt> [class 3]
|
|
691
|
+
# MUST be followed by an independent verification of the thing attested.
|
|
692
|
+
soleur_op_barrier() {
|
|
693
|
+
local var_name="$1" prompt_text="$2" skip reply
|
|
694
|
+
skip="$(soleur_op_skip_value "$var_name")"
|
|
695
|
+
if [[ -n "$skip" ]]; then
|
|
696
|
+
printf ' [skip] %s (%s is set; the caller still runs its own verification)\n' "$prompt_text" "$var_name"
|
|
697
|
+
return 0
|
|
698
|
+
fi
|
|
699
|
+
[[ -t 0 ]] || soleur_op_input_required "$var_name"
|
|
700
|
+
read -r -p "$prompt_text" reply
|
|
701
|
+
[[ "$reply" == "yes" ]] || soleur_op_aborted barrier
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
# soleur_op_in_agent_harness
|
|
705
|
+
# True when this process runs under an agent harness (Claude Code sets CLAUDECODE
|
|
706
|
+
# and CLAUDE_CODE_ENTRYPOINT in every tool call's environment). A typed `yes` on a
|
|
707
|
+
# pseudo-terminal is accepted only when this is false. HONEST LIMIT: an agent that
|
|
708
|
+
# deliberately unsets these names and opens a pty defeats it; that is the
|
|
709
|
+
# hijacked-agent case the receipt threshold already excludes (ADR-264).
|
|
710
|
+
soleur_op_in_agent_harness() {
|
|
711
|
+
[[ -n "${CLAUDECODE:-}" || -n "${CLAUDE_CODE_ENTRYPOINT:-}" ]]
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
# soleur_op_ack_or_die <prompt> [class 2]
|
|
715
|
+
# NO skip variable, by design and by rule. Do not add one.
|
|
716
|
+
soleur_op_ack_or_die() {
|
|
717
|
+
local prompt_text="$1" reply
|
|
718
|
+
if soleur_op_in_agent_harness; then
|
|
719
|
+
soleur_op_input_required "destructive-write-ack(agent-harness-no-typed-ack)" ack
|
|
720
|
+
fi
|
|
721
|
+
[[ -t 0 ]] || soleur_op_input_required "destructive-write-ack(no-skip-variable-by-design)" ack
|
|
722
|
+
read -r -p "$prompt_text" reply
|
|
723
|
+
[[ "$reply" == "yes" ]] || soleur_op_aborted ack
|
|
724
|
+
SOLEUR_OP_ACKED=tty-ack
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
# --- MUTATION ANCHOR: end of prompt helpers ---
|
|
728
|
+
|
|
729
|
+
# ---------------------------------------------------------------------------
|
|
730
|
+
# Staged contract — agent-run stages, harness-approved writes (ADR-264)
|
|
731
|
+
# ---------------------------------------------------------------------------
|
|
732
|
+
#
|
|
733
|
+
# A v2 generated script is driven ONE STAGE AT A TIME by an agent with no TTY:
|
|
734
|
+
#
|
|
735
|
+
# bash bootstrap.sh --list
|
|
736
|
+
# bash bootstrap.sh --stage <name> read stage: runs; write stage: prints the PLAN
|
|
737
|
+
# bash bootstrap.sh --stage <name> --apply --plan-digest <d> write stage: performs the write
|
|
738
|
+
#
|
|
739
|
+
# Every write needs a human acknowledgement of the EXACT command. It moves out of
|
|
740
|
+
# the terminal into the harness approval channel: the approval hook minted a
|
|
741
|
+
# one-time receipt when the human approved the command, and the nonce for it rode
|
|
742
|
+
# inside that command (the "Approval receipts" section above). This library never trusts a value
|
|
743
|
+
# the caller supplies about the approval: the plan is RECOMPUTED, the digest is
|
|
744
|
+
# RECOMPUTED, the nonce is only a lookup key into a record the hook wrote.
|
|
745
|
+
#
|
|
746
|
+
# THE ORDER INSIDE soleur_op_stage_gate IS THE CONTRACT (each step is a mutation
|
|
747
|
+
# row of Guard 11): recompute the plan and compare the digest (drift burns the
|
|
748
|
+
# receipt) -> validate the nonce against its record -> CONSUME the record (rename)
|
|
749
|
+
# -> unset the nonce so no child inherits it -> only then may the caller write.
|
|
750
|
+
# Otherwise a real TTY typed `yes` is the second valid source; otherwise exit 75.
|
|
751
|
+
#
|
|
752
|
+
# NO MARKER INSIDE A `$(...)` CAPTURE: a marker printed by a function whose stdout
|
|
753
|
+
# is captured becomes the captured value. Every function below that prints is
|
|
754
|
+
# called directly, never as `x=$(fn)`.
|
|
755
|
+
|
|
756
|
+
SOLEUR_OP_PLAN_OPS=()
|
|
757
|
+
SOLEUR_OP_APPROVAL=""
|
|
758
|
+
# Set by soleur_op_stage_gate immediately before it exits 75. The EXIT trap of a
|
|
759
|
+
# generated script settles the stage as `refused` on THIS flag, never on the bare
|
|
760
|
+
# exit code: a vendor CLI that happens to exit 75 mid-write must not be reported as
|
|
761
|
+
# a clean refusal.
|
|
762
|
+
SOLEUR_OP_REFUSED=""
|
|
763
|
+
|
|
764
|
+
# soleur_op_shquote <word> — the word as one shell-safe token (raw when it is
|
|
765
|
+
# plain, single-quoted otherwise). Returns 1 and prints nothing for a word that
|
|
766
|
+
# contains a single quote: no quoting of it survives the approval hook's parser, so
|
|
767
|
+
# the caller refuses (soleur_op_plan_emit) instead of printing a command the hook
|
|
768
|
+
# cannot ask about.
|
|
769
|
+
soleur_op_shquote() {
|
|
770
|
+
if [[ "$1" =~ ^[A-Za-z0-9_./:=@+,-]+$ ]]; then
|
|
771
|
+
printf '%s' "$1"
|
|
772
|
+
elif [[ "$1" == *"'"* ]]; then
|
|
773
|
+
return 1
|
|
774
|
+
else
|
|
775
|
+
printf "'%s'" "$1"
|
|
776
|
+
fi
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
# soleur_op_stage_lines <script>
|
|
780
|
+
# The script's own declarations, one per line: name|class|impact|rollback. The
|
|
781
|
+
# SAME lines are read by the approval hook, so there is one source of truth.
|
|
782
|
+
soleur_op_stage_lines() {
|
|
783
|
+
grep -a '^# SOLEUR-STAGE ' "$1" 2>/dev/null | sed 's/^# SOLEUR-STAGE //'
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
# soleur_op_stage_field <script> <stage> <1=name 2=class 3=impact 4=rollback>
|
|
787
|
+
# Empty (return 1) when the stage is not declared.
|
|
788
|
+
soleur_op_stage_field() {
|
|
789
|
+
soleur_op_stage_lines "$1" | awk -F'|' -v s="$2" -v f="$3" '$1 == s && !found { print $f; found = 1 } END { exit !found }'
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
# soleur_op_list <script> — the machine-readable stage table.
|
|
793
|
+
soleur_op_list() {
|
|
794
|
+
soleur_op_stage_lines "$1" | awk -F'|' '{ printf "SOLEUR_BOOTSTRAP_STAGE_DECL stage=%s class=%s\n", $1, $2 }'
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
# soleur_op_plan_begin / soleur_op_plan_op <op-name> <target-name>
|
|
798
|
+
# A plan is a list of operation NAMES and the container/slug/secret NAMES they
|
|
799
|
+
# touch — never a value. The digest covers exactly this list.
|
|
800
|
+
soleur_op_plan_begin() { SOLEUR_OP_PLAN_OPS=(); }
|
|
801
|
+
soleur_op_plan_op() { SOLEUR_OP_PLAN_OPS[${#SOLEUR_OP_PLAN_OPS[@]}]="$1|${2:-}"; }
|
|
802
|
+
soleur_op_plan_count() { printf '%s' "${#SOLEUR_OP_PLAN_OPS[@]}"; }
|
|
803
|
+
|
|
804
|
+
# soleur_op_plan_digest <script-real> <stage> <flags> <impact> <rollback>
|
|
805
|
+
# Deterministic digest of what an apply will do and what the human was told.
|
|
806
|
+
# The impact and rollback TEXT is inside it, so the words the human read are
|
|
807
|
+
# the words that were approved.
|
|
808
|
+
soleur_op_plan_digest() {
|
|
809
|
+
local op content
|
|
810
|
+
content="$(soleur_approval_content_hash "$1")"
|
|
811
|
+
{
|
|
812
|
+
local LC_ALL=C
|
|
813
|
+
printf 'algo=%s\n' "$SOLEUR_APPROVAL_ALGO"
|
|
814
|
+
printf 'script=%d:%s\ncontent=%s\n' "${#1}" "$1" "$content"
|
|
815
|
+
printf 'stage=%d:%s\nflags=%d:%s\n' "${#2}" "$2" "${#3}" "$3"
|
|
816
|
+
printf 'impact=%d:%s\nrollback=%d:%s\n' "${#4}" "$4" "${#5}" "$5"
|
|
817
|
+
for op in ${SOLEUR_OP_PLAN_OPS[@]+"${SOLEUR_OP_PLAN_OPS[@]}"}; do printf 'op=%d:%s\n' "${#op}" "$op"; done
|
|
818
|
+
} | soleur_approval_sha256
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
# soleur_op_plan_emit <script-real> <stage> <rotate 0|1>
|
|
822
|
+
# Prints the plan a human is asked to approve and the exact command that
|
|
823
|
+
# applies it. Declared impact and rollback text come from the script's own
|
|
824
|
+
# stage line; the agent relays them verbatim and composes none of its own.
|
|
825
|
+
soleur_op_plan_emit() {
|
|
826
|
+
local real="$1" stage="$2" rotate="${3:-0}" impact rollback flags="" digest op n apply_cmd approval quoted
|
|
827
|
+
impact="$(soleur_op_stage_field "$real" "$stage" 3)"
|
|
828
|
+
rollback="$(soleur_op_stage_field "$real" "$stage" 4)"
|
|
829
|
+
[[ "$rotate" == "1" ]] && flags="rotate-token"
|
|
830
|
+
digest="$(soleur_op_plan_digest "$real" "$stage" "$flags" "$impact" "$rollback")"
|
|
831
|
+
n="$(soleur_op_plan_count)"
|
|
832
|
+
printf 'SOLEUR_BOOTSTRAP_PLAN stage=%s operations=%s digest=%s\n' "$stage" "$n" "$digest"
|
|
833
|
+
for op in ${SOLEUR_OP_PLAN_OPS[@]+"${SOLEUR_OP_PLAN_OPS[@]}"}; do
|
|
834
|
+
printf 'SOLEUR_BOOTSTRAP_PLAN_OP stage=%s op=%s target=%s\n' "$stage" "${op%%|*}" "${op#*|}"
|
|
835
|
+
done
|
|
836
|
+
printf 'SOLEUR_BOOTSTRAP_IMPACT stage=%s text=%s\n' "$stage" "$impact"
|
|
837
|
+
printf 'SOLEUR_BOOTSTRAP_ROLLBACK stage=%s text=%s\n' "$stage" "$rollback"
|
|
838
|
+
if (( n == 0 )); then
|
|
839
|
+
printf 'Nothing to change for this stage: do NOT run --apply, there is nothing to approve.\n'
|
|
840
|
+
SOLEUR_OP_PLAN_DIGEST="$digest"
|
|
841
|
+
return 0
|
|
842
|
+
fi
|
|
843
|
+
if ! quoted="$(soleur_op_shquote "$real")"; then
|
|
844
|
+
printf 'SOLEUR_BOOTSTRAP_PATH_UNSUPPORTED stage=%s\n' "$stage"
|
|
845
|
+
printf 'This script lives at a path containing a single quote, which the approval prompt cannot carry. Move the script to a path without one and run the stage again; nothing was changed.\n'
|
|
846
|
+
exit 64
|
|
847
|
+
fi
|
|
848
|
+
apply_cmd="bash ${quoted} --stage ${stage} --apply --plan-digest ${digest}"
|
|
849
|
+
[[ "$rotate" == "1" ]] && apply_cmd="${apply_cmd} --rotate-token"
|
|
850
|
+
# approval_digest names the exact command printed below (script bytes included). It is an
|
|
851
|
+
# identifier for the receipt, never an approval: no variable or flag turns it into one.
|
|
852
|
+
if [[ "$rotate" == "1" ]]; then
|
|
853
|
+
approval="$(soleur_approval_binding_digest "$real" "$stage" --stage "$stage" --apply --plan-digest "$digest" --rotate-token)"
|
|
854
|
+
else
|
|
855
|
+
approval="$(soleur_approval_binding_digest "$real" "$stage" --stage "$stage" --apply --plan-digest "$digest")"
|
|
856
|
+
fi
|
|
857
|
+
printf 'SOLEUR_BOOTSTRAP_APPLY_COMMAND stage=%s approval_digest=%s\n' "$stage" "$approval"
|
|
858
|
+
printf ' %s\n' "$apply_cmd"
|
|
859
|
+
printf 'Tell the person the impact and rollback lines above in plain words, then run the command above exactly as printed (one simple command) and let the approval prompt carry their decision.\n'
|
|
860
|
+
SOLEUR_OP_PLAN_DIGEST="$digest"
|
|
861
|
+
}
|
|
862
|
+
SOLEUR_OP_PLAN_DIGEST=""
|
|
863
|
+
|
|
864
|
+
# soleur_op_stage_ok <stage> <changed 0|1> [approval]
|
|
865
|
+
soleur_op_stage_ok() {
|
|
866
|
+
if [[ -n "${3:-}" ]]; then
|
|
867
|
+
printf 'SOLEUR_BOOTSTRAP_STAGE_OK stage=%s changed=%s approval=%s\n' "$1" "$2" "$3"
|
|
868
|
+
else
|
|
869
|
+
printf 'SOLEUR_BOOTSTRAP_STAGE_OK stage=%s changed=%s\n' "$1" "$2"
|
|
870
|
+
fi
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
# soleur_op_precondition_failed <stage> <need-stage> <sentence>
|
|
874
|
+
# Stages are separately addressable, so a later stage re-proves what it depends
|
|
875
|
+
# on instead of trusting that an earlier one ran. Prints no plan digest.
|
|
876
|
+
soleur_op_precondition_failed() {
|
|
877
|
+
printf 'SOLEUR_BOOTSTRAP_PRECONDITION_FAILED stage=%s need=%s\n' "$1" "$2"
|
|
878
|
+
printf '%s\n' "$3"
|
|
879
|
+
# A precondition is a refusal with a named remedy, not a failure of the stage: the
|
|
880
|
+
# EXIT trap settles it as `refused` and prints no "run the stage again" banner.
|
|
881
|
+
SOLEUR_OP_REFUSED=1
|
|
882
|
+
exit 1
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
# soleur_op_ledger_approval <stage> <approval> <nonce-sha12> <plan-digest> <surface>
|
|
886
|
+
# One ledger line per approved write: names and hashes of PLAN data only. The
|
|
887
|
+
# nonce itself is never written anywhere; `nonce_sha12` is the first twelve hex
|
|
888
|
+
# digits of sha256(nonce) — the record's file-name prefix — enough to tie a line
|
|
889
|
+
# to a record and no use as a credential.
|
|
890
|
+
soleur_op_ledger_approval() {
|
|
891
|
+
soleur_op_ledger_write "$(printf '{"ts":"%s","run_id":"%s","event":"approval","stage":"%s","approval":"%s","nonce_sha12":"%s","plan":"%s","surface":"%s"}' \
|
|
892
|
+
"$(soleur_op_now)" "$SOLEUR_OP_RUN_ID" "$(soleur_op_json_escape "$1")" "$(soleur_op_json_escape "$2")" \
|
|
893
|
+
"$(soleur_op_json_escape "$3")" "$(soleur_op_json_escape "$4")" "$(soleur_op_json_escape "$5")")"
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
# --- MUTATION ANCHOR: start of stage gate ---
|
|
897
|
+
|
|
898
|
+
# soleur_op_stage_gate <script-real> <stage> <computed-plan-digest> <supplied-digest> <impact> <rollback> <argv...>
|
|
899
|
+
# Returns 0 ONLY when this stage's write is approved; every other outcome exits
|
|
900
|
+
# 75 (or 1 when a person typed anything but yes) with nothing written.
|
|
901
|
+
# <argv...> is the invocation exactly as the shell handed it over ("$@").
|
|
902
|
+
# A caller MUST call this before the first mutating command of the stage and
|
|
903
|
+
# MUST NOT run any child before it returns.
|
|
904
|
+
soleur_op_stage_gate() {
|
|
905
|
+
local real="$1" stage="$2" computed="$3" supplied="$4" impact="$5" rollback="$6" binding surface
|
|
906
|
+
shift 6
|
|
907
|
+
surface="${CLAUDE_CODE_ENTRYPOINT:-unknown}"
|
|
908
|
+
binding="$(soleur_approval_binding_digest "$real" "$stage" "$@")"
|
|
909
|
+
|
|
910
|
+
# 1. The plan the human approved is the plan about to run, or nothing runs. The
|
|
911
|
+
# supplied digest is only ever COMPARED, never trusted.
|
|
912
|
+
if [[ "$supplied" != "$computed" ]]; then
|
|
913
|
+
soleur_approval_burn
|
|
914
|
+
printf 'SOLEUR_BOOTSTRAP_PLAN_DRIFT stage=%s\n' "$stage"
|
|
915
|
+
printf 'The digest does not match the plan this script computes now (the state changed since the plan was shown, or the digest was not the one printed), so nothing was changed. Run --stage %s again (without --apply), use the digest it prints, tell the person what is different if anything is, and ask for approval again.\n' "$stage"
|
|
916
|
+
soleur_op_run_halt plan_drift "$stage"
|
|
917
|
+
SOLEUR_OP_REFUSED=1
|
|
918
|
+
exit 75
|
|
919
|
+
fi
|
|
920
|
+
|
|
921
|
+
# 2. A presented nonce must match a live record for exactly this invocation.
|
|
922
|
+
if soleur_approval_consume "$binding"; then
|
|
923
|
+
SOLEUR_OP_APPROVAL="harness-receipt"
|
|
924
|
+
soleur_op_ledger_approval "$stage" harness-receipt "$SOLEUR_APPROVAL_NONCE_SHA12" "$computed" "$surface"
|
|
925
|
+
return 0
|
|
926
|
+
fi
|
|
927
|
+
if [[ -n "$SOLEUR_APPROVAL_REASON" ]]; then
|
|
928
|
+
_SOLEUR_APPROVAL_NONCE_HELD=""
|
|
929
|
+
printf 'SOLEUR_BOOTSTRAP_APPROVAL_INVALID stage=%s reason=%s\n' "$stage" "$SOLEUR_APPROVAL_REASON"
|
|
930
|
+
case "$SOLEUR_APPROVAL_REASON" in
|
|
931
|
+
no-record|expired|digest-mismatch|consumed)
|
|
932
|
+
printf 'That approval is not usable for this command (%s), so nothing was changed. Issue the apply command once more exactly as printed by the plan, and the person will be asked again.\n' "$SOLEUR_APPROVAL_REASON" ;;
|
|
933
|
+
*)
|
|
934
|
+
printf 'A safety check on the approval failed (%s) and nothing was changed. Stop and report this to the person; do not retry.\n' "$SOLEUR_APPROVAL_REASON" ;;
|
|
935
|
+
esac
|
|
936
|
+
soleur_op_run_halt approval_invalid "$stage"
|
|
937
|
+
SOLEUR_OP_REFUSED=1
|
|
938
|
+
exit 75
|
|
939
|
+
fi
|
|
940
|
+
|
|
941
|
+
# 3. The second valid source: a real person typing yes at a real terminal, and
|
|
942
|
+
# ONLY outside an agent harness. A pseudo-terminal an agent opens (script, a
|
|
943
|
+
# python pty, expect) satisfies `-t 0`, so a bare TTY test would let the agent
|
|
944
|
+
# type its own `yes`; inside a harness the terminal is not evidence of a person.
|
|
945
|
+
if [[ -t 0 && -t 1 ]] && ! soleur_op_in_agent_harness; then
|
|
946
|
+
printf ' If this fails: %s\n Rollback: %s\n' "$impact" "$rollback"
|
|
947
|
+
soleur_op_ack_or_die " Apply stage ${stage}? Type 'yes': "
|
|
948
|
+
SOLEUR_OP_APPROVAL="tty-ack"
|
|
949
|
+
soleur_op_ledger_approval "$stage" tty-ack "" "$computed" "$surface"
|
|
950
|
+
return 0
|
|
951
|
+
fi
|
|
952
|
+
|
|
953
|
+
# 4. No approval and no terminal: refuse. The agent re-issues the command as one
|
|
954
|
+
# simple command so the approval hook can ask the person, or reports that this
|
|
955
|
+
# surface cannot make production changes yet.
|
|
956
|
+
printf 'SOLEUR_BOOTSTRAP_APPROVAL_REQUIRED stage=%s approval_digest=%s surface=%s\n' "$stage" "$binding" "$surface"
|
|
957
|
+
printf 'This change needs the person to approve this exact command, and nothing approved it. In Claude Code with the Soleur plugin, issue the apply command as ONE simple command (no pipe, no semicolon, no &&, no substitution) and the person will be asked at the prompt. On a surface without that prompt I cannot make this change from here yet: nothing was changed, the plan is saved above, and it needs Claude Code with the Soleur plugin on the person'"'"'s computer (tracked in #9388). Tell the person exactly that; do not ask them to open a terminal.\n'
|
|
958
|
+
soleur_op_run_halt approval_required "$stage"
|
|
959
|
+
SOLEUR_OP_REFUSED=1
|
|
960
|
+
exit 75
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
# --- MUTATION ANCHOR: end of stage gate ---
|
|
964
|
+
|
|
965
|
+
# ---------------------------------------------------------------------------
|
|
966
|
+
# .env upsert — EXACT-KEY, and the mechanism is the trailing `=`
|
|
967
|
+
# ---------------------------------------------------------------------------
|
|
968
|
+
#
|
|
969
|
+
# Revision R41: exact-key is the MAJORITY precedent, not a correction to one.
|
|
970
|
+
# The four community setup scripts now write `.env` through `_wenv_write <env>
|
|
971
|
+
# <remove-ERE> <KEY=VALUE>...` with an exact-key ERE (`^(KEY_A|KEY_B)=`); a PREFIX
|
|
972
|
+
# ERE such as `^LINKEDIN_` is correct only for a fixed block of keys sharing that
|
|
973
|
+
# prefix and also removes unrelated keys. Drop the `=` here and upserting
|
|
974
|
+
# `X_API_KEY` silently removes `X_API_KEY_SECRET`. This library takes no lock
|
|
975
|
+
# (tracked in #10204); the community helper's lock is a separate mechanism.
|
|
976
|
+
#
|
|
977
|
+
# No precedent fsyncs, and none handles a concurrent writer. This library
|
|
978
|
+
# inherits that limitation: acceptable for a single-operator script on the
|
|
979
|
+
# founder's own machine, and stated rather than assumed.
|
|
980
|
+
|
|
981
|
+
# soleur_op_validate_key <KEY>
|
|
982
|
+
# ONE validator for BOTH .env writers. The key is interpolated into a BRE
|
|
983
|
+
# (`grep -v "^${key}="`), so anything outside the identifier alphabet is either
|
|
984
|
+
# a regex metacharacter (`.` matches every key; `[` is a grep exit 2) or a
|
|
985
|
+
# newline that splits the file. Refused, never escaped.
|
|
986
|
+
soleur_op_validate_key() {
|
|
987
|
+
local key="$1"
|
|
988
|
+
if [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
|
989
|
+
printf 'SOLEUR_BOOTSTRAP_BAD_ARG key=%s\n' "${key//[$'\n\r']/ }"
|
|
990
|
+
return 1
|
|
991
|
+
fi
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
# soleur_op_env_key_count <file>
|
|
995
|
+
# `-a` on EVERY grep over the .env: without it grep's binary-file heuristic
|
|
996
|
+
# applies, and (measured) one cp1252 byte in a comment under a UTF-8 locale
|
|
997
|
+
# silently drops that line with rc 0, while a NUL byte anywhere makes `grep -v`
|
|
998
|
+
# print nothing at all — the mv then installs an .env holding only the new key.
|
|
999
|
+
soleur_op_env_key_count() {
|
|
1000
|
+
grep -a -c '^[A-Za-z_][A-Za-z0-9_]*=' "$1" 2>/dev/null || true
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
# soleur_op_env_filter_out <env-file> <KEY> <tmp>
|
|
1004
|
+
# Every line but `KEY=...` into <tmp>. grep exit 1 (nothing left) is a normal
|
|
1005
|
+
# outcome; exit 2 (unreadable file, a bad pattern) is NOT — the old `|| true`
|
|
1006
|
+
# turned it into an empty <tmp> that the mv then installed as the .env,
|
|
1007
|
+
# wiping every key and printing green. Shared by upsert and reset so the two
|
|
1008
|
+
# cannot drift.
|
|
1009
|
+
soleur_op_env_filter_out() {
|
|
1010
|
+
local env_file="$1" key="$2" tmp="$3" rc
|
|
1011
|
+
grep -a -v "^${key}=" "$env_file" > "$tmp" && rc=0 || rc=$?
|
|
1012
|
+
if (( rc > 1 )); then
|
|
1013
|
+
printf 'SOLEUR_BOOTSTRAP_ENV_READ_FAILED path=%s\n' "$env_file"
|
|
1014
|
+
rm -f "$tmp"
|
|
1015
|
+
return 1
|
|
1016
|
+
fi
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
# soleur_op_env_commit <env-file> <tmp> <keys-before> <min-keys-after>
|
|
1020
|
+
# The mv is REFUSED when the file about to be installed holds fewer keys than
|
|
1021
|
+
# the writer can legitimately produce: an upsert only adds or replaces (never
|
|
1022
|
+
# shrinks), a reset shrinks by at most one. Anything else is the filter having
|
|
1023
|
+
# lost lines — the failure both greps above were made loud for — caught on the
|
|
1024
|
+
# artefact rather than on the mechanism.
|
|
1025
|
+
soleur_op_env_commit() {
|
|
1026
|
+
local env_file="$1" tmp="$2" before="$3" min_after="$4" after
|
|
1027
|
+
after="$(soleur_op_env_key_count "$tmp")"
|
|
1028
|
+
if (( after < min_after )); then
|
|
1029
|
+
printf 'SOLEUR_BOOTSTRAP_ENV_KEYCOUNT_DROP before=%s after=%s path=%s\n' "$before" "$after" "$env_file"
|
|
1030
|
+
rm -f "$tmp"
|
|
1031
|
+
return 1
|
|
1032
|
+
fi
|
|
1033
|
+
mv "$tmp" "$env_file"
|
|
1034
|
+
chmod 600 "$env_file"
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
# soleur_op_has_control_char <value>
|
|
1038
|
+
# Success when the value holds ANY control character: the C0 set (tab, ESC,
|
|
1039
|
+
# the bell ...), DEL, and — the two the line-break check below already names —
|
|
1040
|
+
# CR and LF. The class is evaluated under `local LC_ALL=C` so the verdict is
|
|
1041
|
+
# BYTE-based and does not depend on the caller's locale (a bare `LC_ALL=C`
|
|
1042
|
+
# assignment would leak into the sourcing caller; `local` cannot). Under C only
|
|
1043
|
+
# the 0x00-0x1f and 0x7f bytes are control characters, so a UTF-8 credential is
|
|
1044
|
+
# judged the same in every locale.
|
|
1045
|
+
#
|
|
1046
|
+
# U+0085 (NEL), U+2028 and U+2029 are deliberately NOT refused, and that is
|
|
1047
|
+
# stated rather than assumed: they are the Unicode "line" separators, but the
|
|
1048
|
+
# sink here is a file read by `grep -a '^KEY='` and by the generated scripts'
|
|
1049
|
+
# byte-oriented `read`/`source`, none of which treats them as a line end — only a
|
|
1050
|
+
# consumer that Unicode-splits lines (a Python `splitlines`, a JS regexp `$`
|
|
1051
|
+
# with `m`) would, and no consumer of this file does. Refusing them would also
|
|
1052
|
+
# reject legitimate non-ASCII credentials. The ledger, a JSON sink that DOES care,
|
|
1053
|
+
# strips them in its own writer.
|
|
1054
|
+
soleur_op_has_control_char() {
|
|
1055
|
+
local LC_ALL=C
|
|
1056
|
+
[[ "$1" == *[[:cntrl:]]* ]]
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
# soleur_op_env_upsert <env-file> <KEY> <value>
|
|
1060
|
+
# ATOMIC: the new `KEY=value` line is appended to the temp file BEFORE the
|
|
1061
|
+
# `mv`, so there is no instant at which the key is absent from the .env (the
|
|
1062
|
+
# old shape had mv → chmod → separate append; a Ctrl-C in between lost the
|
|
1063
|
+
# key). The .env is resolved through any symlink first, so a linked .env is
|
|
1064
|
+
# rewritten in place rather than replaced by a regular file.
|
|
1065
|
+
soleur_op_env_upsert() {
|
|
1066
|
+
local env_file="$1" key="$2" value="$3" tmp before_count
|
|
1067
|
+
soleur_op_validate_key "$key" || return 1
|
|
1068
|
+
if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
|
|
1069
|
+
printf 'SOLEUR_BOOTSTRAP_BAD_ARG key=%s reason=value-contains-line-break\n' "$key"
|
|
1070
|
+
return 1
|
|
1071
|
+
fi
|
|
1072
|
+
# Any OTHER control character (tab, ESC, DEL ...): a value written raw as `KEY=value`
|
|
1073
|
+
# must stay one printable line. The marker names the KEY (already validated to the
|
|
1074
|
+
# identifier alphabet) and never the value.
|
|
1075
|
+
if soleur_op_has_control_char "$value"; then
|
|
1076
|
+
printf 'SOLEUR_BOOTSTRAP_BAD_ARG key=%s reason=value-contains-control-char\n' "$key"
|
|
1077
|
+
return 1
|
|
1078
|
+
fi
|
|
1079
|
+
env_file="$(readlink -f -- "$env_file" 2>/dev/null || printf '%s' "$env_file")"
|
|
1080
|
+
[[ -f "$env_file" ]] || : > "$env_file"
|
|
1081
|
+
before_count="$(soleur_op_env_key_count "$env_file")"
|
|
1082
|
+
# `.tmp.` in the name so a `.env*` ignore pattern covers the sibling too; the
|
|
1083
|
+
# template's not-ignored check probes this exact shape.
|
|
1084
|
+
#
|
|
1085
|
+
# A SOURCED library must not install an owning `trap ... EXIT`: it would REPLACE
|
|
1086
|
+
# the consumer's — provision-hetzner.sh installs `cleanup`, the generated template
|
|
1087
|
+
# installs `on_exit` — and `on_exit` is the terminal-outcome mechanism this whole
|
|
1088
|
+
# feature rests on. Clobbering it to tidy a tempfile trades a bounded leak for the
|
|
1089
|
+
# silent loss of the founder's stopped-at-stage report. So every path out of these
|
|
1090
|
+
# two helpers removes its own temp explicitly; the temp is a full copy of the
|
|
1091
|
+
# credentials file, which is why this is not merely tidiness.
|
|
1092
|
+
# lint-trap-ownership: ok sourced library; an EXIT trap here would clobber the consumer's on_exit/cleanup, and every path below removes this temp explicitly
|
|
1093
|
+
tmp="$(mktemp "${env_file}.tmp.XXXXXX")" || {
|
|
1094
|
+
printf 'SOLEUR_BOOTSTRAP_BAD_ARG helper=env_upsert reason=mktemp-failed\n'
|
|
1095
|
+
return 1
|
|
1096
|
+
}
|
|
1097
|
+
soleur_op_env_filter_out "$env_file" "$key" "$tmp" || { rm -f "$tmp"; return 1; }
|
|
1098
|
+
printf '%s=%s\n' "$key" "$value" >> "$tmp" || { rm -f "$tmp"; return 1; }
|
|
1099
|
+
# On success env_commit `mv`s the temp into place, so there is nothing to remove;
|
|
1100
|
+
# on failure it leaves it, and this is the only frame that still holds the name.
|
|
1101
|
+
soleur_op_env_commit "$env_file" "$tmp" "$before_count" "$before_count" || { rm -f "$tmp"; return 1; }
|
|
1102
|
+
soleur_op_ledger_note env_upsert "$key" "keys_before=${before_count}"
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
# soleur_op_env_reset <env-file> <KEY>
|
|
1106
|
+
# Revision R21a: a WRONG credential is otherwise permanent. The value is
|
|
1107
|
+
# persisted before it can be validated, and the environment-first ladder means
|
|
1108
|
+
# a re-run never re-prompts — it fails identically forever. This is the
|
|
1109
|
+
# `--reset <KEY>` path every generated script exposes.
|
|
1110
|
+
soleur_op_env_reset() {
|
|
1111
|
+
local env_file="$1" key="$2" tmp before_count
|
|
1112
|
+
soleur_op_validate_key "$key" || return 1
|
|
1113
|
+
env_file="$(readlink -f -- "$env_file" 2>/dev/null || printf '%s' "$env_file")"
|
|
1114
|
+
[[ -f "$env_file" ]] || return 0
|
|
1115
|
+
before_count="$(soleur_op_env_key_count "$env_file")"
|
|
1116
|
+
# lint-trap-ownership: ok sourced library; same reason as env_upsert above — an EXIT trap here would clobber the consumer's, and every path below removes this temp explicitly
|
|
1117
|
+
tmp="$(mktemp "${env_file}.tmp.XXXXXX")" || return 1
|
|
1118
|
+
soleur_op_env_filter_out "$env_file" "$key" "$tmp" || { rm -f "$tmp"; return 1; }
|
|
1119
|
+
soleur_op_env_commit "$env_file" "$tmp" "$before_count" "$(( before_count > 0 ? before_count - 1 : 0 ))" || { rm -f "$tmp"; return 1; }
|
|
1120
|
+
soleur_op_ledger_note env_reset "$key" ""
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
# ---------------------------------------------------------------------------
|
|
1124
|
+
# GitHub writes — two helpers, deliberately NOT one
|
|
1125
|
+
# ---------------------------------------------------------------------------
|
|
1126
|
+
#
|
|
1127
|
+
# Argv is CORRECT for a login and WRONG for a credential. Merging the two "for
|
|
1128
|
+
# symmetry" is the mutation Guard 3 drives, because the merged helper would let
|
|
1129
|
+
# a secret take the argv path — visible in /proc/<pid>/cmdline to every process
|
|
1130
|
+
# on the machine, and in shell history when re-run by hand.
|
|
1131
|
+
|
|
1132
|
+
# soleur_op_gh_secret_set <repo> <SECRET-NAME> <value>
|
|
1133
|
+
# STDIN ONLY. The write's own output is redirected — a third-party CLI can dump
|
|
1134
|
+
# unrelated secrets as a side effect of a write (the case
|
|
1135
|
+
# .claude/hooks/doppler-secrets-delete-redirect.sh already blocks) — and the
|
|
1136
|
+
# result is confirmed by a SEPARATE read.
|
|
1137
|
+
soleur_op_gh_secret_set() {
|
|
1138
|
+
local repo="$1" sec_name="$2" value="$3" listed
|
|
1139
|
+
printf '%s' "$value" | gh secret set "$sec_name" -R "$repo" >/dev/null 2>&1 || {
|
|
1140
|
+
printf 'SOLEUR_BOOTSTRAP_SECRET_WRITE_FAILED name=%s repo=%s\n' "$sec_name" "$repo"
|
|
1141
|
+
return 1
|
|
1142
|
+
}
|
|
1143
|
+
listed="$(gh secret list -R "$repo" 2>/dev/null || true)"
|
|
1144
|
+
if ! grep -qE "^${sec_name}[[:space:]]" <<<"$listed"; then
|
|
1145
|
+
# The write returned 0 and the read-back did not confirm it, so the secret
|
|
1146
|
+
# MAY be live. The terminal-outcome trap must not tell the founder nothing
|
|
1147
|
+
# changed — that is the one sentence that stops them revoking. Exported so
|
|
1148
|
+
# a trap in the calling script can see it.
|
|
1149
|
+
SOLEUR_OP_WRITE_MAY_HAVE_LANDED=1
|
|
1150
|
+
export SOLEUR_OP_WRITE_MAY_HAVE_LANDED
|
|
1151
|
+
printf 'SOLEUR_BOOTSTRAP_SECRET_VERIFY_FAILED name=%s repo=%s\n' "$sec_name" "$repo"
|
|
1152
|
+
return 1
|
|
1153
|
+
fi
|
|
1154
|
+
soleur_op_ledger_note gh_secret "$sec_name" "repo=${repo}"
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
# soleur_op_gh_variable_set <repo> <VARIABLE-NAME> <value>
|
|
1158
|
+
# Argv, because a repo VARIABLE is not confidential and masking it would make a
|
|
1159
|
+
# delivery failure harder to diagnose (the reason
|
|
1160
|
+
# provision-operator-digest-repo.sh writes OPERATOR_GH_LOGIN this way).
|
|
1161
|
+
#
|
|
1162
|
+
# The refusal below is NOVEL — no in-repo precedent has it. It is what stops
|
|
1163
|
+
# this helper from quietly becoming a secret path.
|
|
1164
|
+
# Case-insensitive and CONTAINS-form (review P2-10): the old suffix-only,
|
|
1165
|
+
# case-sensitive `^(.*_)?(TOKEN|KEY|SECRET|PASSWORD|PAT)$` admitted
|
|
1166
|
+
# HCLOUD_TOKEN_PRD, hcloud_token, SECRET_KEY_BASE, API_KEYS, PRIVATE_KEY_PEM,
|
|
1167
|
+
# CREDENTIALS, DB_PASSWD and SENTRY_DSN onto argv. `tr`, not `${var^^}`: the
|
|
1168
|
+
# latter is bash 4.0+ and this file's floor is 3.2.
|
|
1169
|
+
soleur_op_gh_variable_set() {
|
|
1170
|
+
local repo="$1" var_name="$2" value="$3" upper
|
|
1171
|
+
upper="$(printf '%s' "$var_name" | tr '[:lower:]' '[:upper:]')"
|
|
1172
|
+
if [[ "$upper" =~ (^|_)(TOKEN|KEY|SECRET|PASSWORD|PASSWD|PASSPHRASE|PAT|CREDENTIALS?|DSN|PRIVATE)(S?$|_) ]]; then
|
|
1173
|
+
printf 'SOLEUR_BOOTSTRAP_UNSAFE_VARIABLE name=%s reason=secret-shaped-name-on-argv\n' "$var_name"
|
|
1174
|
+
return 1
|
|
1175
|
+
fi
|
|
1176
|
+
gh variable set "$var_name" -R "$repo" --body "$value" >/dev/null 2>&1 || {
|
|
1177
|
+
printf 'SOLEUR_BOOTSTRAP_VARIABLE_WRITE_FAILED name=%s repo=%s\n' "$var_name" "$repo"
|
|
1178
|
+
return 1
|
|
1179
|
+
}
|
|
1180
|
+
soleur_op_ledger_note gh_variable "$var_name" "repo=${repo}"
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
# ---------------------------------------------------------------------------
|
|
1184
|
+
# Cross-platform URL opening — the one genuinely new primitive
|
|
1185
|
+
# ---------------------------------------------------------------------------
|
|
1186
|
+
#
|
|
1187
|
+
# ADDITIVE ONLY. The URL is printed FIRST and the opener's exit code is NEVER
|
|
1188
|
+
# branched on, so a headless box, a locked-down desktop or a broken handler
|
|
1189
|
+
# degrades to "here is the URL" rather than to a failed stage. In-repo prior art
|
|
1190
|
+
# (community/scripts/linkedin-setup.sh) is `xdg-open`/`open` only; the WSL arm is
|
|
1191
|
+
# new — `wslview`, `explorer.exe` and `$WSL_DISTRO_NAME` detection appear nowhere
|
|
1192
|
+
# else in this repository.
|
|
1193
|
+
#
|
|
1194
|
+
# Each opener runs in the BACKGROUND with stdin detached (review P2-11):
|
|
1195
|
+
# `xdg-open` on a box with no DISPLAY hands the URL to a terminal browser
|
|
1196
|
+
# (w3m, lynx) that seizes the tty and stdin, and the script looks hung one line
|
|
1197
|
+
# before its next prompt. The URL is already printed, so nothing is lost if the
|
|
1198
|
+
# opener never returns. It may carry a query string — a `?token=` would be
|
|
1199
|
+
# printed and passed on argv; do not put a secret in a URL you open this way.
|
|
1200
|
+
#
|
|
1201
|
+
# SOLEUR_OP_NO_OPEN=1 prints the URL and skips every opener — for a test
|
|
1202
|
+
# harness, a CI runner or a founder who does not want a tab stolen. It is the
|
|
1203
|
+
# only environment variable this primitive reads.
|
|
1204
|
+
soleur_op_open_url() {
|
|
1205
|
+
local url="$1"
|
|
1206
|
+
printf ' %s\n' "$url"
|
|
1207
|
+
if [[ "${SOLEUR_OP_NO_OPEN:-}" == "1" ]]; then
|
|
1208
|
+
return 0
|
|
1209
|
+
fi
|
|
1210
|
+
if [[ -n "${WSL_DISTRO_NAME:-}${WSL_INTEROP:-}" ]]; then
|
|
1211
|
+
if command -v wslview >/dev/null 2>&1; then
|
|
1212
|
+
(wslview "$url" || true) </dev/null >/dev/null 2>&1 &
|
|
1213
|
+
elif command -v explorer.exe >/dev/null 2>&1; then
|
|
1214
|
+
(explorer.exe "$url" || true) </dev/null >/dev/null 2>&1 &
|
|
1215
|
+
fi
|
|
1216
|
+
elif command -v xdg-open >/dev/null 2>&1; then
|
|
1217
|
+
(xdg-open "$url" || true) </dev/null >/dev/null 2>&1 &
|
|
1218
|
+
elif command -v open >/dev/null 2>&1; then
|
|
1219
|
+
(open "$url" || true) </dev/null >/dev/null 2>&1 &
|
|
1220
|
+
fi
|
|
1221
|
+
return 0
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
# ---------------------------------------------------------------------------
|
|
1225
|
+
# Closing summary
|
|
1226
|
+
# ---------------------------------------------------------------------------
|
|
1227
|
+
|
|
1228
|
+
soleur_op_summary_begin() {
|
|
1229
|
+
printf '\n'
|
|
1230
|
+
soleur_op_green "$1"
|
|
1231
|
+
soleur_op_green "Operator follow-ups:"
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
soleur_op_summary_line() {
|
|
1235
|
+
soleur_op_green " $1"
|
|
1236
|
+
}
|