@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,1153 @@
|
|
|
1
|
+
// Pure, network-free reconcile logic for the twice-daily source-vs-live Better Stack reconcile
|
|
2
|
+
// (#6549 item 2; monitors arm + unmanaged-live class #7884). The CLI wrapper that fetches the live
|
|
3
|
+
// payload lives in `plugins/soleur/scripts/reconcile-live-heartbeats.ts`.
|
|
4
|
+
//
|
|
5
|
+
// Why this exists: `heartbeat-reprovision-parity.test.ts` proves a feeder exists in SOURCE, but
|
|
6
|
+
// `lifecycle { ignore_changes = [paused] }` (plus these resources being untargeted) makes the .tf
|
|
7
|
+
// `paused` value only a LOWER BOUND on live state. A heartbeat that is `paused` or absent in LIVE
|
|
8
|
+
// Better Stack is invisible to any source-only test — the exact state that hid the registry
|
|
9
|
+
// heartbeat for 9 days (#6537). This module compares the live payload against the executable
|
|
10
|
+
// MANIFEST and the declared `.tf` blocks. It only READS; it never unpauses anything.
|
|
11
|
+
//
|
|
12
|
+
// #7884 widened it: every live Better Stack uptime MONITOR and HEARTBEAT must be accounted for by
|
|
13
|
+
// exactly one resolved declaration (monitors by literal `url`, heartbeats by resolved `name`), and a
|
|
14
|
+
// declared monitor's live type / keyword / paused / alarm leaves must equal its declaration.
|
|
15
|
+
// `for_each` / `count` are resolved EXACTLY from literal variable defaults in the `.tf` SOURCE
|
|
16
|
+
// (`resolveInfraVariables`) — source defaults only: a `TF_VAR_*` / `-var` / tfvars override the
|
|
17
|
+
// apply uses is NOT seen here (ADR-117 amendment), so a root whose apply overrides a default can
|
|
18
|
+
// resolve a different instance set than live. Any shape that cannot be resolved exactly — including
|
|
19
|
+
// `*_override.tf` / `*.tf.json` files, which merge into declarations this scanner does not model —
|
|
20
|
+
// throws `UnresolvableDeclaration` (fail closed — a guess would either invent false
|
|
21
|
+
// `unmanaged-live` rows or silently absorb a hand-made object).
|
|
22
|
+
|
|
23
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
import type { ManifestEntry } from "./heartbeat-manifest";
|
|
27
|
+
|
|
28
|
+
/** A declaration the reconcile cannot resolve exactly. `resource` is `<type>.<name>` (or a file name). */
|
|
29
|
+
export class UnresolvableDeclaration extends Error {
|
|
30
|
+
readonly resource: string;
|
|
31
|
+
readonly why: string;
|
|
32
|
+
constructor(resource: string, why: string) {
|
|
33
|
+
super(`unresolvable declaration ${resource}: ${why}`);
|
|
34
|
+
this.name = "UnresolvableDeclaration";
|
|
35
|
+
this.resource = resource;
|
|
36
|
+
this.why = why;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A `.tf` file that could not be parsed at all (unbalanced block, unterminated comment, read error). */
|
|
41
|
+
export class DeclarationParseError extends Error {
|
|
42
|
+
readonly file: string;
|
|
43
|
+
readonly detail: string;
|
|
44
|
+
constructor(file: string, detail: string) {
|
|
45
|
+
super(`${file}: ${detail}`);
|
|
46
|
+
this.name = "DeclarationParseError";
|
|
47
|
+
this.file = file;
|
|
48
|
+
this.detail = detail;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One concrete `betteruptime_heartbeat` instance resolved from the infra `.tf` source. */
|
|
53
|
+
export interface DiscoveredHeartbeat {
|
|
54
|
+
/** The `.tf` resource label — `betteruptime_heartbeat.<resourceName>`; the MANIFEST join key. */
|
|
55
|
+
resourceName: string;
|
|
56
|
+
/** The resolved `name = "..."` attribute (`${each.key}` substituted) — how live Better Stack keys it. */
|
|
57
|
+
liveName: string;
|
|
58
|
+
/** The `for_each` key, or `"0"` for a `count` instance; undefined for a plain block. */
|
|
59
|
+
instanceKey?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The source-declared `paused` value (defaults to false when the attribute is absent). Carried
|
|
62
|
+
* for reporting/diagnostics only — `reconcileHeartbeats` keys its decision on live state +
|
|
63
|
+
* `feeder.kind`, never on this (source `paused` is only a lower bound on live; that decoupling is
|
|
64
|
+
* the whole reason a live reconcile exists).
|
|
65
|
+
*/
|
|
66
|
+
sourcePaused: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the block carries a `count = var.<X> ? 1 : 0` meta-argument. Diagnostic only since
|
|
69
|
+
* #7884: a count resolved to 0 yields NO instance (not expected live), a count resolved to 1 yields
|
|
70
|
+
* one instance that is expected live like any other.
|
|
71
|
+
*/
|
|
72
|
+
countGated: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One concrete `betteruptime_monitor` instance resolved from the infra `.tf` source (#7884).
|
|
77
|
+
*
|
|
78
|
+
* Shape contract (also consumed by the Guard 1 contract test): every optional leaf is `undefined`
|
|
79
|
+
* when the ATTRIBUTE IS ABSENT from the block and the literal value otherwise — so `""` and absent
|
|
80
|
+
* `required_keyword` are distinguishable. A present-but-non-literal value (e.g. `var.x`) never
|
|
81
|
+
* reaches this shape: `parseMonitorBlocks` throws `UnresolvableDeclaration` instead.
|
|
82
|
+
*/
|
|
83
|
+
export interface DiscoveredMonitor {
|
|
84
|
+
resourceName: string;
|
|
85
|
+
/** `betteruptime_monitor.<resourceName>`. */
|
|
86
|
+
resource: string;
|
|
87
|
+
/** `"0"` for a `count` instance; undefined for a plain block (`for_each` is unsupported). */
|
|
88
|
+
instanceKey?: string;
|
|
89
|
+
/** Literal `url` — the monitors-arm join key (a rename never reads as unmanaged). */
|
|
90
|
+
url: string;
|
|
91
|
+
monitorType: string;
|
|
92
|
+
requiredKeyword?: string;
|
|
93
|
+
paused?: boolean;
|
|
94
|
+
email?: boolean;
|
|
95
|
+
call?: boolean;
|
|
96
|
+
sms?: boolean;
|
|
97
|
+
push?: boolean;
|
|
98
|
+
verifySsl?: boolean;
|
|
99
|
+
followRedirects?: boolean;
|
|
100
|
+
confirmationPeriod?: number;
|
|
101
|
+
checkFrequency?: number;
|
|
102
|
+
requestTimeout?: number;
|
|
103
|
+
recoveryPeriod?: number;
|
|
104
|
+
/** Whether the block carries a `count` meta-argument. */
|
|
105
|
+
hasCount: boolean;
|
|
106
|
+
/** Whether the block carries a `for_each` meta-argument (always false today: it throws). */
|
|
107
|
+
hasForEach: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** One heartbeat as reported by `GET /api/v2/heartbeats` (`data[].id` + `data[].attributes`). */
|
|
111
|
+
export interface LiveHeartbeat {
|
|
112
|
+
/** Vendor id, numeric string (validated at fetch). */
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
/** Validated boolean at fetch (a non-boolean row is refused, never read as false). */
|
|
116
|
+
paused: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One monitor as reported by `GET /api/v2/monitors` (#7884). Booleans are validated at fetch. */
|
|
120
|
+
export interface LiveMonitor {
|
|
121
|
+
/** Vendor id, numeric string (validated at fetch). */
|
|
122
|
+
id: string;
|
|
123
|
+
url: string;
|
|
124
|
+
/** `pronounceable_name`; `""` when absent. Vendor text — reporting only, never a join key. */
|
|
125
|
+
name: string;
|
|
126
|
+
monitorType: string;
|
|
127
|
+
/** `null` when the vendor returns null/absent; compared equal to `""`. */
|
|
128
|
+
requiredKeyword: string | null;
|
|
129
|
+
paused: boolean;
|
|
130
|
+
email: boolean;
|
|
131
|
+
call: boolean;
|
|
132
|
+
sms: boolean;
|
|
133
|
+
push: boolean;
|
|
134
|
+
verifySsl: boolean;
|
|
135
|
+
followRedirects: boolean;
|
|
136
|
+
/** Numeric leaves: `null` when the vendor returns null (compared as the literal `null`). */
|
|
137
|
+
confirmationPeriod: number | null;
|
|
138
|
+
checkFrequency: number | null;
|
|
139
|
+
requestTimeout: number | null;
|
|
140
|
+
recoveryPeriod: number | null;
|
|
141
|
+
maintenanceFrom: string | null;
|
|
142
|
+
maintenanceTo: string | null;
|
|
143
|
+
/** `null` when absent. */
|
|
144
|
+
maintenanceDays: string[] | null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* `fed-but-paused` / `absent-live` are the heartbeat classes (a)/(b) below (`absent-live` is also
|
|
149
|
+
* the monitors-arm class for a declared monitor with no live object on its URL). `logs-alert-paused`
|
|
150
|
+
* / `logs-alert-absent` are the #8097 `logs_alert` arm (ADR-218 §5). `unmanaged-live` and
|
|
151
|
+
* `monitor-config-drift` are the #7884 classes.
|
|
152
|
+
* Every consumer that switches on this union is listed in the scheduled-terraform-drift.yml
|
|
153
|
+
* issue-body decode list (cq-union-widening-grep-three-patterns); the workflow test derives its
|
|
154
|
+
* list from this constant.
|
|
155
|
+
*/
|
|
156
|
+
export const VIOLATION_REASONS = [
|
|
157
|
+
"fed-but-paused",
|
|
158
|
+
"absent-live",
|
|
159
|
+
"logs-alert-paused",
|
|
160
|
+
"logs-alert-absent",
|
|
161
|
+
"unmanaged-live",
|
|
162
|
+
"monitor-config-drift",
|
|
163
|
+
] as const;
|
|
164
|
+
export type ViolationReason = (typeof VIOLATION_REASONS)[number];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The per-row routing token every MISMATCH line carries exactly once, before its first `"`:
|
|
168
|
+
* `route=<reason>~<subject>`, where subject is `resource.<type>.<name>[.<instance-key>]`,
|
|
169
|
+
* `id.<id>` (unmanaged-live) or `id.<id>.<field>` (monitor-config-drift). Distinct violations in one
|
|
170
|
+
* run have distinct tokens (a `for_each` instance key is part of the subject), so an issue filer can
|
|
171
|
+
* key escalation on it. A regex SOURCE string so a non-TS consumer can reuse it verbatim.
|
|
172
|
+
*/
|
|
173
|
+
export const ROUTE_TOKEN_RE = "^route=[a-z-]+~[A-Za-z0-9_.-]+$";
|
|
174
|
+
|
|
175
|
+
/** Heartbeat classes (a)/(b). `live` is the heartbeat live STATE. */
|
|
176
|
+
export interface HeartbeatViolation {
|
|
177
|
+
kind: "heartbeat";
|
|
178
|
+
resourceName: string;
|
|
179
|
+
liveName: string;
|
|
180
|
+
/** The `for_each` key / `"0"` for count; part of the route subject. */
|
|
181
|
+
instanceKey?: string;
|
|
182
|
+
live: "paused" | "absent";
|
|
183
|
+
reason: Extract<ViolationReason, "fed-but-paused" | "absent-live">;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* #8097 `logs_alert` arm. `live: "logs_alert"` is a SURFACE tag kept because the marker grammar
|
|
188
|
+
* `live=…` is a wire contract consumed by scheduled-terraform-drift.yml's decode list and ADR-218.
|
|
189
|
+
*/
|
|
190
|
+
export interface LogsAlertViolation {
|
|
191
|
+
kind: "logs_alert";
|
|
192
|
+
resourceName: string;
|
|
193
|
+
liveName: string;
|
|
194
|
+
live: "logs_alert";
|
|
195
|
+
reason: Extract<ViolationReason, "logs-alert-paused" | "logs-alert-absent">;
|
|
196
|
+
/** Vendor `paused_reason`, rendered inside a quoted `detail="…"`; absent for `logs-alert-absent`. */
|
|
197
|
+
detail?: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export type MonitorField =
|
|
201
|
+
| "monitor_type"
|
|
202
|
+
| "required_keyword"
|
|
203
|
+
| "paused"
|
|
204
|
+
| "email"
|
|
205
|
+
| "call"
|
|
206
|
+
| "sms"
|
|
207
|
+
| "push"
|
|
208
|
+
| "confirmation_period"
|
|
209
|
+
| "check_frequency"
|
|
210
|
+
| "request_timeout"
|
|
211
|
+
| "recovery_period"
|
|
212
|
+
| "verify_ssl"
|
|
213
|
+
| "follow_redirects"
|
|
214
|
+
| "maintenance";
|
|
215
|
+
|
|
216
|
+
/** #7884 monitors arm: a declared instance absent live, or a live field drifted from its declaration. */
|
|
217
|
+
export type MonitorViolation =
|
|
218
|
+
| { kind: "monitor"; reason: Extract<ViolationReason, "absent-live">; resourceName: string; instanceKey?: string; url: string }
|
|
219
|
+
| {
|
|
220
|
+
kind: "monitor";
|
|
221
|
+
reason: Extract<ViolationReason, "monitor-config-drift">;
|
|
222
|
+
resourceName: string;
|
|
223
|
+
id: string;
|
|
224
|
+
field: MonitorField;
|
|
225
|
+
declared: string;
|
|
226
|
+
live: string;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** #7884: a live object no resolved declaration accounts for (or one of ≥2 sharing a join key). */
|
|
230
|
+
export type UnmanagedViolation =
|
|
231
|
+
| { kind: "unmanaged"; surface: "monitors"; reason: Extract<ViolationReason, "unmanaged-live">; id: string; url: string; name: string; dup: boolean }
|
|
232
|
+
| { kind: "unmanaged"; surface: "heartbeats"; reason: Extract<ViolationReason, "unmanaged-live">; id: string; name: string; dup: boolean };
|
|
233
|
+
|
|
234
|
+
export type Violation = HeartbeatViolation | LogsAlertViolation | MonitorViolation | UnmanagedViolation;
|
|
235
|
+
|
|
236
|
+
export function assertNever(x: never): never {
|
|
237
|
+
throw new Error(`unhandled variant: ${JSON.stringify(x)}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* One route-subject segment that may carry arbitrary text (an instance key, a vendor id): kept
|
|
242
|
+
* verbatim when it is already in the token charset, else hex-encoded behind an `x--` prefix. A
|
|
243
|
+
* verbatim key that itself starts `x--` is encoded too, so the mapping is injective (distinct keys
|
|
244
|
+
* never share a token).
|
|
245
|
+
*/
|
|
246
|
+
function routeSegment(raw: string): string {
|
|
247
|
+
if (/^[A-Za-z0-9_.-]+$/.test(raw) && !raw.startsWith("x--")) return raw;
|
|
248
|
+
return `x--${Buffer.from(raw, "utf8").toString("hex")}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resourceSubject(type: string, name: string, instanceKey?: string): string {
|
|
252
|
+
const base = `resource.${type}.${routeSegment(name)}`;
|
|
253
|
+
return instanceKey === undefined ? base : `${base}.${routeSegment(instanceKey)}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The `route=<reason>~<subject>` token for one violation (see `ROUTE_TOKEN_RE`). */
|
|
257
|
+
export function routeToken(v: Violation): string {
|
|
258
|
+
let subject: string;
|
|
259
|
+
switch (v.kind) {
|
|
260
|
+
case "heartbeat":
|
|
261
|
+
subject = resourceSubject("betteruptime_heartbeat", v.resourceName, v.instanceKey);
|
|
262
|
+
break;
|
|
263
|
+
case "logs_alert":
|
|
264
|
+
subject = resourceSubject("logtail_exploration_alert", v.resourceName);
|
|
265
|
+
break;
|
|
266
|
+
case "monitor":
|
|
267
|
+
subject =
|
|
268
|
+
v.reason === "absent-live"
|
|
269
|
+
? resourceSubject("betteruptime_monitor", v.resourceName, v.instanceKey)
|
|
270
|
+
: `id.${routeSegment(v.id)}.${v.field}`;
|
|
271
|
+
break;
|
|
272
|
+
case "unmanaged":
|
|
273
|
+
subject = `id.${routeSegment(v.id)}`;
|
|
274
|
+
break;
|
|
275
|
+
default:
|
|
276
|
+
return assertNever(v);
|
|
277
|
+
}
|
|
278
|
+
return `route=${v.reason}~${subject}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** A `logtail_exploration_alert` block parsed from the infra `.tf` source (#8097). */
|
|
282
|
+
export interface DiscoveredLogsAlert {
|
|
283
|
+
resourceName: string;
|
|
284
|
+
/** The `name = "..."` attribute — how the Telemetry API keys the alert. */
|
|
285
|
+
liveName: string;
|
|
286
|
+
/**
|
|
287
|
+
* Whether the resource's declared `paused` RESOLVES to false — i.e. the source intent is "armed",
|
|
288
|
+
* so a live pause is drift worth reporting.
|
|
289
|
+
*
|
|
290
|
+
* True for a literal `paused = false`, for an absent `paused` (the provider default is unpaused,
|
|
291
|
+
* which is the same intent), and — since #8296 — for a `var`-driven expression whose variable
|
|
292
|
+
* default is a known boolean that resolves the expression to false.
|
|
293
|
+
*
|
|
294
|
+
* The reconciler's original premise was "every declared alert writes `paused = false` as intent".
|
|
295
|
+
* #6894 broke it: the wrong-volume alert ships `paused = !var.inngest_luks_cutover_complete`
|
|
296
|
+
* because before the cutover the condition it watches is the CORRECT state, so an armed rule
|
|
297
|
+
* would page continuously and get muted before it ever mattered. That was handled by treating
|
|
298
|
+
* EVERY expression as intent-to-pause, which exempted the alert from drift reporting
|
|
299
|
+
* permanently — including after the cutover armed it, when a live pause is exactly the silent
|
|
300
|
+
* failure the arm exists to surface.
|
|
301
|
+
*
|
|
302
|
+
* Resolving the expression removes the exemption without removing the variable. An unresolved
|
|
303
|
+
* variable, a non-boolean default, or an expression shape `resolvePausedIntent` does not
|
|
304
|
+
* understand stays EXEMPT (this field reads false and nothing is reported). Note the polarity:
|
|
305
|
+
* this file's header calls "throw `UnresolvableDeclaration`" fail-closed, and the monitor arm
|
|
306
|
+
* does exactly that for a non-literal `paused`; this arm deliberately does the QUIET thing
|
|
307
|
+
* instead, because reporting a pause we cannot prove is unintended is a false page. Quiet is
|
|
308
|
+
* not free — it collapses "could not resolve" into "nothing to report" — and the trade is
|
|
309
|
+
* recorded here rather than hidden behind the word "closed".
|
|
310
|
+
*
|
|
311
|
+
* Resolved from the SOURCE default only. A `TF_VAR_*` override in Doppler (the apply reads
|
|
312
|
+
* `soleur/prd_terraform`) is invisible here, so an override that PAUSES an alert whose default
|
|
313
|
+
* arms it will be reported as `logs-alert-paused`. That is intended: it is the only detector a
|
|
314
|
+
* forgotten override has. The triage text in scheduled-terraform-drift.yml names the check.
|
|
315
|
+
*/
|
|
316
|
+
pausedResolvesFalse: boolean;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** One alert as reported by `GET telemetry.betterstack.com/api/v2/alerts` (`data[].attributes`). */
|
|
320
|
+
export interface LiveLogsAlert {
|
|
321
|
+
name: string;
|
|
322
|
+
paused: boolean;
|
|
323
|
+
/** Vendor free text; `""` when absent/null (coalesced once, at parse). */
|
|
324
|
+
pausedReason: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── HCL scanning (string-, interpolation- and heredoc-aware) ───────────────────────────────────
|
|
328
|
+
|
|
329
|
+
/** Index just past the closing `"` of the string opened at `open` (which must be a `"`). -1 if unterminated. */
|
|
330
|
+
function skipString(text: string, open: number): number {
|
|
331
|
+
let i = open + 1;
|
|
332
|
+
while (i < text.length) {
|
|
333
|
+
const ch = text[i];
|
|
334
|
+
if (ch === "\\") {
|
|
335
|
+
i += 2;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (ch === "\n") return -1; // HCL quoted strings are single-line
|
|
339
|
+
if (ch === '"') return i + 1;
|
|
340
|
+
if ((ch === "$" || ch === "%") && text[i + 1] === "{") {
|
|
341
|
+
// `$${` / `%%{` are literal escapes, not template sequences.
|
|
342
|
+
if (text[i - 1] === ch) {
|
|
343
|
+
i += 2;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const close = matchBalanced(text, i + 1);
|
|
347
|
+
if (close === -1) return -1;
|
|
348
|
+
i = close + 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
i++;
|
|
352
|
+
}
|
|
353
|
+
return -1;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Index just past a heredoc opened at `open` (`<<` or `<<-`), or -1 when it is not a heredoc. */
|
|
357
|
+
function skipHeredoc(text: string, open: number): number {
|
|
358
|
+
return heredocSpan(text, open)?.end ?? -1;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** `bodyStart` is just past the opener line, `bodyEnd` the newline before the closer, `end` past the closer. */
|
|
362
|
+
function heredocSpan(text: string, open: number): { bodyStart: number; bodyEnd: number; end: number } | null {
|
|
363
|
+
const m = /^<<-?([A-Za-z_][A-Za-z0-9_]*)[ \t]*\n/.exec(text.slice(open, open + 256));
|
|
364
|
+
if (!m) return null;
|
|
365
|
+
const tag = m[1];
|
|
366
|
+
const bodyStart = open + m[0].length;
|
|
367
|
+
// A line scan, not `new RegExp(tag)`: the tag comes from the scanned text, and building a pattern
|
|
368
|
+
// from it is a data-to-regex flow even though the capture above admits identifier characters only.
|
|
369
|
+
for (let nl = bodyStart - 1; nl !== -1; ) {
|
|
370
|
+
const next = text.indexOf("\n", nl + 1);
|
|
371
|
+
const lineEnd = next === -1 ? text.length : next;
|
|
372
|
+
if (text.slice(nl + 1, lineEnd).replace(/^[ \t]+|[ \t]+$/g, "") === tag) {
|
|
373
|
+
return { bodyStart, bodyEnd: Math.max(bodyStart, nl), end: lineEnd };
|
|
374
|
+
}
|
|
375
|
+
nl = next;
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* One string-, interpolation- and heredoc-aware pass over HCL text that removes comments: `#` and
|
|
382
|
+
* `//` to end of line, and `/* … *\/` blocks (replaced by spaces, newlines kept, so line structure
|
|
383
|
+
* and brace positions survive). A comment opener inside a quoted string or a heredoc is content,
|
|
384
|
+
* not a comment. An unterminated block comment throws (Terraform rejects it too; stripping to EOF
|
|
385
|
+
* would silently drop every declaration after it). With `maskHeredocBodies`, each heredoc body is
|
|
386
|
+
* blanked as well, so text inside one (e.g. a `resource "…" "…" {` line in a `<<EOT` script) can
|
|
387
|
+
* never be read as a declaration.
|
|
388
|
+
*/
|
|
389
|
+
function scanHcl(text: string, maskHeredocBodies: boolean): string {
|
|
390
|
+
let out = "";
|
|
391
|
+
let i = 0;
|
|
392
|
+
while (i < text.length) {
|
|
393
|
+
const ch = text[i];
|
|
394
|
+
if (ch === '"') {
|
|
395
|
+
const end = skipString(text, i);
|
|
396
|
+
const nl = text.indexOf("\n", i);
|
|
397
|
+
const stop = end !== -1 ? end : nl === -1 ? text.length : nl;
|
|
398
|
+
out += text.slice(i, stop);
|
|
399
|
+
i = stop;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (ch === "<" && text[i + 1] === "<") {
|
|
403
|
+
const span = heredocSpan(text, i);
|
|
404
|
+
if (span) {
|
|
405
|
+
out += text.slice(i, span.bodyStart);
|
|
406
|
+
const body = text.slice(span.bodyStart, span.bodyEnd);
|
|
407
|
+
out += maskHeredocBodies ? body.replace(/[^\n]/g, " ") : body;
|
|
408
|
+
out += text.slice(span.bodyEnd, span.end);
|
|
409
|
+
i = span.end;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (ch === "#" || (ch === "/" && text[i + 1] === "/")) {
|
|
414
|
+
const nl = text.indexOf("\n", i);
|
|
415
|
+
i = nl === -1 ? text.length : nl;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
419
|
+
const close = text.indexOf("*/", i + 2);
|
|
420
|
+
if (close === -1) throw new Error("Unterminated block comment");
|
|
421
|
+
out += text.slice(i, close + 2).replace(/[^\n]/g, " ");
|
|
422
|
+
i = close + 2;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
out += ch;
|
|
426
|
+
i++;
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Strip HCL comments (`#`, `//`, `/* *\/`) so a `count =` / `paused =` token — or a whole resource
|
|
433
|
+
* block — that appears only inside a comment cannot be mistaken for real config. A comment opener
|
|
434
|
+
* inside a double-quoted string or a heredoc is preserved.
|
|
435
|
+
*/
|
|
436
|
+
export function stripComments(text: string): string {
|
|
437
|
+
return scanHcl(text, false);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** The view every declaration parser reads: comments removed AND heredoc bodies blanked. */
|
|
441
|
+
function codeView(text: string): string {
|
|
442
|
+
return scanHcl(text, true);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const OPENERS: Record<string, string> = { "{": "}", "[": "]", "(": ")" };
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Index of the bracket closing the one opened at `open` (`{`, `[` or `(`), skipping quoted strings
|
|
449
|
+
* (with `${…}` interpolation) and heredocs. -1 when unbalanced.
|
|
450
|
+
*/
|
|
451
|
+
function matchBalanced(text: string, open: number): number {
|
|
452
|
+
const stack: string[] = [OPENERS[text[open]]];
|
|
453
|
+
let i = open + 1;
|
|
454
|
+
while (i < text.length) {
|
|
455
|
+
const ch = text[i];
|
|
456
|
+
if (ch === '"') {
|
|
457
|
+
const end = skipString(text, i);
|
|
458
|
+
if (end === -1) return -1;
|
|
459
|
+
i = end;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (ch === "<" && text[i + 1] === "<") {
|
|
463
|
+
const end = skipHeredoc(text, i);
|
|
464
|
+
if (end !== -1) {
|
|
465
|
+
i = end;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (ch in OPENERS) stack.push(OPENERS[ch]);
|
|
470
|
+
else if (ch === "}" || ch === "]" || ch === ")") {
|
|
471
|
+
if (stack.pop() !== ch) return -1;
|
|
472
|
+
if (stack.length === 0) return i;
|
|
473
|
+
}
|
|
474
|
+
i++;
|
|
475
|
+
}
|
|
476
|
+
return -1;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* End index (exclusive) of the expression starting at `start`: runs to the first newline outside
|
|
481
|
+
* any bracket/string/heredoc (or `stopAtComma` at depth 0), clamped to `limit`.
|
|
482
|
+
*/
|
|
483
|
+
function expressionEnd(text: string, start: number, limit: number, stopAtComma = false): number {
|
|
484
|
+
let i = start;
|
|
485
|
+
while (i < limit) {
|
|
486
|
+
const ch = text[i];
|
|
487
|
+
if (ch === "\n") return i;
|
|
488
|
+
if (stopAtComma && ch === ",") return i;
|
|
489
|
+
if (ch === '"') {
|
|
490
|
+
const end = skipString(text, i);
|
|
491
|
+
if (end === -1) return limit;
|
|
492
|
+
i = end;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (ch === "<" && text[i + 1] === "<") {
|
|
496
|
+
const end = skipHeredoc(text, i);
|
|
497
|
+
if (end !== -1) {
|
|
498
|
+
i = end;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (ch in OPENERS) {
|
|
503
|
+
const close = matchBalanced(text, i);
|
|
504
|
+
if (close === -1) return limit;
|
|
505
|
+
i = close + 1;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
i++;
|
|
509
|
+
}
|
|
510
|
+
return limit;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Top-level (depth-1) `key = expr` attributes of a `{…}` block body. Nested blocks
|
|
515
|
+
* (`lifecycle {…}`, `validation {…}`, `request_headers {…}`) are skipped whole, so an attribute
|
|
516
|
+
* inside them is never read as the block's own.
|
|
517
|
+
*/
|
|
518
|
+
function topLevelAttributes(body: string): Map<string, string> {
|
|
519
|
+
const out = new Map<string, string>();
|
|
520
|
+
const limit = body.length - 1; // exclude the closing brace
|
|
521
|
+
const attr = /[ \t\r\n]*([A-Za-z_][A-Za-z0-9_-]*)[ \t]*=(?!=)[ \t]*/y;
|
|
522
|
+
const block = /[ \t\r\n]*[A-Za-z_][A-Za-z0-9_-]*(?:[ \t]+"[^"\n]*")*[ \t]*\{/y;
|
|
523
|
+
let i = 1;
|
|
524
|
+
while (i < limit) {
|
|
525
|
+
attr.lastIndex = i;
|
|
526
|
+
const a = attr.exec(body);
|
|
527
|
+
if (a) {
|
|
528
|
+
const valueStart = attr.lastIndex;
|
|
529
|
+
const end = expressionEnd(body, valueStart, limit);
|
|
530
|
+
out.set(a[1], body.slice(valueStart, end).trim());
|
|
531
|
+
i = end + 1;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
block.lastIndex = i;
|
|
535
|
+
const b = block.exec(body);
|
|
536
|
+
if (b) {
|
|
537
|
+
const close = matchBalanced(body, block.lastIndex - 1);
|
|
538
|
+
if (close === -1) throw new Error("Unbalanced nested block");
|
|
539
|
+
i = close + 1;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const nl = body.indexOf("\n", i);
|
|
543
|
+
i = nl === -1 ? limit : nl + 1;
|
|
544
|
+
}
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Brace-matched extraction of every `<kind> "<label>" [ "<name>" ] {…}` block in comment-stripped
|
|
550
|
+
* HCL. Throws on an unbalanced block so a malformed source can never silently drop a resource.
|
|
551
|
+
*/
|
|
552
|
+
function labeledBlocks(stripped: string, headerRe: RegExp, what: (m: RegExpExecArray) => string): { m: RegExpExecArray; body: string }[] {
|
|
553
|
+
const out: { m: RegExpExecArray; body: string }[] = [];
|
|
554
|
+
let m: RegExpExecArray | null;
|
|
555
|
+
while ((m = headerRe.exec(stripped)) !== null) {
|
|
556
|
+
const openBrace = headerRe.lastIndex - 1;
|
|
557
|
+
const end = matchBalanced(stripped, openBrace);
|
|
558
|
+
if (end === -1) {
|
|
559
|
+
throw new Error(`Unbalanced braces for ${what(m)}`);
|
|
560
|
+
}
|
|
561
|
+
out.push({ m, body: stripped.slice(openBrace, end + 1) });
|
|
562
|
+
headerRe.lastIndex = end + 1;
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** A block label: quoted (`"web-1_x"`) or bare (`web_1`); captures group 1 (quoted) or 2 (bare). */
|
|
568
|
+
const LABEL = '(?:"([A-Za-z_][A-Za-z0-9_-]*)"|([A-Za-z_][A-Za-z0-9_-]*))';
|
|
569
|
+
/** A block keyword only at the start of input or after whitespace / a brace (never mid-identifier). */
|
|
570
|
+
const HEADER_START = "(?<![^\\s{}])";
|
|
571
|
+
|
|
572
|
+
function resourceBlocks(code: string, type: string): { resourceName: string; body: string }[] {
|
|
573
|
+
const header = new RegExp(`${HEADER_START}resource\\s+(?:"${type}"|${type}(?![A-Za-z0-9_-]))\\s+${LABEL}\\s*\\{`, "g");
|
|
574
|
+
return labeledBlocks(code, header, (m) => `${type}.${m[1] ?? m[2]}`).map(({ m, body }) => ({ resourceName: m[1] ?? m[2], body }));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ─── Literal values ─────────────────────────────────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Decode a quoted HCL string expression. `${each.key}` is substituted with `eachKey` when given;
|
|
581
|
+
* any other template sequence (`${…}` / `%{…}`), or trailing text after the closing quote, makes
|
|
582
|
+
* the value non-literal → `null`.
|
|
583
|
+
*/
|
|
584
|
+
function decodeString(raw: string, eachKey?: string): string | null {
|
|
585
|
+
if (!raw.startsWith('"')) return null;
|
|
586
|
+
let out = "";
|
|
587
|
+
let i = 1;
|
|
588
|
+
while (i < raw.length) {
|
|
589
|
+
const ch = raw[i];
|
|
590
|
+
if (ch === '"') {
|
|
591
|
+
return i === raw.length - 1 ? out : null;
|
|
592
|
+
}
|
|
593
|
+
if (ch === "\\") {
|
|
594
|
+
const n = raw[i + 1];
|
|
595
|
+
const simple: Record<string, string> = { n: "\n", r: "\r", t: "\t", '"': '"', "\\": "\\" };
|
|
596
|
+
if (n in simple) {
|
|
597
|
+
out += simple[n];
|
|
598
|
+
i += 2;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (n === "u" && /^[0-9A-Fa-f]{4}$/.test(raw.slice(i + 2, i + 6))) {
|
|
602
|
+
out += String.fromCharCode(parseInt(raw.slice(i + 2, i + 6), 16));
|
|
603
|
+
i += 6;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
if ((ch === "$" || ch === "%") && raw[i + 1] === ch && raw[i + 2] === "{") {
|
|
609
|
+
out += `${ch}{`;
|
|
610
|
+
i += 3;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (ch === "%" && raw[i + 1] === "{") return null;
|
|
614
|
+
if (ch === "$" && raw[i + 1] === "{") {
|
|
615
|
+
const close = raw.indexOf("}", i + 2);
|
|
616
|
+
if (close === -1) return null;
|
|
617
|
+
const inner = raw.slice(i + 2, close).trim();
|
|
618
|
+
if (inner === "each.key" && eachKey !== undefined) {
|
|
619
|
+
out += eachKey;
|
|
620
|
+
i = close + 1;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
out += ch;
|
|
626
|
+
i++;
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const IDENT = "[A-Za-z_][A-Za-z0-9_-]*";
|
|
632
|
+
|
|
633
|
+
/** A literal variable default the resolver understands. */
|
|
634
|
+
export type InfraVariableDefault =
|
|
635
|
+
| { kind: "map"; keys: string[] }
|
|
636
|
+
| { kind: "bool"; value: boolean }
|
|
637
|
+
| { kind: "other" };
|
|
638
|
+
export type InfraVariables = ReadonlyMap<string, InfraVariableDefault>;
|
|
639
|
+
|
|
640
|
+
/** Top-level keys of a literal `{ … }` map/object expression, or `null` when not a plain literal map. */
|
|
641
|
+
function literalMapKeys(raw: string): string[] | null {
|
|
642
|
+
if (!raw.startsWith("{")) return null;
|
|
643
|
+
const close = matchBalanced(raw, 0);
|
|
644
|
+
if (close !== raw.length - 1) return null;
|
|
645
|
+
const keys: string[] = [];
|
|
646
|
+
const keyRe = new RegExp(`[\\s,]*(?:("(?:[^"\\\\\\n]|\\\\.)*")|(${IDENT}))[ \\t]*[=:](?!=)[ \\t]*`, "y");
|
|
647
|
+
let i = 1;
|
|
648
|
+
const limit = raw.length - 1;
|
|
649
|
+
while (i < limit) {
|
|
650
|
+
if (/^[\s,]*$/.test(raw.slice(i, limit))) break;
|
|
651
|
+
keyRe.lastIndex = i;
|
|
652
|
+
const k = keyRe.exec(raw);
|
|
653
|
+
if (!k) return null;
|
|
654
|
+
const key = k[1] !== undefined ? decodeString(k[1]) : k[2];
|
|
655
|
+
if (key === null) return null;
|
|
656
|
+
keys.push(key);
|
|
657
|
+
const end = expressionEnd(raw, keyRe.lastIndex, limit, true);
|
|
658
|
+
i = end + 1;
|
|
659
|
+
}
|
|
660
|
+
return keys;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Parse every `variable "<X>" { default = … }` in one `.tf` text (comments and heredoc bodies ignored). */
|
|
664
|
+
export function parseInfraVariables(tfText: string): Map<string, InfraVariableDefault> {
|
|
665
|
+
const code = codeView(tfText);
|
|
666
|
+
const out = new Map<string, InfraVariableDefault>();
|
|
667
|
+
const header = new RegExp(`${HEADER_START}variable\\s+${LABEL}\\s*\\{`, "g");
|
|
668
|
+
for (const { m, body } of labeledBlocks(code, header, (x) => `variable.${x[1] ?? x[2]}`)) {
|
|
669
|
+
const raw = topLevelAttributes(body).get("default");
|
|
670
|
+
if (raw === undefined) continue;
|
|
671
|
+
let value: InfraVariableDefault = { kind: "other" };
|
|
672
|
+
if (raw === "true" || raw === "false") value = { kind: "bool", value: raw === "true" };
|
|
673
|
+
else {
|
|
674
|
+
const keys = literalMapKeys(raw);
|
|
675
|
+
if (keys !== null) value = { kind: "map", keys };
|
|
676
|
+
}
|
|
677
|
+
out.set(m[1] ?? m[2], value);
|
|
678
|
+
}
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* The `*.tf` files of an infra directory, sorted. Throws `UnresolvableDeclaration` (resource = the
|
|
684
|
+
* file name) when the directory holds an override (`override.tf`, `*_override.tf`) or JSON
|
|
685
|
+
* configuration (`*.tf.json`) file: Terraform merges those into declarations this scanner does not
|
|
686
|
+
* model, so any verdict computed without them could be wrong in either direction.
|
|
687
|
+
*/
|
|
688
|
+
export function listTfFiles(infraDir: string): string[] {
|
|
689
|
+
const names = readdirSync(infraDir).sort();
|
|
690
|
+
for (const f of names) {
|
|
691
|
+
if (f === "override.tf" || f.endsWith("_override.tf") || f.endsWith(".tf.json")) {
|
|
692
|
+
throw new UnresolvableDeclaration(f, "override and JSON configuration files are unsupported");
|
|
693
|
+
}
|
|
694
|
+
// A tfvars file is a value source the apply WOULD read and this resolver does not model; since
|
|
695
|
+
// #8296 a var-driven `paused` is resolved from declared defaults, so a silently-ignored tfvars
|
|
696
|
+
// could invert that verdict in either direction. Refuse rather than resolve past it.
|
|
697
|
+
if (f === "terraform.tfvars" || f === "terraform.tfvars.json" || f.endsWith(".auto.tfvars") || f.endsWith(".auto.tfvars.json")) {
|
|
698
|
+
throw new UnresolvableDeclaration(f, "tfvars files are unsupported: this resolver reads declared defaults only");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return names.filter((f) => f.endsWith(".tf"));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Variables resolved per file; a file that fails to parse contributes nothing and is reported. */
|
|
705
|
+
export interface InfraVariableResolution {
|
|
706
|
+
vars: Map<string, InfraVariableDefault>;
|
|
707
|
+
errors: DeclarationParseError[];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Every literal variable default declared across the `*.tf` files of an infra directory, parsed
|
|
712
|
+
* file by file: an unparseable file is collected in `errors` (all-or-nothing for that file) so an
|
|
713
|
+
* unrelated broken block cannot blind every arm. A declaration that needed a variable from the
|
|
714
|
+
* broken file still fails closed (`UnresolvableDeclaration`: no literal default). Throws only when
|
|
715
|
+
* the directory itself cannot be listed or holds an override file (`listTfFiles`).
|
|
716
|
+
*/
|
|
717
|
+
export function resolveInfraVariablesPerFile(infraDir: string): InfraVariableResolution {
|
|
718
|
+
const vars = new Map<string, InfraVariableDefault>();
|
|
719
|
+
const errors: DeclarationParseError[] = [];
|
|
720
|
+
for (const file of listTfFiles(infraDir)) {
|
|
721
|
+
try {
|
|
722
|
+
const text = readFileSync(join(infraDir, file), "utf8");
|
|
723
|
+
if (!text.includes("variable")) continue;
|
|
724
|
+
for (const [k, v] of parseInfraVariables(text)) vars.set(k, v);
|
|
725
|
+
} catch (err) {
|
|
726
|
+
errors.push(new DeclarationParseError(file, String((err as Error)?.message ?? err)));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return { vars, errors };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** Strict form of `resolveInfraVariablesPerFile`: throws the first `DeclarationParseError`. */
|
|
733
|
+
export function resolveInfraVariables(infraDir: string): Map<string, InfraVariableDefault> {
|
|
734
|
+
const { vars, errors } = resolveInfraVariablesPerFile(infraDir);
|
|
735
|
+
if (errors.length > 0) throw errors[0];
|
|
736
|
+
return vars;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
interface Instance {
|
|
740
|
+
/** Substituted for `${each.key}` (for_each only). */
|
|
741
|
+
eachKey?: string;
|
|
742
|
+
/** The Terraform instance key: the for_each key, or `"0"` for a count instance. */
|
|
743
|
+
instanceKey?: string;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Resolve a block's `for_each` / `count` into its instances: one plain instance for a plain block,
|
|
748
|
+
* one per map entry for `for_each = var.<X>` (literal map default), one (`"0"`) or none for
|
|
749
|
+
* `count = var.<X> ? 1 : 0` (literal bool default). Anything else throws.
|
|
750
|
+
*/
|
|
751
|
+
function resolveInstances(
|
|
752
|
+
resource: string,
|
|
753
|
+
attrs: Map<string, string>,
|
|
754
|
+
vars: InfraVariables,
|
|
755
|
+
): { instances: Instance[]; countGated: boolean } {
|
|
756
|
+
const forEach = attrs.get("for_each");
|
|
757
|
+
const count = attrs.get("count");
|
|
758
|
+
if (forEach !== undefined && count !== undefined) {
|
|
759
|
+
throw new UnresolvableDeclaration(resource, "both for_each and count");
|
|
760
|
+
}
|
|
761
|
+
if (forEach !== undefined) {
|
|
762
|
+
const m = new RegExp(`^var\\.(${IDENT})$`).exec(forEach);
|
|
763
|
+
if (!m) throw new UnresolvableDeclaration(resource, `for_each is not exactly var.<name>: ${forEach}`);
|
|
764
|
+
const v = vars.get(m[1]);
|
|
765
|
+
if (!v || v.kind !== "map") {
|
|
766
|
+
throw new UnresolvableDeclaration(resource, `for_each variable ${m[1]} has no literal map default`);
|
|
767
|
+
}
|
|
768
|
+
return { instances: v.keys.map((k) => ({ eachKey: k, instanceKey: k })), countGated: false };
|
|
769
|
+
}
|
|
770
|
+
if (count !== undefined) {
|
|
771
|
+
const m = new RegExp(`^var\\.(${IDENT})\\s*\\?\\s*1\\s*:\\s*0$`).exec(count);
|
|
772
|
+
if (!m) throw new UnresolvableDeclaration(resource, `count is not exactly var.<name> ? 1 : 0: ${count}`);
|
|
773
|
+
const v = vars.get(m[1]);
|
|
774
|
+
if (!v || v.kind !== "bool") {
|
|
775
|
+
throw new UnresolvableDeclaration(resource, `count variable ${m[1]} has no literal bool default`);
|
|
776
|
+
}
|
|
777
|
+
return { instances: v.value ? [{ instanceKey: "0" }] : [], countGated: true };
|
|
778
|
+
}
|
|
779
|
+
return { instances: [{}], countGated: false };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ─── Declarations ───────────────────────────────────────────────────────────────────────────────
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Every concrete `betteruptime_heartbeat` instance in one `.tf` text, with `for_each`/`count`
|
|
786
|
+
* resolved against `vars`. Throws on an unbalanced block (never silently drop a heartbeat) and
|
|
787
|
+
* `UnresolvableDeclaration` on a name/meta-argument shape it cannot resolve exactly.
|
|
788
|
+
*/
|
|
789
|
+
export function parseHeartbeatBlocks(tfText: string, vars: InfraVariables = new Map()): DiscoveredHeartbeat[] {
|
|
790
|
+
const code = codeView(tfText);
|
|
791
|
+
const out: DiscoveredHeartbeat[] = [];
|
|
792
|
+
for (const { resourceName, body } of resourceBlocks(code, "betteruptime_heartbeat")) {
|
|
793
|
+
const resource = `betteruptime_heartbeat.${resourceName}`;
|
|
794
|
+
const attrs = topLevelAttributes(body);
|
|
795
|
+
const { instances, countGated } = resolveInstances(resource, attrs, vars);
|
|
796
|
+
const rawName = attrs.get("name");
|
|
797
|
+
if (rawName === undefined) throw new UnresolvableDeclaration(resource, "no name attribute");
|
|
798
|
+
// Absent `paused` defaults to active (false) — the conservative reading; an omission cannot
|
|
799
|
+
// silently exempt a live heartbeat. Mirrors heartbeat-reprovision-parity.test.ts.
|
|
800
|
+
const sourcePaused = attrs.get("paused") === "true";
|
|
801
|
+
for (const { eachKey, instanceKey } of instances) {
|
|
802
|
+
const liveName = decodeString(rawName, eachKey);
|
|
803
|
+
if (liveName === null || liveName === "") {
|
|
804
|
+
throw new UnresolvableDeclaration(resource, `name is not a literal (or \${each.key} template): ${rawName}`);
|
|
805
|
+
}
|
|
806
|
+
out.push({ resourceName, liveName, instanceKey, sourcePaused, countGated });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return out;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function optionalBool(resource: string, attrs: Map<string, string>, key: string): boolean | undefined {
|
|
813
|
+
const raw = attrs.get(key);
|
|
814
|
+
if (raw === undefined) return undefined;
|
|
815
|
+
if (raw !== "true" && raw !== "false") throw new UnresolvableDeclaration(resource, `${key} is not a literal bool`);
|
|
816
|
+
return raw === "true";
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function optionalInt(resource: string, attrs: Map<string, string>, key: string): number | undefined {
|
|
820
|
+
const raw = attrs.get(key);
|
|
821
|
+
if (raw === undefined) return undefined;
|
|
822
|
+
if (!/^(0|[1-9][0-9]{0,8})$/.test(raw)) throw new UnresolvableDeclaration(resource, `${key} is not a literal integer`);
|
|
823
|
+
return Number(raw);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Every concrete `betteruptime_monitor` instance in one `.tf` text (#7884). See `DiscoveredMonitor`
|
|
828
|
+
* for the returned shape. `url` and `monitor_type` must be literal strings; `required_keyword` a
|
|
829
|
+
* literal string or absent; the boolean alarm leaves (`paused`, `email`, `call`, `sms`, `push`,
|
|
830
|
+
* `verify_ssl`, `follow_redirects`) literal bools or absent; the numeric leaves
|
|
831
|
+
* (`confirmation_period`, `check_frequency`, `request_timeout`, `recovery_period`) literal integers
|
|
832
|
+
* or absent; `count` exactly `var.<bool> ? 1 : 0`; `for_each` is unsupported. Anything else throws
|
|
833
|
+
* `UnresolvableDeclaration`. Duplicate URLs across files are checked by `assertUniqueMonitorUrls`.
|
|
834
|
+
*/
|
|
835
|
+
export function parseMonitorBlocks(tfText: string, vars: InfraVariables = new Map()): DiscoveredMonitor[] {
|
|
836
|
+
const code = codeView(tfText);
|
|
837
|
+
const out: DiscoveredMonitor[] = [];
|
|
838
|
+
for (const { resourceName, body } of resourceBlocks(code, "betteruptime_monitor")) {
|
|
839
|
+
const resource = `betteruptime_monitor.${resourceName}`;
|
|
840
|
+
const attrs = topLevelAttributes(body);
|
|
841
|
+
if (attrs.has("for_each")) throw new UnresolvableDeclaration(resource, "for_each on betteruptime_monitor is unsupported");
|
|
842
|
+
const { instances } = resolveInstances(resource, attrs, vars);
|
|
843
|
+
const rawUrl = attrs.get("url");
|
|
844
|
+
const url = rawUrl === undefined ? null : decodeString(rawUrl);
|
|
845
|
+
if (url === null || url === "") throw new UnresolvableDeclaration(resource, "url is not a literal string");
|
|
846
|
+
const rawType = attrs.get("monitor_type");
|
|
847
|
+
const monitorType = rawType === undefined ? null : decodeString(rawType);
|
|
848
|
+
if (monitorType === null) throw new UnresolvableDeclaration(resource, "monitor_type is not a literal string");
|
|
849
|
+
const rawKw = attrs.get("required_keyword");
|
|
850
|
+
const requiredKeyword = rawKw === undefined ? undefined : decodeString(rawKw);
|
|
851
|
+
if (requiredKeyword === null) throw new UnresolvableDeclaration(resource, "required_keyword is not a literal string");
|
|
852
|
+
const leaves = {
|
|
853
|
+
paused: optionalBool(resource, attrs, "paused"),
|
|
854
|
+
email: optionalBool(resource, attrs, "email"),
|
|
855
|
+
call: optionalBool(resource, attrs, "call"),
|
|
856
|
+
sms: optionalBool(resource, attrs, "sms"),
|
|
857
|
+
push: optionalBool(resource, attrs, "push"),
|
|
858
|
+
verifySsl: optionalBool(resource, attrs, "verify_ssl"),
|
|
859
|
+
followRedirects: optionalBool(resource, attrs, "follow_redirects"),
|
|
860
|
+
confirmationPeriod: optionalInt(resource, attrs, "confirmation_period"),
|
|
861
|
+
checkFrequency: optionalInt(resource, attrs, "check_frequency"),
|
|
862
|
+
requestTimeout: optionalInt(resource, attrs, "request_timeout"),
|
|
863
|
+
recoveryPeriod: optionalInt(resource, attrs, "recovery_period"),
|
|
864
|
+
};
|
|
865
|
+
for (const { instanceKey } of instances) {
|
|
866
|
+
out.push({
|
|
867
|
+
resourceName,
|
|
868
|
+
resource,
|
|
869
|
+
instanceKey,
|
|
870
|
+
url,
|
|
871
|
+
monitorType,
|
|
872
|
+
requiredKeyword,
|
|
873
|
+
...leaves,
|
|
874
|
+
hasCount: attrs.has("count"),
|
|
875
|
+
hasForEach: attrs.has("for_each"),
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return out;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Two declared monitor instances on one URL are ambiguous (which one owns the live object?). */
|
|
883
|
+
export function assertUniqueMonitorUrls(declared: readonly DiscoveredMonitor[]): void {
|
|
884
|
+
const seen = new Set<string>();
|
|
885
|
+
for (const d of declared) {
|
|
886
|
+
if (seen.has(d.url)) {
|
|
887
|
+
throw new UnresolvableDeclaration(`betteruptime_monitor.${d.resourceName}`, `duplicate declared url ${d.url}`);
|
|
888
|
+
}
|
|
889
|
+
seen.add(d.url);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Extract every `logtail_exploration_alert` block's live name (#8097 `logs_alert` arm).
|
|
895
|
+
*
|
|
896
|
+
* `vars` (#8296) resolves a `var`-driven `paused`. It is the SAME resolution the heartbeat and
|
|
897
|
+
* monitor arms already take — `parseHeartbeatBlocks` / `parseMonitorBlocks` have carried a `vars`
|
|
898
|
+
* parameter since #7884; this arm was the one that did not.
|
|
899
|
+
*/
|
|
900
|
+
export function parseLogsAlertBlocks(tfText: string, vars: InfraVariables = new Map()): DiscoveredLogsAlert[] {
|
|
901
|
+
const code = codeView(tfText);
|
|
902
|
+
return resourceBlocks(code, "logtail_exploration_alert").map(({ resourceName, body }) => {
|
|
903
|
+
const nameMatch = /\bname\s*=\s*"([^"]+)"/.exec(body);
|
|
904
|
+
// `paused` is read from the block rather than assumed. A resource with no `paused` at all also
|
|
905
|
+
// counts as intent-false: the provider's default is unpaused, which is the same intent.
|
|
906
|
+
const pausedMatch = /^\s*paused\s*=\s*(.+?)\s*$/m.exec(body);
|
|
907
|
+
const pausedResolvesFalse =
|
|
908
|
+
pausedMatch === null || resolvePausedIntent(pausedMatch[1].trim(), vars) === false;
|
|
909
|
+
return { resourceName, liveName: nameMatch ? nameMatch[1] : "", pausedResolvesFalse };
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Resolve a declared `paused` expression to a boolean, or `null` when it cannot be resolved.
|
|
915
|
+
*
|
|
916
|
+
* Understands exactly three shapes: a boolean literal, `var.<name>`, and `!var.<name>`. The name is
|
|
917
|
+
* matched EXACTLY — never by prefix, so `var.foo_complete` can never be satisfied by a declaration
|
|
918
|
+
* of `var.foo`. Anything else (a conditional, a `local.`, a function call, a QUOTED literal such as
|
|
919
|
+
* `"false"` which HCL would coerce but this does not, a variable whose default is not a literal
|
|
920
|
+
* boolean) returns `null`, and the caller treats `null` as "leave it exempt".
|
|
921
|
+
*/
|
|
922
|
+
const PAUSED_VAR_RE = new RegExp(`^(!?)\\s*var\\.(${IDENT})$`);
|
|
923
|
+
function resolvePausedIntent(raw: string, vars: InfraVariables): boolean | null {
|
|
924
|
+
if (raw === "false") return false;
|
|
925
|
+
if (raw === "true") return true;
|
|
926
|
+
const m = PAUSED_VAR_RE.exec(raw);
|
|
927
|
+
if (m === null) return null;
|
|
928
|
+
const def = vars.get(m[2]);
|
|
929
|
+
if (def === undefined || def.kind !== "bool") return null;
|
|
930
|
+
return m[1] === "!" ? !def.value : def.value;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ─── Reconcile ──────────────────────────────────────────────────────────────────────────────────
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Reconcile the declared Logs alerts against the live Telemetry payload (#8097 / ADR-218).
|
|
937
|
+
*
|
|
938
|
+
* - **logs-alert-absent** — a declared `logtail_exploration_alert` missing from the live payload
|
|
939
|
+
* (the main apply never created it, or it was deleted vendor-side).
|
|
940
|
+
* - **logs-alert-paused** — present but `paused` live, AND the declaration's `paused` resolves to
|
|
941
|
+
* false (a literal, an absent attribute, or since #8296 a `var`-driven expression whose default
|
|
942
|
+
* resolves to false). Unlike heartbeats there is no fed/unfed distinction: the declaration says
|
|
943
|
+
* armed, so a live pause is a vendor-side rejection (`paused_reason` carried as `detail`) or a
|
|
944
|
+
* hand pause. A declaration whose `paused` cannot be resolved stays exempt.
|
|
945
|
+
*
|
|
946
|
+
* Foreign live alerts (not declared in `.tf`) are ignored — the arm only READS.
|
|
947
|
+
*/
|
|
948
|
+
export function reconcileLogsAlerts(
|
|
949
|
+
declared: readonly DiscoveredLogsAlert[],
|
|
950
|
+
live: readonly LiveLogsAlert[],
|
|
951
|
+
): LogsAlertViolation[] {
|
|
952
|
+
const byName = new Map<string, LiveLogsAlert>();
|
|
953
|
+
for (const a of live) byName.set(a.name, a);
|
|
954
|
+
const violations: LogsAlertViolation[] = [];
|
|
955
|
+
for (const d of declared) {
|
|
956
|
+
const l = byName.get(d.liveName);
|
|
957
|
+
if (!l) {
|
|
958
|
+
violations.push({ kind: "logs_alert", resourceName: d.resourceName, liveName: d.liveName, live: "logs_alert", reason: "logs-alert-absent" });
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
// A live pause is drift ONLY where the declaration RESOLVES to `paused = false` — a literal, an
|
|
962
|
+
// absent attribute, or (since #8296) a `var`-driven expression whose default resolves to false;
|
|
963
|
+
// see `resolvePausedIntent`. Where it resolves to true, or cannot be resolved, the pause is
|
|
964
|
+
// taken as what the author asked for and is not reported.
|
|
965
|
+
if (l.paused && d.pausedResolvesFalse) {
|
|
966
|
+
violations.push({
|
|
967
|
+
kind: "logs_alert",
|
|
968
|
+
resourceName: d.resourceName,
|
|
969
|
+
liveName: d.liveName,
|
|
970
|
+
live: "logs_alert",
|
|
971
|
+
reason: "logs-alert-paused",
|
|
972
|
+
detail: l.pausedReason,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return violations;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
type ManifestRow = Pick<ManifestEntry, "name" | "feeder" | "arming_pending">;
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Reconcile the live Better Stack payload against the MANIFEST + discovered `.tf` instances.
|
|
983
|
+
*
|
|
984
|
+
* - **(a) fed-but-paused** — a heartbeat instance whose MANIFEST feeder is a working feeder
|
|
985
|
+
* (`kind ∈ {cron,timer}`) that is `paused` in the live payload (the #6537 9-days-dark shape).
|
|
986
|
+
* A row carrying `arming_pending` is EXEMPT from (a): its paused state is a deliberately-deferred
|
|
987
|
+
* arming window owned by an issue (ADR-117's FED-but-inert legal state), not a forgotten monitor.
|
|
988
|
+
* - **(b) absent-live** — a resolved instance present in `.tf`/MANIFEST but missing from the live
|
|
989
|
+
* payload (the `git_data_prd` shape, #6548). A `count` resolved to 0 yields no instance at all
|
|
990
|
+
* (the item-1 paid-tier carve-out, evaluated rather than assumed since #7884).
|
|
991
|
+
* `arming_pending` does NOT exempt (b) — a declared-but-not-applied monitor is still surfaced.
|
|
992
|
+
*
|
|
993
|
+
* Iterates CONCRETE names: a `for_each` resource is checked once per resolved key (#7884 — the
|
|
994
|
+
* templated `${each.key}` name used to be compared verbatim and always read absent).
|
|
995
|
+
* The two classes are mutually exclusive per instance (present-but-paused vs. absent).
|
|
996
|
+
*/
|
|
997
|
+
export function reconcileHeartbeats(
|
|
998
|
+
manifest: readonly ManifestRow[],
|
|
999
|
+
discovered: readonly DiscoveredHeartbeat[],
|
|
1000
|
+
live: readonly LiveHeartbeat[],
|
|
1001
|
+
): HeartbeatViolation[] {
|
|
1002
|
+
const livePausedByName = new Map<string, boolean>();
|
|
1003
|
+
for (const hb of live) livePausedByName.set(hb.name, livePausedByName.get(hb.name) === true || hb.paused);
|
|
1004
|
+
|
|
1005
|
+
const discByResource = new Map<string, DiscoveredHeartbeat[]>();
|
|
1006
|
+
for (const d of discovered) {
|
|
1007
|
+
const list = discByResource.get(d.resourceName) ?? [];
|
|
1008
|
+
list.push(d);
|
|
1009
|
+
discByResource.set(d.resourceName, list);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const violations: HeartbeatViolation[] = [];
|
|
1013
|
+
for (const row of manifest) {
|
|
1014
|
+
// A manifest row with no matching .tf instance is a source-consistency issue that the static
|
|
1015
|
+
// parity test owns (or a count resolved to 0); the live reconcile has no name to check, so skip.
|
|
1016
|
+
const instances = discByResource.get(row.name) ?? [];
|
|
1017
|
+
const fed = row.feeder.kind === "cron" || row.feeder.kind === "timer";
|
|
1018
|
+
for (const disc of instances) {
|
|
1019
|
+
const base = { kind: "heartbeat" as const, resourceName: disc.resourceName, liveName: disc.liveName, instanceKey: disc.instanceKey };
|
|
1020
|
+
const present = livePausedByName.has(disc.liveName);
|
|
1021
|
+
if (!present) {
|
|
1022
|
+
violations.push({ ...base, live: "absent", reason: "absent-live" });
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (fed && !row.arming_pending && livePausedByName.get(disc.liveName) === true) {
|
|
1026
|
+
violations.push({ ...base, live: "paused", reason: "fed-but-paused" });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return violations;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* `unmanaged-live` per live heartbeat whose name is no declared instance's name, and for EVERY
|
|
1035
|
+
* live heartbeat of a name that ≥2 live heartbeats share (`dup`; none is picked as the managed one).
|
|
1036
|
+
*/
|
|
1037
|
+
export function findUnmanagedHeartbeats(
|
|
1038
|
+
declared: readonly DiscoveredHeartbeat[],
|
|
1039
|
+
live: readonly LiveHeartbeat[],
|
|
1040
|
+
): UnmanagedViolation[] {
|
|
1041
|
+
const declaredNames = new Set(declared.map((d) => d.liveName));
|
|
1042
|
+
const countByName = new Map<string, number>();
|
|
1043
|
+
for (const hb of live) countByName.set(hb.name, (countByName.get(hb.name) ?? 0) + 1);
|
|
1044
|
+
const out: UnmanagedViolation[] = [];
|
|
1045
|
+
for (const hb of live) {
|
|
1046
|
+
const dup = (countByName.get(hb.name) ?? 0) >= 2;
|
|
1047
|
+
if (dup || !declaredNames.has(hb.name)) {
|
|
1048
|
+
out.push({ kind: "unmanaged", surface: "heartbeats", reason: "unmanaged-live", id: hb.id, name: hb.name, dup });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return out;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/** Group live monitors by URL (shared by `reconcileMonitors` and `matchedMonitorIds`). */
|
|
1055
|
+
function liveMonitorsByUrl(live: readonly LiveMonitor[]): Map<string, LiveMonitor[]> {
|
|
1056
|
+
const byUrl = new Map<string, LiveMonitor[]>();
|
|
1057
|
+
for (const m of live) {
|
|
1058
|
+
const list = byUrl.get(m.url) ?? [];
|
|
1059
|
+
list.push(m);
|
|
1060
|
+
byUrl.set(m.url, list);
|
|
1061
|
+
}
|
|
1062
|
+
return byUrl;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const ALL_WEEK = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
1066
|
+
|
|
1067
|
+
/** A live maintenance window rendered for a drift row, or `null` when the monitor has none. */
|
|
1068
|
+
function liveMaintenanceWindow(l: LiveMonitor): string | null {
|
|
1069
|
+
const days = l.maintenanceDays;
|
|
1070
|
+
const limitedDays = days !== null && !ALL_WEEK.every((d) => days.includes(d));
|
|
1071
|
+
if (l.maintenanceFrom === null && l.maintenanceTo === null && !limitedDays) return null;
|
|
1072
|
+
return `from=${l.maintenanceFrom ?? "null"} to=${l.maintenanceTo ?? "null"} days=${(days ?? []).join(",")}`;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** Declared alarm leaves compared only when the declaration sets them: [field, declared, live]. */
|
|
1076
|
+
function declaredLeaves(d: DiscoveredMonitor, l: LiveMonitor): [MonitorField, boolean | number | undefined, boolean | number | null][] {
|
|
1077
|
+
return [
|
|
1078
|
+
["email", d.email, l.email],
|
|
1079
|
+
["call", d.call, l.call],
|
|
1080
|
+
["sms", d.sms, l.sms],
|
|
1081
|
+
["push", d.push, l.push],
|
|
1082
|
+
["confirmation_period", d.confirmationPeriod, l.confirmationPeriod],
|
|
1083
|
+
["check_frequency", d.checkFrequency, l.checkFrequency],
|
|
1084
|
+
["request_timeout", d.requestTimeout, l.requestTimeout],
|
|
1085
|
+
["recovery_period", d.recoveryPeriod, l.recoveryPeriod],
|
|
1086
|
+
["verify_ssl", d.verifySsl, l.verifySsl],
|
|
1087
|
+
["follow_redirects", d.followRedirects, l.followRedirects],
|
|
1088
|
+
];
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Reconcile declared monitor instances against the live monitors (#7884).
|
|
1093
|
+
*
|
|
1094
|
+
* - **absent-live** — a declared instance with no live monitor on its URL.
|
|
1095
|
+
* - **monitor-config-drift** — for EVERY live monitor on a declared URL (including each of ≥2
|
|
1096
|
+
* duplicates), one row per differing field: `monitor_type`, `required_keyword` (`null` == `""`,
|
|
1097
|
+
* absent == `""`) and `paused` (absent == false) always; the alarm leaves (`email`, `call`, `sms`,
|
|
1098
|
+
* `push`, `confirmation_period`, `check_frequency`, `request_timeout`, `recovery_period`,
|
|
1099
|
+
* `verify_ssl`, `follow_redirects`) when the declaration sets them; and `maintenance` whenever the
|
|
1100
|
+
* live monitor has a maintenance window (from/to set, or fewer than all seven days) — a window
|
|
1101
|
+
* silences the alarm, so it is always reported.
|
|
1102
|
+
* - **unmanaged-live** — a live monitor whose URL matches no declared instance, or every live
|
|
1103
|
+
* monitor on a URL that ≥2 live monitors share (`dup`; none is picked as managed).
|
|
1104
|
+
*/
|
|
1105
|
+
export function reconcileMonitors(
|
|
1106
|
+
declared: readonly DiscoveredMonitor[],
|
|
1107
|
+
live: readonly LiveMonitor[],
|
|
1108
|
+
): (MonitorViolation | UnmanagedViolation)[] {
|
|
1109
|
+
const liveByUrl = liveMonitorsByUrl(live);
|
|
1110
|
+
const declaredUrls = new Set(declared.map((d) => d.url));
|
|
1111
|
+
|
|
1112
|
+
const violations: (MonitorViolation | UnmanagedViolation)[] = [];
|
|
1113
|
+
for (const d of declared) {
|
|
1114
|
+
const onUrl = liveByUrl.get(d.url) ?? [];
|
|
1115
|
+
if (onUrl.length === 0) {
|
|
1116
|
+
violations.push({ kind: "monitor", reason: "absent-live", resourceName: d.resourceName, instanceKey: d.instanceKey, url: d.url });
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
for (const l of onUrl) {
|
|
1120
|
+
const drift = (field: MonitorField, declaredValue: string, liveValue: string) => {
|
|
1121
|
+
if (declaredValue !== liveValue) {
|
|
1122
|
+
violations.push({ kind: "monitor", reason: "monitor-config-drift", resourceName: d.resourceName, id: l.id, field, declared: declaredValue, live: liveValue });
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
drift("monitor_type", d.monitorType, l.monitorType);
|
|
1126
|
+
drift("required_keyword", d.requiredKeyword ?? "", l.requiredKeyword ?? "");
|
|
1127
|
+
drift("paused", String(d.paused ?? false), String(l.paused));
|
|
1128
|
+
for (const [field, declaredValue, liveValue] of declaredLeaves(d, l)) {
|
|
1129
|
+
if (declaredValue !== undefined) drift(field, String(declaredValue), String(liveValue));
|
|
1130
|
+
}
|
|
1131
|
+
const window = liveMaintenanceWindow(l);
|
|
1132
|
+
if (window !== null) drift("maintenance", "none", window);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
for (const l of live) {
|
|
1136
|
+
const dup = (liveByUrl.get(l.url)?.length ?? 0) >= 2;
|
|
1137
|
+
if (dup || !declaredUrls.has(l.url)) {
|
|
1138
|
+
violations.push({ kind: "unmanaged", surface: "monitors", reason: "unmanaged-live", id: l.id, url: l.url, name: l.name, dup });
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return violations;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/** Live ids that exactly one declared instance owns (its URL has exactly one live monitor), ascending. */
|
|
1145
|
+
export function matchedMonitorIds(declared: readonly DiscoveredMonitor[], live: readonly LiveMonitor[]): string[] {
|
|
1146
|
+
const liveByUrl = liveMonitorsByUrl(live);
|
|
1147
|
+
const ids: string[] = [];
|
|
1148
|
+
for (const d of declared) {
|
|
1149
|
+
const onUrl = liveByUrl.get(d.url) ?? [];
|
|
1150
|
+
if (onUrl.length === 1) ids.push(onUrl[0].id);
|
|
1151
|
+
}
|
|
1152
|
+
return ids.sort((a, b) => Number(a) - Number(b));
|
|
1153
|
+
}
|