@soleur/opencode 0.0.0-stage → 3.354.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (832) hide show
  1. package/.claude-plugin/agents.manifest.json +476 -0
  2. package/.claude-plugin/plugin.json +42 -0
  3. package/README.md +44 -2
  4. package/agents/community/.gitkeep +0 -0
  5. package/agents/engineering/cto.md +87 -0
  6. package/agents/engineering/design/ddd-architect.md +95 -0
  7. package/agents/engineering/discovery/agent-finder.md +214 -0
  8. package/agents/engineering/discovery/functional-discovery.md +182 -0
  9. package/agents/engineering/infra/infra-security.md +151 -0
  10. package/agents/engineering/infra/platform-strategist.md +109 -0
  11. package/agents/engineering/infra/terraform-architect.md +171 -0
  12. package/agents/engineering/prompt-engineer.md +37 -0
  13. package/agents/engineering/research/best-practices-researcher.md +115 -0
  14. package/agents/engineering/research/framework-docs-researcher.md +93 -0
  15. package/agents/engineering/research/git-history-analyzer.md +53 -0
  16. package/agents/engineering/research/learnings-researcher.md +285 -0
  17. package/agents/engineering/research/repo-research-analyst.md +118 -0
  18. package/agents/engineering/review/agent-native-reviewer.md +279 -0
  19. package/agents/engineering/review/architecture-strategist.md +61 -0
  20. package/agents/engineering/review/code-quality-analyst.md +92 -0
  21. package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
  22. package/agents/engineering/review/data-integrity-guardian.md +82 -0
  23. package/agents/engineering/review/data-migration-expert.md +98 -0
  24. package/agents/engineering/review/deployment-verification-agent.md +183 -0
  25. package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
  26. package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
  27. package/agents/engineering/review/legacy-code-expert.md +55 -0
  28. package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
  29. package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
  30. package/agents/engineering/review/performance-oracle.md +130 -0
  31. package/agents/engineering/review/security-sentinel.md +167 -0
  32. package/agents/engineering/review/semgrep-sast.md +36 -0
  33. package/agents/engineering/review/test-design-reviewer.md +150 -0
  34. package/agents/engineering/review/user-impact-reviewer.md +70 -0
  35. package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
  36. package/agents/finance/budget-analyst.md +29 -0
  37. package/agents/finance/cfo.md +44 -0
  38. package/agents/finance/financial-reporter.md +30 -0
  39. package/agents/finance/revenue-analyst.md +30 -0
  40. package/agents/legal/clo.md +115 -0
  41. package/agents/legal/legal-compliance-auditor.md +110 -0
  42. package/agents/legal/legal-document-generator.md +134 -0
  43. package/agents/marketing/analytics-analyst.md +25 -0
  44. package/agents/marketing/brand-architect.md +181 -0
  45. package/agents/marketing/cmo.md +76 -0
  46. package/agents/marketing/conversion-optimizer.md +19 -0
  47. package/agents/marketing/copywriter.md +20 -0
  48. package/agents/marketing/fact-checker.md +70 -0
  49. package/agents/marketing/growth-strategist.md +145 -0
  50. package/agents/marketing/paid-media-strategist.md +28 -0
  51. package/agents/marketing/pricing-strategist.md +17 -0
  52. package/agents/marketing/programmatic-seo-specialist.md +25 -0
  53. package/agents/marketing/retention-strategist.md +23 -0
  54. package/agents/marketing/seo-aeo-analyst.md +164 -0
  55. package/agents/operations/coo.md +50 -0
  56. package/agents/operations/ops-advisor.md +78 -0
  57. package/agents/operations/ops-provisioner.md +101 -0
  58. package/agents/operations/ops-research.md +52 -0
  59. package/agents/operations/service-automator.md +278 -0
  60. package/agents/product/business-validator.md +206 -0
  61. package/agents/product/competitive-intelligence.md +89 -0
  62. package/agents/product/cpo.md +63 -0
  63. package/agents/product/design/ux-design-lead.md +209 -0
  64. package/agents/product/spec-flow-analyzer.md +122 -0
  65. package/agents/sales/cro.md +44 -0
  66. package/agents/sales/deal-architect.md +29 -0
  67. package/agents/sales/outbound-strategist.md +29 -0
  68. package/agents/sales/pipeline-analyst.md +29 -0
  69. package/agents/support/cco.md +48 -0
  70. package/agents/support/community-manager.md +570 -0
  71. package/agents/support/ticket-triage.md +70 -0
  72. package/bin/soleur-opencode.js +29 -0
  73. package/commands/go.md +660 -0
  74. package/commands/help.md +273 -0
  75. package/commands/sync.md +977 -0
  76. package/commands/version.md +103 -0
  77. package/hooks/antigravity-hooks.json +84 -0
  78. package/hooks/antigravity-session-start.sh +15 -0
  79. package/hooks/bare-root-cwd-guard.sh +131 -0
  80. package/hooks/browser-snapshot-credential-guard.sh +200 -0
  81. package/hooks/codex-session-start.sh +9 -0
  82. package/hooks/compaction-state.sh +525 -0
  83. package/hooks/destructive-command-guard.sh +1387 -0
  84. package/hooks/devin-session-start.sh +90 -0
  85. package/hooks/ext-filter-detect.sh +171 -0
  86. package/hooks/hooks.json +133 -0
  87. package/hooks/lib/hook-tool-kind.sh +24 -0
  88. package/hooks/lib/log-rotation.sh +176 -0
  89. package/hooks/lib/shell-argv.pl +576 -0
  90. package/hooks/memory-backstop.sh +1114 -0
  91. package/hooks/operator-stage-approval.sh +391 -0
  92. package/hooks/stop-hook.sh +367 -0
  93. package/hooks/unkept-promise-hook.sh +383 -0
  94. package/hooks/version-banner.sh +127 -0
  95. package/hooks/welcome-hook.sh +50 -0
  96. package/install.js +29 -0
  97. package/lib/agent-registry.ts +154 -0
  98. package/lib/c4-canonical-cli.mjs +34 -0
  99. package/lib/c4-canonical.mjs +46 -0
  100. package/lib/c4-from-components.ts +554 -0
  101. package/lib/go-routing.ts +81 -0
  102. package/lib/grok-inspect-contract.ts +311 -0
  103. package/lib/harness-model-map.ts +101 -0
  104. package/lib/harness-parity.ts +726 -0
  105. package/lib/harness-tool-map.ts +180 -0
  106. package/lib/harness.ts +716 -0
  107. package/lib/heartbeat-live-reconcile.ts +1153 -0
  108. package/lib/heartbeat-manifest.ts +361 -0
  109. package/lib/kb-coverage.ts +252 -0
  110. package/lib/pr-merge-poll.ts +103 -0
  111. package/lib/workflow-fidelity.ts +388 -0
  112. package/opencode/INSTRUCTIONS.md +171 -0
  113. package/opencode/agents/engineering/cto.md +10 -0
  114. package/opencode/agents/engineering/design/ddd-architect.md +10 -0
  115. package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
  116. package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
  117. package/opencode/agents/engineering/infra/infra-security.md +10 -0
  118. package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
  119. package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
  120. package/opencode/agents/engineering/prompt-engineer.md +10 -0
  121. package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
  122. package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
  123. package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
  124. package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
  125. package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
  126. package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
  127. package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
  128. package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
  129. package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
  130. package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
  131. package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
  132. package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
  133. package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
  134. package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
  135. package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
  136. package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
  137. package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
  138. package/opencode/agents/engineering/review/performance-oracle.md +10 -0
  139. package/opencode/agents/engineering/review/security-sentinel.md +10 -0
  140. package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
  141. package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
  142. package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
  143. package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
  144. package/opencode/agents/finance/budget-analyst.md +10 -0
  145. package/opencode/agents/finance/cfo.md +10 -0
  146. package/opencode/agents/finance/financial-reporter.md +10 -0
  147. package/opencode/agents/finance/revenue-analyst.md +10 -0
  148. package/opencode/agents/legal/clo.md +10 -0
  149. package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
  150. package/opencode/agents/legal/legal-document-generator.md +10 -0
  151. package/opencode/agents/marketing/analytics-analyst.md +10 -0
  152. package/opencode/agents/marketing/brand-architect.md +10 -0
  153. package/opencode/agents/marketing/cmo.md +10 -0
  154. package/opencode/agents/marketing/conversion-optimizer.md +10 -0
  155. package/opencode/agents/marketing/copywriter.md +10 -0
  156. package/opencode/agents/marketing/fact-checker.md +10 -0
  157. package/opencode/agents/marketing/growth-strategist.md +10 -0
  158. package/opencode/agents/marketing/paid-media-strategist.md +10 -0
  159. package/opencode/agents/marketing/pricing-strategist.md +10 -0
  160. package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
  161. package/opencode/agents/marketing/retention-strategist.md +10 -0
  162. package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
  163. package/opencode/agents/operations/coo.md +10 -0
  164. package/opencode/agents/operations/ops-advisor.md +10 -0
  165. package/opencode/agents/operations/ops-provisioner.md +10 -0
  166. package/opencode/agents/operations/ops-research.md +10 -0
  167. package/opencode/agents/operations/service-automator.md +10 -0
  168. package/opencode/agents/product/business-validator.md +10 -0
  169. package/opencode/agents/product/competitive-intelligence.md +10 -0
  170. package/opencode/agents/product/cpo.md +10 -0
  171. package/opencode/agents/product/design/ux-design-lead.md +10 -0
  172. package/opencode/agents/product/spec-flow-analyzer.md +10 -0
  173. package/opencode/agents/sales/cro.md +10 -0
  174. package/opencode/agents/sales/deal-architect.md +10 -0
  175. package/opencode/agents/sales/outbound-strategist.md +10 -0
  176. package/opencode/agents/sales/pipeline-analyst.md +10 -0
  177. package/opencode/agents/support/cco.md +10 -0
  178. package/opencode/agents/support/community-manager.md +10 -0
  179. package/opencode/agents/support/ticket-triage.md +10 -0
  180. package/opencode/commands/go.md +10 -0
  181. package/opencode/commands/help.md +10 -0
  182. package/opencode/commands/sync.md +10 -0
  183. package/opencode/commands/version.md +11 -0
  184. package/opencode/opencode.json +8 -0
  185. package/opencode/plugin.ts +818 -0
  186. package/package.json +36 -4
  187. package/scripts/admin-merge-ready.sh +545 -0
  188. package/scripts/admin-merge-ready.test.sh +753 -0
  189. package/scripts/affected-tests.sh +908 -0
  190. package/scripts/affected-tests.test.sh +1797 -0
  191. package/scripts/alpha-metrics.sh +78 -0
  192. package/scripts/alpha-metrics.test.sh +115 -0
  193. package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
  194. package/scripts/audit-flag-flip.sh +107 -0
  195. package/scripts/check-red-on-main.sh +316 -0
  196. package/scripts/ci-head-verdict.sh +411 -0
  197. package/scripts/cloud-detect.sh +193 -0
  198. package/scripts/delegation-verify.sh +657 -0
  199. package/scripts/delegation-verify.test.sh +1424 -0
  200. package/scripts/deploy-arm.sh +475 -0
  201. package/scripts/domain-model-drift.sh +419 -0
  202. package/scripts/emit-decision.sh +144 -0
  203. package/scripts/emit-decision.test.sh +144 -0
  204. package/scripts/ext-filter-divergence-corpus.txt +15 -0
  205. package/scripts/ext-filter-divergence.sh +189 -0
  206. package/scripts/generate-c4-from-components.ts +442 -0
  207. package/scripts/grok-fidelity-gate.sh +68 -0
  208. package/scripts/grok-pre-push-gate.sh +232 -0
  209. package/scripts/harness-discovery-smoke.ts +906 -0
  210. package/scripts/harness-parity-census.ts +92 -0
  211. package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
  212. package/scripts/install-opencode-adapter.ts +509 -0
  213. package/scripts/lib/affected-tests/common.sh +365 -0
  214. package/scripts/lib/affected-tests/go.sh +157 -0
  215. package/scripts/lib/affected-tests/js.sh +188 -0
  216. package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
  217. package/scripts/lib/affected-tests/python.sh +185 -0
  218. package/scripts/lib/context-sanitize.sh +49 -0
  219. package/scripts/lib/domain-model-lib.sh +180 -0
  220. package/scripts/lib/ext-filter-grammar.tsv +34 -0
  221. package/scripts/lib/operator-script.sh +1236 -0
  222. package/scripts/lib/proc.sh +580 -0
  223. package/scripts/lib/session-state.sh +748 -0
  224. package/scripts/lib/tmp-classify.sh +843 -0
  225. package/scripts/lib/wrapper-boundary.sh +47 -0
  226. package/scripts/lint-ratchet.py +1547 -0
  227. package/scripts/monitor-pr-checks.sh +441 -0
  228. package/scripts/pipeline-tally.sh +756 -0
  229. package/scripts/pipeline-tally.test.sh +740 -0
  230. package/scripts/plugin-version.sh +528 -0
  231. package/scripts/precommit-guard.sh +115 -0
  232. package/scripts/precommit-guard.test.sh +254 -0
  233. package/scripts/reconcile-live-heartbeats.ts +705 -0
  234. package/scripts/render-c4-model.sh +217 -0
  235. package/scripts/resolve-git-root.sh +55 -0
  236. package/scripts/resolve-regenerable-conflicts.sh +363 -0
  237. package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
  238. package/scripts/ruff-requirements.txt +22 -0
  239. package/scripts/run-in-pid-namespace.sh +98 -0
  240. package/scripts/run-in-pid-namespace.test.sh +290 -0
  241. package/scripts/scan-ancestor-signal-helpers.py +556 -0
  242. package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
  243. package/scripts/setup-ralph-loop.sh +238 -0
  244. package/scripts/sync-cursor-name-map.ts +316 -0
  245. package/scripts/sync-grok-agent-compat.ts +188 -0
  246. package/scripts/sync-npm-package.ts +165 -0
  247. package/scripts/sync-opencode-adapter.ts +498 -0
  248. package/scripts/sync-pr-behind.sh +647 -0
  249. package/scripts/sync-pr-behind.test.sh +252 -0
  250. package/scripts/taste-profile-update.sh +180 -0
  251. package/scripts/taste-profile-update.test.sh +167 -0
  252. package/scripts/write-budget-marker.sh +23 -0
  253. package/scripts/write-kb-coverage.ts +113 -0
  254. package/skills/admin-ip-refresh/SKILL.md +81 -0
  255. package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
  256. package/skills/agent-browser/SKILL.md +698 -0
  257. package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
  258. package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
  259. package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
  260. package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
  261. package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
  262. package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
  263. package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
  264. package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
  265. package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
  266. package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
  267. package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
  268. package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
  269. package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
  270. package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
  271. package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
  272. package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
  273. package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
  274. package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
  275. package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
  276. package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
  277. package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
  278. package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
  279. package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
  280. package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
  281. package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
  282. package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
  283. package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
  284. package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
  285. package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
  286. package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
  287. package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
  288. package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
  289. package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
  290. package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
  291. package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
  292. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
  293. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
  294. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
  295. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
  296. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
  297. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
  298. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
  299. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
  300. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
  301. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
  302. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
  303. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
  304. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
  305. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
  306. package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
  307. package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
  308. package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
  309. package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
  310. package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
  311. package/skills/agent-native-architecture/SKILL.md +479 -0
  312. package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
  313. package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
  314. package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
  315. package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
  316. package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
  317. package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
  318. package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
  319. package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
  320. package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
  321. package/skills/agent-native-architecture/references/product-implications.md +478 -0
  322. package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
  323. package/skills/agent-native-architecture/references/self-modification.md +294 -0
  324. package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
  325. package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
  326. package/skills/agent-native-audit/SKILL.md +300 -0
  327. package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
  328. package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
  329. package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
  330. package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
  331. package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
  332. package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
  333. package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
  334. package/skills/architecture/SKILL.md +436 -0
  335. package/skills/architecture/references/adr-template.md +154 -0
  336. package/skills/architecture/references/likec4-reference.md +136 -0
  337. package/skills/architecture/references/nfr-reference.md +126 -0
  338. package/skills/archive-kb/SKILL.md +86 -0
  339. package/skills/archive-kb/scripts/archive-kb.sh +300 -0
  340. package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
  341. package/skills/atdd-developer/SKILL.md +65 -0
  342. package/skills/brainstorm/SKILL.md +701 -0
  343. package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
  344. package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
  345. package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
  346. package/skills/brainstorm/references/ui-surface-terms.md +42 -0
  347. package/skills/brainstorm-techniques/SKILL.md +231 -0
  348. package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
  349. package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
  350. package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
  351. package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
  352. package/skills/campaign-calendar/SKILL.md +145 -0
  353. package/skills/cf-token-scope/SKILL.md +164 -0
  354. package/skills/cf-token-scope/references/widen-playbook.md +114 -0
  355. package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
  356. package/skills/changelog/SKILL.md +142 -0
  357. package/skills/code-to-prd/SKILL.md +66 -0
  358. package/skills/code-to-prd/references/banner-template.md +12 -0
  359. package/skills/code-to-prd/references/prd-template.md +41 -0
  360. package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
  361. package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
  362. package/skills/code-to-prd/test/fixture/.env.example +7 -0
  363. package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
  364. package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
  365. package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
  366. package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
  367. package/skills/code-to-prd/test/fixture/package.json +6 -0
  368. package/skills/cohort-status/SKILL.md +62 -0
  369. package/skills/community/SKILL.md +210 -0
  370. package/skills/community/scripts/bsky-community.sh +644 -0
  371. package/skills/community/scripts/bsky-setup.sh +631 -0
  372. package/skills/community/scripts/community-router.sh +93 -0
  373. package/skills/community/scripts/discord-community.sh +487 -0
  374. package/skills/community/scripts/discord-setup.sh +784 -0
  375. package/skills/community/scripts/github-community.sh +706 -0
  376. package/skills/community/scripts/hn-community.sh +233 -0
  377. package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
  378. package/skills/community/scripts/linkedin-community.sh +750 -0
  379. package/skills/community/scripts/linkedin-setup.sh +758 -0
  380. package/skills/community/scripts/x-community.sh +890 -0
  381. package/skills/community/scripts/x-setup.sh +713 -0
  382. package/skills/community/test/community-argv.test.sh +2922 -0
  383. package/skills/community/test/github-community.test.sh +981 -0
  384. package/skills/competitive-analysis/SKILL.md +69 -0
  385. package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
  386. package/skills/compound/SKILL.md +673 -0
  387. package/skills/compound/scripts/token-efficiency-report.sh +334 -0
  388. package/skills/compound/test/phase-16.test.sh +379 -0
  389. package/skills/compound-capture/SKILL.md +791 -0
  390. package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
  391. package/skills/compound-capture/assets/resolution-template.md +116 -0
  392. package/skills/compound-capture/references/yaml-schema.md +67 -0
  393. package/skills/compound-capture/schema.yaml +181 -0
  394. package/skills/constraint-scaffold/SKILL.md +175 -0
  395. package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
  396. package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
  397. package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
  398. package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
  399. package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
  400. package/skills/constraint-scaffold/references/shared-runner.template +135 -0
  401. package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
  402. package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
  403. package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
  404. package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
  405. package/skills/constraint-scaffold/test/generator.test.sh +93 -0
  406. package/skills/constraint-scaffold/test/parity.test.sh +285 -0
  407. package/skills/content-writer/SKILL.md +242 -0
  408. package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
  409. package/skills/cron-delete/SKILL.md +56 -0
  410. package/skills/cron-list/SKILL.md +54 -0
  411. package/skills/deepen-plan/SKILL.md +937 -0
  412. package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
  413. package/skills/deploy/SKILL.md +115 -0
  414. package/skills/deploy/references/hetzner-setup.md +67 -0
  415. package/skills/deploy/scripts/deploy.sh +37 -0
  416. package/skills/deploy-docs/SKILL.md +95 -0
  417. package/skills/dhh-rails-style/SKILL.md +203 -0
  418. package/skills/dhh-rails-style/references/architecture.md +717 -0
  419. package/skills/dhh-rails-style/references/controllers.md +323 -0
  420. package/skills/dhh-rails-style/references/frontend.md +556 -0
  421. package/skills/dhh-rails-style/references/gems.md +316 -0
  422. package/skills/dhh-rails-style/references/models.md +388 -0
  423. package/skills/dhh-rails-style/references/testing.md +354 -0
  424. package/skills/discord-content/SKILL.md +126 -0
  425. package/skills/docs-site/SKILL.md +214 -0
  426. package/skills/drain-labeled-backlog/SKILL.md +203 -0
  427. package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
  428. package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
  429. package/skills/drain-prs/SKILL.md +143 -0
  430. package/skills/drain-prs/scripts/triage-prs.sh +192 -0
  431. package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
  432. package/skills/dspy-ruby/SKILL.md +634 -0
  433. package/skills/dspy-ruby/assets/config-template.rb +359 -0
  434. package/skills/dspy-ruby/assets/module-template.rb +326 -0
  435. package/skills/dspy-ruby/assets/signature-template.rb +143 -0
  436. package/skills/dspy-ruby/references/core-concepts.md +268 -0
  437. package/skills/dspy-ruby/references/optimization.md +623 -0
  438. package/skills/dspy-ruby/references/providers.md +335 -0
  439. package/skills/eval-harness/README.md +194 -0
  440. package/skills/eval-harness/SKILL.md +157 -0
  441. package/skills/eval-harness/enums/go-routes.json +13 -0
  442. package/skills/eval-harness/enums/incident-threshold.json +5 -0
  443. package/skills/eval-harness/enums/lane.json +5 -0
  444. package/skills/eval-harness/enums/tool-selection.json +20 -0
  445. package/skills/eval-harness/enums/triage-levels.json +5 -0
  446. package/skills/eval-harness/gated-skills.json +34 -0
  447. package/skills/eval-harness/models.generated.json +5 -0
  448. package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
  449. package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
  450. package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
  451. package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
  452. package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
  453. package/skills/eval-harness/prompts/go-baseline.txt +6 -0
  454. package/skills/eval-harness/prompts/go-skill.txt +21 -0
  455. package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
  456. package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
  457. package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
  458. package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
  459. package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
  460. package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
  461. package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
  462. package/skills/eval-harness/prompts/triage-skill.txt +10 -0
  463. package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
  464. package/skills/eval-harness/scripts/extract-block.cjs +87 -0
  465. package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
  466. package/skills/eval-harness/scripts/gen-models.sh +41 -0
  467. package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
  468. package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
  469. package/skills/eval-harness/scripts/parse-label.cjs +93 -0
  470. package/skills/eval-harness/scripts/verdict.cjs +132 -0
  471. package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
  472. package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
  473. package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
  474. package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
  475. package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
  476. package/skills/eval-harness/test/eval-gate.test.sh +132 -0
  477. package/skills/eval-harness/test/extract-block.test.sh +87 -0
  478. package/skills/eval-harness/test/gate-classification.test.sh +77 -0
  479. package/skills/eval-harness/test/gen-models.test.sh +75 -0
  480. package/skills/eval-harness/test/measure-classification.test.sh +74 -0
  481. package/skills/eval-harness/test/parse-label.test.sh +129 -0
  482. package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
  483. package/skills/eval-harness/test/verdict.test.sh +123 -0
  484. package/skills/every-style-editor/SKILL.md +142 -0
  485. package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
  486. package/skills/feature-tweet/SKILL.md +199 -0
  487. package/skills/feature-video/SKILL.md +412 -0
  488. package/skills/feature-video/scripts/check_deps.sh +281 -0
  489. package/skills/file-todos/SKILL.md +267 -0
  490. package/skills/file-todos/assets/todo-template.md +166 -0
  491. package/skills/fix-issue/SKILL.md +278 -0
  492. package/skills/fix-issue/references/agent-authored-exclusion.md +120 -0
  493. package/skills/fix-issue/references/exclude-label-jq-snippet.md +97 -0
  494. package/skills/flag-bootstrap/SETUP.md +142 -0
  495. package/skills/flag-create/SKILL.md +145 -0
  496. package/skills/flag-create/scripts/create.sh +273 -0
  497. package/skills/flag-delete/SKILL.md +167 -0
  498. package/skills/flag-delete/scripts/delete.sh +288 -0
  499. package/skills/flag-list/SKILL.md +99 -0
  500. package/skills/flag-list/scripts/list.sh +275 -0
  501. package/skills/flag-set-role/SKILL.md +229 -0
  502. package/skills/flag-set-role/scripts/flip.sh +774 -0
  503. package/skills/frontend-anti-slop/SKILL.md +106 -0
  504. package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
  505. package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
  506. package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
  507. package/skills/frontend-design/SKILL.md +77 -0
  508. package/skills/gdpr-gate/NOTICE +136 -0
  509. package/skills/gdpr-gate/SKILL.md +341 -0
  510. package/skills/gdpr-gate/references/fields.md +160 -0
  511. package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
  512. package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
  513. package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
  514. package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
  515. package/skills/gdpr-gate/references/layers/frontend.md +249 -0
  516. package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
  517. package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
  518. package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
  519. package/skills/gdpr-gate/references/legal-consent.md +140 -0
  520. package/skills/gdpr-gate/references/non-negotiables.md +60 -0
  521. package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
  522. package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
  523. package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
  524. package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
  525. package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
  526. package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
  527. package/skills/gemini-imagegen/SKILL.md +283 -0
  528. package/skills/gemini-imagegen/requirements.txt +5 -0
  529. package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
  530. package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
  531. package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
  532. package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
  533. package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
  534. package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
  535. package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
  536. package/skills/git-worktree/SKILL.md +534 -0
  537. package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
  538. package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
  539. package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
  540. package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
  541. package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
  542. package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
  543. package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
  544. package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
  545. package/skills/go/SKILL.md +54 -0
  546. package/skills/growth/SKILL.md +164 -0
  547. package/skills/harvest-debt/SKILL.md +78 -0
  548. package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
  549. package/skills/heal-skill/SKILL.md +173 -0
  550. package/skills/help/SKILL.md +46 -0
  551. package/skills/incident/NOTICE +39 -0
  552. package/skills/incident/SKILL.md +395 -0
  553. package/skills/incident/scripts/dry-run.sh +448 -0
  554. package/skills/incident/scripts/redact-engine.py +412 -0
  555. package/skills/incident/scripts/redact-sentinel.sh +44 -0
  556. package/skills/incident/templates/pir.md +139 -0
  557. package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
  558. package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
  559. package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
  560. package/skills/incident/test/fixtures/positive-corpus.md +87 -0
  561. package/skills/incident/test/redact-sentinel.test.sh +1420 -0
  562. package/skills/invoice/SKILL.md +206 -0
  563. package/skills/kb-glossary/SKILL.md +71 -0
  564. package/skills/kb-glossary/references/glossary-format.md +111 -0
  565. package/skills/kb-glossary/references/rejected-request-register.md +115 -0
  566. package/skills/kb-search/SKILL.md +276 -0
  567. package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
  568. package/skills/legal-audit/SKILL.md +161 -0
  569. package/skills/legal-generate/NOTICE +242 -0
  570. package/skills/legal-generate/SKILL.md +244 -0
  571. package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
  572. package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
  573. package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
  574. package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
  575. package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
  576. package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
  577. package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
  578. package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
  579. package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
  580. package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
  581. package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
  582. package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
  583. package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
  584. package/skills/linear-fetch/SKILL.md +247 -0
  585. package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
  586. package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
  587. package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
  588. package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
  589. package/skills/linear-fetch/test/parity.test.sh +112 -0
  590. package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
  591. package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
  592. package/skills/merge-pr/SKILL.md +670 -0
  593. package/skills/model-launch-review/SKILL.md +255 -0
  594. package/skills/model-launch-review/scripts/audit-models.sh +501 -0
  595. package/skills/one-shot/SKILL.md +316 -0
  596. package/skills/one-shot/references/token-discipline.md +74 -0
  597. package/skills/operator-bootstrap/SKILL.md +412 -0
  598. package/skills/operator-bootstrap/template.sh +439 -0
  599. package/skills/operator-digest/SKILL.md +287 -0
  600. package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
  601. package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
  602. package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
  603. package/skills/operator-rephrase/SKILL.md +98 -0
  604. package/skills/pencil-setup/SKILL.md +201 -0
  605. package/skills/pencil-setup/scripts/check_deps.sh +556 -0
  606. package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
  607. package/skills/pencil-setup/scripts/package-lock.json +1172 -0
  608. package/skills/pencil-setup/scripts/package.json +14 -0
  609. package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
  610. package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
  611. package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
  612. package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
  613. package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
  614. package/skills/plan/SKILL.md +1140 -0
  615. package/skills/plan/references/differential-migration-testing.md +185 -0
  616. package/skills/plan/references/plan-community-discovery.md +42 -0
  617. package/skills/plan/references/plan-founder-check.md +137 -0
  618. package/skills/plan/references/plan-functional-overlap.md +25 -0
  619. package/skills/plan/references/plan-host-fanout.md +51 -0
  620. package/skills/plan/references/plan-issue-templates.md +779 -0
  621. package/skills/plan/references/plan-network-outage-checklist.md +131 -0
  622. package/skills/plan/references/plan-scope-check.md +126 -0
  623. package/skills/plan/references/plan-sharp-edges.md +331 -0
  624. package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
  625. package/skills/plan/references/webapp-cwv-observability.md +143 -0
  626. package/skills/plan-review/SKILL.md +91 -0
  627. package/skills/plan-review/lib/named-panel.mjs +58 -0
  628. package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
  629. package/skills/postmerge/SKILL.md +642 -0
  630. package/skills/postmerge/references/deploy-status-debugging.md +141 -0
  631. package/skills/postmerge/references/graceful-degradation.md +20 -0
  632. package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
  633. package/skills/preflight/SKILL.md +1553 -0
  634. package/skills/preflight/references/check-13-founder-check.md +286 -0
  635. package/skills/preflight/scripts/founder-check.py +1640 -0
  636. package/skills/preflight/scripts/parse-form-a.awk +107 -0
  637. package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
  638. package/skills/product-roadmap/SKILL.md +357 -0
  639. package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
  640. package/skills/provision-cloudflare/SKILL.md +67 -0
  641. package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
  642. package/skills/provision-doppler/SKILL.md +58 -0
  643. package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
  644. package/skills/provision-github/SKILL.md +59 -0
  645. package/skills/provision-github/scripts/provision-github.sh +300 -0
  646. package/skills/provision-hetzner/SKILL.md +76 -0
  647. package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
  648. package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
  649. package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
  650. package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
  651. package/skills/qa/SKILL.md +263 -0
  652. package/skills/qa/references/browser-preflight.md +22 -0
  653. package/skills/questionnaire-generate/SKILL.md +328 -0
  654. package/skills/questionnaire-generate/references/questionnaire.template +60 -0
  655. package/skills/rclone/SKILL.md +162 -0
  656. package/skills/rclone/scripts/check_setup.sh +63 -0
  657. package/skills/release-announce/SKILL.md +65 -0
  658. package/skills/release-docs/SKILL.md +92 -0
  659. package/skills/reproduce-bug/SKILL.md +358 -0
  660. package/skills/resolve-debt/SKILL.md +85 -0
  661. package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
  662. package/skills/resolve-parallel/SKILL.md +45 -0
  663. package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
  664. package/skills/resolve-pr-parallel/SKILL.md +68 -0
  665. package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
  666. package/skills/resolve-todo-parallel/SKILL.md +57 -0
  667. package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
  668. package/skills/review/SKILL.md +1620 -0
  669. package/skills/review/references/anti-slop-scanner-hook.md +40 -0
  670. package/skills/review/references/defect-classes-migrations.md +9 -0
  671. package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
  672. package/skills/review/references/review-e2e-testing.md +116 -0
  673. package/skills/review/references/review-evidence-comment.md +36 -0
  674. package/skills/review/references/review-todo-structure.md +165 -0
  675. package/skills/review/references/risk-tier-and-fix-rounds.md +97 -0
  676. package/skills/review/references/semgrep-custom-rules.yaml +174 -0
  677. package/skills/review/references/ultrathink-phases.md +98 -0
  678. package/skills/review/references/wfs.md +6 -0
  679. package/skills/review/scripts/emit-review-trailer.sh +499 -0
  680. package/skills/review/scripts/ensure-semgrep.sh +99 -0
  681. package/skills/review/scripts/fix-round-seats.sh +197 -0
  682. package/skills/review/test/emit-review-trailer.test.sh +387 -0
  683. package/skills/review/test/ensure-semgrep.test.sh +111 -0
  684. package/skills/review/test/fix-round-seats.test.sh +237 -0
  685. package/skills/review/workflows/README.md +147 -0
  686. package/skills/review/workflows/review.workflow.js +667 -0
  687. package/skills/schedule/SKILL.md +796 -0
  688. package/skills/seo-aeo/SKILL.md +139 -0
  689. package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
  690. package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
  691. package/skills/ship/SKILL.md +3023 -0
  692. package/skills/ship/references/adr-ordinal-collision.md +47 -0
  693. package/skills/ship/references/affected-gate-contract.md +115 -0
  694. package/skills/ship/references/ci-run-diagnosis.md +29 -0
  695. package/skills/ship/references/ci-workflow-authoring.md +68 -0
  696. package/skills/ship/references/cleanup-merged-archival.md +39 -0
  697. package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
  698. package/skills/ship/references/cto-ruling-armed-push.md +5 -0
  699. package/skills/ship/references/decision-challenges-render.md +71 -0
  700. package/skills/ship/references/delegated-act-authoring-form.md +7 -0
  701. package/skills/ship/references/detached-battery-verdict.md +13 -0
  702. package/skills/ship/references/epic-close-sweep.md +62 -0
  703. package/skills/ship/references/feature-tweet-draft.md +42 -0
  704. package/skills/ship/references/followthrough-issue-body-template.md +47 -0
  705. package/skills/ship/references/followthrough-stub-template.sh +92 -0
  706. package/skills/ship/references/merge-queue-dequeue.md +39 -0
  707. package/skills/ship/references/operator-holds.md +179 -0
  708. package/skills/ship/references/pipeline-tally-render.md +43 -0
  709. package/skills/ship/references/pr-overlap.md +153 -0
  710. package/skills/ship/references/queue-mode.md +7 -0
  711. package/skills/ship/references/ready-run-wait.md +36 -0
  712. package/skills/ship/references/red-on-main-quarantine.md +110 -0
  713. package/skills/ship/references/register-update-pr-pattern.md +88 -0
  714. package/skills/ship/references/settle-then-admin-merge.md +130 -0
  715. package/skills/ship/references/supabase-column-probe.md +13 -0
  716. package/skills/ship/scripts/auto-close-scan.sh +76 -0
  717. package/skills/ship/scripts/battery-owed.sh +403 -0
  718. package/skills/ship/scripts/hold-gate-eval.sh +410 -0
  719. package/skills/ship/scripts/net-issue-flow.sh +665 -0
  720. package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
  721. package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
  722. package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
  723. package/skills/skill-creator/SKILL.md +299 -0
  724. package/skills/skill-creator/references/api-security.md +248 -0
  725. package/skills/skill-creator/references/authoring-levers.md +110 -0
  726. package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
  727. package/skills/skill-creator/references/best-practices.md +431 -0
  728. package/skills/skill-creator/references/common-patterns.md +639 -0
  729. package/skills/skill-creator/references/core-principles.md +193 -0
  730. package/skills/skill-creator/references/executable-code.md +188 -0
  731. package/skills/skill-creator/references/iteration-and-testing.md +509 -0
  732. package/skills/skill-creator/references/official-spec.md +193 -0
  733. package/skills/skill-creator/references/recommended-structure.md +180 -0
  734. package/skills/skill-creator/references/skill-structure.md +396 -0
  735. package/skills/skill-creator/references/using-scripts.md +122 -0
  736. package/skills/skill-creator/references/using-templates.md +120 -0
  737. package/skills/skill-creator/references/workflows-and-validation.md +541 -0
  738. package/skills/skill-creator/scripts/init_skill.py +322 -0
  739. package/skills/skill-creator/scripts/package_skill.py +110 -0
  740. package/skills/skill-creator/scripts/quick_validate.py +64 -0
  741. package/skills/skill-creator/templates/router-skill.md +73 -0
  742. package/skills/skill-creator/templates/simple-skill.md +34 -0
  743. package/skills/skill-creator/workflows/add-reference.md +96 -0
  744. package/skills/skill-creator/workflows/add-script.md +104 -0
  745. package/skills/skill-creator/workflows/add-template.md +83 -0
  746. package/skills/skill-creator/workflows/add-workflow.md +128 -0
  747. package/skills/skill-creator/workflows/audit-skill.md +159 -0
  748. package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
  749. package/skills/skill-creator/workflows/create-new-skill.md +207 -0
  750. package/skills/skill-creator/workflows/get-guidance.md +134 -0
  751. package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
  752. package/skills/skill-creator/workflows/verify-skill.md +226 -0
  753. package/skills/skill-security-scan/SKILL.md +145 -0
  754. package/skills/skill-security-scan/references/disclaimer.md +21 -0
  755. package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
  756. package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
  757. package/skills/skill-security-scan/references/override-mechanism.md +171 -0
  758. package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
  759. package/skills/skill-security-scan/references/regex-patterns.md +136 -0
  760. package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
  761. package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
  762. package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
  763. package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
  764. package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
  765. package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
  766. package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
  767. package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
  768. package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
  769. package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
  770. package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
  771. package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
  772. package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
  773. package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
  774. package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
  775. package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
  776. package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
  777. package/skills/skill-security-scan/scripts/lib.sh +197 -0
  778. package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
  779. package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
  780. package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
  781. package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
  782. package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
  783. package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
  784. package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
  785. package/skills/social-distribute/SKILL.md +485 -0
  786. package/skills/spec-templates/SKILL.md +274 -0
  787. package/skills/sync/SKILL.md +49 -0
  788. package/skills/test-browser/SKILL.md +464 -0
  789. package/skills/test-browser/references/feature-map.md +161 -0
  790. package/skills/test-fix-loop/SKILL.md +159 -0
  791. package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
  792. package/skills/triage/SKILL.md +402 -0
  793. package/skills/trigger-cron/SKILL.md +136 -0
  794. package/skills/trigger-cron/scripts/trigger.sh +198 -0
  795. package/skills/user-set-role/SKILL.md +147 -0
  796. package/skills/user-set-role/scripts/set-role.sh +218 -0
  797. package/skills/user-story-writer/SKILL.md +77 -0
  798. package/skills/ux-audit/SKILL.md +251 -0
  799. package/skills/ux-audit/references/finding.schema.json +62 -0
  800. package/skills/ux-audit/references/route-list.yaml +95 -0
  801. package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
  802. package/skills/ux-audit/scripts/bot-signin.ts +135 -0
  803. package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
  804. package/skills/verification-scaffold/SKILL.md +73 -0
  805. package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
  806. package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
  807. package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
  808. package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
  809. package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
  810. package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
  811. package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
  812. package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
  813. package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
  814. package/skills/verification-scaffold/references/guards.tsv +21 -0
  815. package/skills/verification-scaffold/references/verification-readme.template +55 -0
  816. package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
  817. package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
  818. package/skills/verification-scaffold/test/generator.test.sh +319 -0
  819. package/skills/verification-scaffold/test/manifest.test.sh +144 -0
  820. package/skills/work/SKILL.md +1480 -0
  821. package/skills/work/references/end-of-work-resume.md +7 -0
  822. package/skills/work/references/hook-test-main-cwd.md +24 -0
  823. package/skills/work/references/key-principles.md +83 -0
  824. package/skills/work/references/property-testing.md +122 -0
  825. package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
  826. package/skills/work/references/work-agent-teams.md +97 -0
  827. package/skills/work/references/work-lifecycle-parallel.md +147 -0
  828. package/skills/work/references/work-lockfile-bumps.md +97 -0
  829. package/skills/work/references/work-scratch-sandboxes.md +212 -0
  830. package/skills/work/references/work-subagent-fanout.md +61 -0
  831. package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
  832. 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
+ }