@tyroneross/build-loop 0.36.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor/rules/build-loop-surface.mdc +12 -11
  6. package/.cursor/rules/skill-index.mdc +33 -0
  7. package/AGENTS.md +213 -34
  8. package/README.md +99 -31
  9. package/agents/advisor.md +4 -4
  10. package/agents/alignment-checker.md +2 -2
  11. package/agents/architecture-scout.md +4 -4
  12. package/agents/build-orchestrator.md +38 -36
  13. package/agents/database-assessor.md +11 -5
  14. package/agents/design-contract-specialist.md +8 -8
  15. package/agents/fact-checker.md +13 -3
  16. package/agents/fix-critique.md +2 -2
  17. package/agents/independent-auditor.md +60 -7
  18. package/agents/leak-scanner.md +82 -0
  19. package/agents/overfitting-reviewer.md +2 -2
  20. package/agents/plan-critic.md +1 -1
  21. package/agents/promotion-reviewer.md +5 -5
  22. package/agents/retrospective-synthesizer.md +138 -35
  23. package/agents/scope-auditor.md +82 -11
  24. package/agents/security-reviewer.md +56 -2
  25. package/agents/self-improvement-architect.md +17 -3
  26. package/agents/transcript-pattern-miner.md +5 -5
  27. package/agents/ui-validator.md +1 -1
  28. package/bin/build-loop-debugger.js +143 -0
  29. package/bin/build-loop-install.js +1 -4
  30. package/bin/build-loop-load-probe.js +345 -0
  31. package/codex-skills/build-loop/SKILL.md +28 -6
  32. package/commands/feedback.md +37 -0
  33. package/dist/src/interactive-verifier.d.ts +1 -14
  34. package/dist/src/interactive-verifier.d.ts.map +1 -1
  35. package/dist/src/interactive-verifier.js +6 -113
  36. package/dist/src/interactive-verifier.js.map +1 -1
  37. package/dist/src/quality.d.ts +5 -0
  38. package/dist/src/quality.d.ts.map +1 -0
  39. package/dist/src/quality.js +81 -0
  40. package/dist/src/quality.js.map +1 -0
  41. package/dist/src/storage.d.ts.map +1 -1
  42. package/dist/src/storage.js +37 -3
  43. package/dist/src/storage.js.map +1 -1
  44. package/docs/agent-surface-policy.md +35 -31
  45. package/docs/memory-setup.md +19 -0
  46. package/hooks/git/pre-push +65 -4
  47. package/hooks/hooks.json +95 -38
  48. package/hooks/pre-commit +20 -1
  49. package/hooks/pre-edit-rally-point.sh +10 -3
  50. package/hooks/session-start-codex-hook-trust.sh +30 -0
  51. package/hooks/session-start-git-hooks.sh +3 -1
  52. package/hooks/session-start-rally-point.sh +52 -4
  53. package/hooks/session-start-worktree-gc.sh +47 -94
  54. package/hooks/stop-transcript-sweep.sh +173 -0
  55. package/hooks/test_closeout.sh +14 -2
  56. package/package.json +8 -7
  57. package/scripts/README.md +1 -1
  58. package/scripts/_paths.py +65 -0
  59. package/scripts/groundwork_exchange.py +1012 -0
  60. package/scripts/install_memory.py +33 -1
  61. package/scripts/lessons_index/ingest.py +13 -2
  62. package/scripts/lessons_index/query.py +36 -13
  63. package/scripts/memory_context/__init__.py +108 -14
  64. package/scripts/memory_graph/__init__.py +5 -1
  65. package/scripts/project_resolver.py +42 -36
  66. package/scripts/sync_plugin_cache.py +37 -2
  67. package/skills/agent-rally-point/SKILL.md +46 -0
  68. package/skills/api-registry-bridge/SKILL.md +1 -1
  69. package/skills/architecture/dead/SKILL.md +1 -1
  70. package/skills/architecture/impact/SKILL.md +1 -1
  71. package/skills/architecture/review/SKILL.md +1 -1
  72. package/skills/architecture/rules/SKILL.md +3 -3
  73. package/skills/architecture/scan/SKILL.md +1 -1
  74. package/skills/architecture/trace/SKILL.md +1 -1
  75. package/skills/attribution-standard/SKILL.md +6 -6
  76. package/skills/auto-decision-capture/SKILL.md +31 -2
  77. package/skills/auto-finding-capture/SKILL.md +28 -1
  78. package/skills/build-loop/SKILL.md +131 -23
  79. package/skills/build-loop/fallbacks.md +16 -21
  80. package/skills/build-loop/phases/ui-validation.md +2 -2
  81. package/skills/build-loop/references/advisor-dispatch-ladder.md +1 -1
  82. package/skills/build-loop/references/apple-native-planning.md +1 -1
  83. package/skills/build-loop/references/autonomous-and-per-commit-modes.md +11 -5
  84. package/skills/build-loop/references/autonomy-dashboard.md +115 -0
  85. package/skills/build-loop/references/capability-routing.md +24 -2
  86. package/skills/build-loop/references/coordination.md +24 -6
  87. package/skills/build-loop/references/experiment-results-template.md +15 -3
  88. package/skills/build-loop/references/leadership.md +1 -1
  89. package/skills/build-loop/references/memory.md +14 -3
  90. package/skills/build-loop/references/modular-systems-pack.md +8 -0
  91. package/skills/build-loop/references/output-style.md +86 -0
  92. package/skills/build-loop/references/phase-1-assess.md +102 -2
  93. package/skills/build-loop/references/phase-2-plan.md +9 -1
  94. package/skills/build-loop/references/phase-3-execute.md +5 -2
  95. package/skills/build-loop/references/phase-4-review.md +85 -8
  96. package/skills/build-loop/references/phase-5-iterate.md +76 -8
  97. package/skills/build-loop/references/phase-6-learn.md +10 -17
  98. package/skills/build-loop/references/privileged-request-broker.md +254 -0
  99. package/skills/build-loop/references/resource-aware-execution.md +183 -0
  100. package/skills/build-loop/references/self-recursive-dev.md +2 -2
  101. package/skills/build-loop/references/status-output-format.md +207 -0
  102. package/skills/build-loop/references/verify-dispatch.md +56 -2
  103. package/skills/building-with-deepagents/SKILL.md +1 -1
  104. package/skills/claim-scope/SKILL.md +185 -0
  105. package/skills/color-engine/SKILL.md +103 -0
  106. package/skills/color-engine/_core.py +464 -0
  107. package/skills/color-engine/color_engine.py +175 -0
  108. package/skills/cost-rca/SKILL.md +61 -0
  109. package/skills/data-plane-worktrees/SKILL.md +139 -0
  110. package/skills/data-plane-worktrees/agents/openai.yaml +4 -0
  111. package/skills/database-practice/SKILL.md +200 -0
  112. package/skills/database-practice/references/diagnostic-queries.sql +126 -0
  113. package/skills/database-practice/references/vector-and-graph-tuning.md +208 -0
  114. package/skills/database-practice/scripts/db_table_map.py +1244 -0
  115. package/skills/database-practice/scripts/test_db_table_map.py +514 -0
  116. package/skills/debug-loop/SKILL.md +36 -6
  117. package/skills/debugging-memory/SKILL.md +32 -430
  118. package/skills/debugging-memory/references/pattern-extraction.md +4 -4
  119. package/skills/debugging-memory/references/search.md +32 -120
  120. package/skills/debugging-memory/references/store.md +32 -126
  121. package/skills/debugging-memory/references/subagent-integration.md +1 -1
  122. package/skills/decision-queue/SKILL.md +251 -0
  123. package/skills/decision-queue/assets/template.html +1242 -0
  124. package/skills/decision-queue/references/example-large-queue-batching.md +164 -0
  125. package/skills/decision-queue/scripts/regen_template_constants.py +160 -0
  126. package/skills/defenseclaw-bridge/SKILL.md +2 -2
  127. package/skills/defenseclaw-bridge/references/dc-config-mapping.md +2 -9
  128. package/skills/drain-proposals/SKILL.md +53 -0
  129. package/skills/focused-loop-builder/SKILL.md +31 -0
  130. package/skills/focused-loop-builder/references/spec-format.md +27 -0
  131. package/skills/handoff/SKILL.md +169 -8
  132. package/skills/ibr-bridge/SKILL.md +4 -1
  133. package/skills/knowledge/SKILL.md +26 -14
  134. package/skills/knowledge/references/review-mode.md +2 -3
  135. package/skills/knowledge/templates/madr-minimal.md +1 -1
  136. package/skills/mcp-builder/SKILL.md +1 -1
  137. package/skills/model-bakeoff/SKILL.md +48 -10
  138. package/skills/model-tiering/SKILL.md +92 -31
  139. package/skills/native-ax-driver/SKILL.md +38 -5
  140. package/skills/native-ax-driver/scripts/native_driver.py +278 -22
  141. package/skills/native-ax-driver/scripts/test_native_driver.py +227 -0
  142. package/skills/optimize/SKILL.md +1 -1
  143. package/skills/plugin-builder/SKILL.md +48 -1
  144. package/skills/plugin-builder/references/build-loop-phase-guidance.md +3 -4
  145. package/skills/plugin-builder/references/distribution.md +13 -2
  146. package/skills/plugin-builder/references/plugin-hygiene-lessons.md +2 -2
  147. package/skills/plugin-tests/SKILL.md +2 -2
  148. package/skills/recursive-retrospective/SKILL.md +1 -1
  149. package/skills/repo-closeout/SKILL.md +17 -0
  150. package/skills/repo-closeout/agents/openai.yaml +4 -0
  151. package/skills/repo-maintenance/SKILL.md +179 -0
  152. package/skills/repo-maintenance/agents/openai.yaml +4 -0
  153. package/skills/repo-maintenance/references/pre-public-hygiene.md +134 -0
  154. package/skills/repo-maintenance/references/repository-taxonomy.md +161 -0
  155. package/skills/repo-maintenance/references/safety-protocol.md +106 -0
  156. package/skills/repo-maintenance/references/stack-profiles.md +138 -0
  157. package/skills/repo-maintenance/scripts/audit_repo_maintenance.py +1198 -0
  158. package/skills/repo-maintenance/scripts/test_audit_repo_maintenance.py +506 -0
  159. package/skills/repository-intelligence/SKILL.md +189 -0
  160. package/skills/repository-intelligence/agents/openai.yaml +4 -0
  161. package/skills/repository-intelligence/references/assessment-rubric.md +88 -0
  162. package/skills/repository-intelligence/scripts/repository_inventory.py +347 -0
  163. package/skills/research/SKILL.md +12 -2
  164. package/skills/root-cause-analysis/SKILL.md +1 -1
  165. package/skills/runtime-parity-verification/SKILL.md +36 -1
  166. package/skills/security-methodology/SKILL.md +23 -10
  167. package/skills/security-methodology/references/agentic-handoff-templates.md +220 -0
  168. package/skills/security-methodology/references/cross-source-matrix.md +1 -1
  169. package/skills/security-methodology/references/owasp-agentic-top-10.md +1 -1
  170. package/skills/security-scan/SKILL.md +55 -15
  171. package/skills/self-improve/SKILL.md +70 -50
  172. package/skills/silent-assumptions/SKILL.md +341 -0
  173. package/skills/silent-assumptions/references/elicitation-detectors.md +342 -0
  174. package/skills/spec-writing/SKILL.md +128 -24
  175. package/skills/spec-writing/scripts/check_checklist.py +114 -15
  176. package/skills/ui-design/SKILL.md +6 -4
  177. package/skills/ui-design/references/color-engine.md +132 -0
  178. package/skills/ui-design/references/design-preferences-from-owned-apps.md +8 -8
  179. package/skills/ui-design/references/ui-guidance-sources.md +1 -1
  180. package/skills/ui-design/references/universal-design-principles.alt.md +2 -2
  181. package/plugin-artifacts/codex/.codex-plugin/plugin.json +0 -41
  182. package/plugin-artifacts/codex/AGENTS.md +0 -560
  183. package/plugin-artifacts/codex/BUILD-ARTIFACT.md +0 -5
  184. package/plugin-artifacts/codex/LICENSE +0 -202
  185. package/plugin-artifacts/codex/README.md +0 -313
  186. package/plugin-artifacts/codex/assets/build-loop-plugin-icon.png +0 -0
  187. package/plugin-artifacts/codex/docs/agent-surface-policy.md +0 -63
  188. package/plugin-artifacts/codex/references/advisor-dispatch-ladder.md +0 -62
  189. package/plugin-artifacts/codex/references/agent-role-taxonomy.md +0 -135
  190. package/plugin-artifacts/codex/references/autonomous-and-per-commit-modes.md +0 -161
  191. package/plugin-artifacts/codex/references/autonomy-config.md +0 -231
  192. package/plugin-artifacts/codex/references/backlog-system.md +0 -285
  193. package/plugin-artifacts/codex/references/capability-routing.md +0 -231
  194. package/plugin-artifacts/codex/references/codex-subagents.md +0 -106
  195. package/plugin-artifacts/codex/references/coordination-file-template.md +0 -181
  196. package/plugin-artifacts/codex/references/coordination-rules.md +0 -552
  197. package/plugin-artifacts/codex/references/dogfood-reload-checkpoint.md +0 -112
  198. package/plugin-artifacts/codex/references/halt-and-ask-protocol.md +0 -102
  199. package/plugin-artifacts/codex/references/implementer-envelope-schema.md +0 -302
  200. package/plugin-artifacts/codex/references/intent-capability-pack.md +0 -257
  201. package/plugin-artifacts/codex/references/intent-exploration-prompts.md +0 -96
  202. package/plugin-artifacts/codex/references/leadership.md +0 -72
  203. package/plugin-artifacts/codex/references/memory-systems.md +0 -261
  204. package/plugin-artifacts/codex/references/memory.md +0 -313
  205. package/plugin-artifacts/codex/references/model-tier-mapping.md +0 -296
  206. package/plugin-artifacts/codex/references/modular-systems-pack.md +0 -96
  207. package/plugin-artifacts/codex/references/phase-1-assess.md +0 -249
  208. package/plugin-artifacts/codex/references/phase-2-plan.md +0 -86
  209. package/plugin-artifacts/codex/references/phase-3-execute.md +0 -49
  210. package/plugin-artifacts/codex/references/phase-4-review.md +0 -341
  211. package/plugin-artifacts/codex/references/phase-5-iterate.md +0 -72
  212. package/plugin-artifacts/codex/references/phase-6-learn.md +0 -58
  213. package/plugin-artifacts/codex/references/recent-design-structures.md +0 -274
  214. package/plugin-artifacts/codex/references/research-trigger-policy.md +0 -140
  215. package/plugin-artifacts/codex/references/runtime-smoke-triggers.md +0 -42
  216. package/plugin-artifacts/codex/references/self-review.md +0 -234
  217. package/plugin-artifacts/codex/references/single-writer-commit-protocol.md +0 -90
  218. package/plugin-artifacts/codex/references/task-capture-policy.md +0 -68
  219. package/plugin-artifacts/codex/references/ui-io-contract.md +0 -116
  220. package/plugin-artifacts/codex/references/ui-spotcheck-protocol.md +0 -65
  221. package/plugin-artifacts/codex/references/verify-dispatch.md +0 -85
  222. package/plugin-artifacts/codex/skills/build-loop/SKILL.md +0 -381
  223. package/plugin-artifacts/codex/skills/build-loop/detect-plugins.mjs +0 -82
  224. package/plugin-artifacts/codex/skills/build-loop/eval-guide.md +0 -65
  225. package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +0 -549
  226. package/plugin-artifacts/codex/skills/build-loop/phases/fact-check.md +0 -42
  227. package/plugin-artifacts/codex/skills/build-loop/phases/ui-validation.md +0 -267
  228. package/plugin-artifacts/codex/skills/build-loop/references/advisor-dispatch-ladder.md +0 -62
  229. package/plugin-artifacts/codex/skills/build-loop/references/apple-native-planning.md +0 -439
  230. package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md +0 -161
  231. package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +0 -231
  232. package/plugin-artifacts/codex/skills/build-loop/references/codex-subagents.md +0 -106
  233. package/plugin-artifacts/codex/skills/build-loop/references/coordination.md +0 -161
  234. package/plugin-artifacts/codex/skills/build-loop/references/correction-aware-capture.md +0 -177
  235. package/plugin-artifacts/codex/skills/build-loop/references/experiment-results-template.md +0 -101
  236. package/plugin-artifacts/codex/skills/build-loop/references/independent-auditor.md +0 -72
  237. package/plugin-artifacts/codex/skills/build-loop/references/intent-capability-pack.md +0 -257
  238. package/plugin-artifacts/codex/skills/build-loop/references/intent-exploration-prompts.md +0 -96
  239. package/plugin-artifacts/codex/skills/build-loop/references/leadership.md +0 -72
  240. package/plugin-artifacts/codex/skills/build-loop/references/memory.md +0 -313
  241. package/plugin-artifacts/codex/skills/build-loop/references/modular-systems-pack.md +0 -96
  242. package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +0 -222
  243. package/plugin-artifacts/codex/skills/build-loop/references/pay-it-forward-arch.md +0 -98
  244. package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +0 -249
  245. package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +0 -86
  246. package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +0 -49
  247. package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +0 -341
  248. package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +0 -72
  249. package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +0 -58
  250. package/plugin-artifacts/codex/skills/build-loop/references/recent-design-structures.md +0 -274
  251. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/ASSESSMENT.md +0 -85
  252. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +0 -149
  253. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +0 -32
  254. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +0 -48
  255. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +0 -60
  256. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +0 -51
  257. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +0 -71
  258. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +0 -52
  259. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/trace-comparison.md +0 -202
  260. package/plugin-artifacts/codex/skills/build-loop/references/self-recursive-dev.md +0 -77
  261. package/plugin-artifacts/codex/skills/build-loop/references/self-review.md +0 -234
  262. package/plugin-artifacts/codex/skills/build-loop/references/ui-io-contract.md +0 -116
  263. package/plugin-artifacts/codex/skills/build-loop/references/verify-dispatch.md +0 -85
  264. package/plugin-artifacts/codex/skills/build-loop/scanners/audit-design-rules.mjs +0 -476
  265. package/plugin-artifacts/codex/skills/build-loop/scanners/require-visual-evidence.mjs +0 -239
  266. package/plugin-artifacts/codex/skills/build-loop/templates/backlog-item.md +0 -35
  267. package/plugin-artifacts/codex/skills/build-loop/templates/codex-worker-prompt.md +0 -100
  268. package/plugin-artifacts/codex/skills/build-loop/templates/ui-subagent-prompt.md +0 -179
  269. package/plugin-artifacts/codex/skills/build-loop/templates/ux-fix-plan.md +0 -40
  270. package/scripts/build_codex_plugin_artifact.py +0 -321
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for the read-only PostgreSQL table map.
3
+
4
+ The psql runner is monkeypatched with fixture output modelled on a real
5
+ production instance: an HNSW index with 33 lifetime scans against 402k table
6
+ inserts, a table reporting n_live_tup 0 while serving 222k index scans, and a
7
+ vector-search function whose proconfig carries enable_seqscan but no HNSW GUCs.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import copy
13
+ import importlib.util
14
+ import json
15
+ import tempfile
16
+ import unittest
17
+ from pathlib import Path
18
+ from unittest import mock
19
+
20
+
21
+ MODULE_PATH = Path(__file__).with_name("db_table_map.py")
22
+ SPEC = importlib.util.spec_from_file_location("db_table_map", MODULE_PATH)
23
+ assert SPEC and SPEC.loader
24
+ MODULE = importlib.util.module_from_spec(SPEC)
25
+ SPEC.loader.exec_module(MODULE)
26
+
27
+ MB = 1024 * 1024
28
+
29
+
30
+ def psql_output(sections: dict[str, list[list[object]]]) -> str:
31
+ lines: list[str] = []
32
+ for name, rows in sections.items():
33
+ lines.append(f"{MODULE.SECTION_MARK}{name}{MODULE.SECTION_END}")
34
+ for row in rows:
35
+ lines.append("\t".join(str(cell) for cell in row))
36
+ return "\n".join(lines) + "\n"
37
+
38
+
39
+ FIXTURE: dict[str, list[list[object]]] = {
40
+ "window": [
41
+ [
42
+ "2026-02-01 00:00:00+00",
43
+ "2026-09-05 12:00:00+00",
44
+ "",
45
+ "appdb",
46
+ "PostgreSQL 17.4 on aarch64-unknown-linux-gnu",
47
+ ]
48
+ ],
49
+ "settings": [
50
+ ["max_connections", "60", ""],
51
+ ["shared_buffers", "32768", "8kB"],
52
+ ["work_mem", "3584", "kB"],
53
+ ],
54
+ # schema name total heap idx toast live dead ins upd del seq idx_scan vac ana reloptions
55
+ "tables": [
56
+ [
57
+ "public", "article_embedding_chunks",
58
+ 3 * 1024 * MB, 400 * MB, 2400 * MB, 8 * MB,
59
+ 398_000, 1_200, 402_158, 12, 40, 90, 33,
60
+ "2026-09-01 03:00:00+00", "2026-09-01 03:10:00+00", "",
61
+ ],
62
+ [
63
+ "public", "article_categories",
64
+ 40 * MB, 20 * MB, 20 * MB, 0,
65
+ 0, 0, 5_000, 10, 4_900, 12, 222_793,
66
+ "", "", "",
67
+ ],
68
+ [
69
+ "public", "ingest_queue",
70
+ 8 * MB, 4 * MB, 4 * MB, 0,
71
+ 0, 900, 12_000, 0, 12_000, 3, 0,
72
+ "", "", "fillfactor=70",
73
+ ],
74
+ [
75
+ "public", "reference_lookup",
76
+ 1 * MB, 512 * 1024, 512 * 1024, 0,
77
+ 148, 0, 0, 0, 0, 40, 0,
78
+ "", "", "",
79
+ ],
80
+ [
81
+ "public", "legacy_import_staging",
82
+ 0, 0, 0, 0,
83
+ 0, 0, 0, 0, 0, 0, 0,
84
+ "", "", "",
85
+ ],
86
+ ],
87
+ "indexes": [
88
+ [
89
+ "public", "article_embedding_chunks", "idx_embedding_chunks_hnsw",
90
+ "hnsw", 2203 * MB, 33,
91
+ "CREATE INDEX idx_embedding_chunks_hnsw ON public.article_embedding_chunks "
92
+ "USING hnsw (embedding vector_cosine_ops) WITH (m='16', ef_construction='64')",
93
+ ],
94
+ [
95
+ "public", "article_categories", "article_categories_pkey",
96
+ "btree", 8 * MB, 222_793,
97
+ "CREATE UNIQUE INDEX article_categories_pkey ON public.article_categories USING btree (id)",
98
+ ],
99
+ ],
100
+ "special_columns": [
101
+ ["public", "article_embedding_chunks", "embedding", "vector"],
102
+ ["public", "article_categories", "search_vector", "tsvector"],
103
+ ],
104
+ "foreign_keys": [
105
+ [
106
+ "article_categories_article_id_fkey",
107
+ "public", "article_categories",
108
+ "public", "article_embedding_chunks",
109
+ ]
110
+ ],
111
+ "functions": [
112
+ ["public", "similarity_search_article_chunks", "enable_seqscan=off", "vector"],
113
+ ["public", "refresh_counters", "search_path=public", ""],
114
+ # A platform function that only carries a search_path GUC. Its body uses
115
+ # `<>`, which a bare `<+>` regex would misread as a vector operator.
116
+ ["storage", "search", 'search_path=""', ""],
117
+ ],
118
+ "database_temp": [["336253", str(2053 * 1024 * MB), "900000", "8100000", "0"]],
119
+ "statements": [
120
+ [
121
+ "221092", "220000000", "996.0", "39.0", "1.00", "0",
122
+ "insert into article_embedding_chunks (article_id, embedding) values ($1, $2)",
123
+ ],
124
+ [
125
+ "5000", "1500000", "300.0", "4.0", "12.00", "10",
126
+ "select * from entities where properties->>'name' ilike $1 and similarity(name, $2) > $3",
127
+ ],
128
+ ],
129
+ "statement_temp": [["91", "5100000", "with recent as (select ...) select * from recent"]],
130
+ }
131
+
132
+
133
+ def fake_runner(has_pgss: bool = True, fixture: dict[str, list[list[object]]] | None = None):
134
+ """Return a run_psql stand-in: first call is the extension probe."""
135
+ calls: list[str] = []
136
+ payload = FIXTURE if fixture is None else fixture
137
+
138
+ def runner(dsn: str, script: str, timeout_s: int, psql: str = "psql") -> str:
139
+ calls.append(script)
140
+ MODULE.assert_read_only(script)
141
+ if "pg_extension" in script:
142
+ return psql_output({"probe": [["1" if has_pgss else "0"]]})
143
+ sections = dict(payload)
144
+ if not has_pgss:
145
+ sections.pop("statements", None)
146
+ sections.pop("statement_temp", None)
147
+ return psql_output(sections)
148
+
149
+ runner.calls = calls # type: ignore[attr-defined]
150
+ return runner
151
+
152
+
153
+ def build_fixture_map(has_pgss: bool = True, fixture=None) -> dict:
154
+ runner = fake_runner(has_pgss, fixture)
155
+ with mock.patch.object(MODULE, "run_psql", runner):
156
+ sections, flag = MODULE.collect("postgres://u:p@h/db", 20)
157
+ return MODULE.build_map(sections, MODULE.DEFAULT_DOMAIN_RULES, "postgres://u:***@h/db", flag)
158
+
159
+
160
+ class DsnTests(unittest.TestCase):
161
+ def test_strips_psql_unsupported_uri_params(self) -> None:
162
+ dsn = (
163
+ "postgres://user:pw@host:6543/postgres"
164
+ "?pgbouncer=true&connection_limit=1&pool_timeout=0&sslmode=require"
165
+ )
166
+ cleaned = MODULE.sanitize_dsn(dsn)
167
+ self.assertNotIn("pgbouncer", cleaned)
168
+ self.assertNotIn("connection_limit", cleaned)
169
+ self.assertNotIn("pool_timeout", cleaned)
170
+ self.assertIn("sslmode=require", cleaned)
171
+ self.assertIn("user:pw@host:6543", cleaned)
172
+
173
+ def test_leaves_a_clean_uri_and_a_keyword_dsn_untouched(self) -> None:
174
+ clean = "postgres://user@host/db?sslmode=require"
175
+ self.assertEqual(MODULE.sanitize_dsn(clean), clean)
176
+ kv = "host=localhost dbname=app user=app"
177
+ self.assertEqual(MODULE.sanitize_dsn(kv), kv)
178
+
179
+ def test_redacts_the_password(self) -> None:
180
+ self.assertEqual(
181
+ MODULE.redact_dsn("postgres://user:secret@host:5432/db?sslmode=require"),
182
+ "host:5432/db",
183
+ )
184
+ self.assertNotIn("secret", MODULE.redact_dsn("host=h password=secret"))
185
+ self.assertEqual(MODULE.redact_dsn("host=h port=5432 dbname=app password=secret"), "h:5432/app")
186
+ # No URL shape and no user survive: secret scanners flag both.
187
+ self.assertNotIn("://", MODULE.redact_dsn("postgres://user:secret@host:5432/db"))
188
+ self.assertNotIn("user", MODULE.redact_dsn("postgres://user:secret@host:5432/db"))
189
+
190
+ def test_resolve_dsn_prefers_flag_then_env(self) -> None:
191
+ env = {"DATABASE_URL": "a", "DIRECT_URL": "b"}
192
+ self.assertEqual(MODULE.resolve_dsn("flag", env), "flag")
193
+ self.assertEqual(MODULE.resolve_dsn(None, env), "a")
194
+ self.assertEqual(MODULE.resolve_dsn(None, {"DIRECT_URL": "b"}), "b")
195
+ with self.assertRaises(SystemExit):
196
+ MODULE.resolve_dsn(None, {})
197
+
198
+
199
+ class ScriptSafetyTests(unittest.TestCase):
200
+ def test_script_is_wrapped_in_a_read_only_transaction(self) -> None:
201
+ script = MODULE.build_script(MODULE.SECTION_SQL, 20)
202
+ self.assertTrue(script.startswith("BEGIN READ ONLY;"))
203
+ self.assertIn("SET LOCAL statement_timeout = '20s';", script)
204
+ self.assertTrue(script.strip().endswith("ROLLBACK;"))
205
+ MODULE.assert_read_only(script)
206
+
207
+ def test_statement_timeout_is_configurable(self) -> None:
208
+ self.assertIn(
209
+ "SET LOCAL statement_timeout = '5s';", MODULE.build_script(MODULE.SECTION_SQL, 5)
210
+ )
211
+
212
+ def test_function_query_excludes_extension_internals(self) -> None:
213
+ # Regression: a live run flagged 107 pgvector-internal C functions
214
+ # (vector_add, halfvec_in, ...) as "vector function without HNSW GUCs".
215
+ sql = dict(MODULE.SECTION_SQL)["functions"]
216
+ self.assertIn("deptype = 'e'", sql)
217
+ self.assertIn("lanname in ('sql', 'plpgsql')", sql)
218
+ self.assertIn("select distinct", sql)
219
+
220
+ def test_function_query_escapes_the_plus_and_skips_platform_schemas(self) -> None:
221
+ sql = dict(MODULE.SECTION_SQL)["functions"]
222
+ # `<+>` unescaped matches the plpgsql not-equals operator `<>`, which
223
+ # flags every platform function in a Supabase database.
224
+ self.assertIn(r"<\+>", sql)
225
+ self.assertNotIn("<+>", sql)
226
+ self.assertIn("'storage'", sql)
227
+ self.assertIn("prorettype", sql)
228
+ self.assertIn("'vector', 'halfvec', 'sparsevec'", sql)
229
+
230
+ def test_ddl_and_dml_are_refused(self) -> None:
231
+ for bad in (
232
+ "BEGIN READ ONLY; drop table article_embeddings; ROLLBACK;",
233
+ "BEGIN READ ONLY; insert into t values (1); ROLLBACK;",
234
+ "BEGIN READ ONLY; update t set a = 1; ROLLBACK;",
235
+ "BEGIN READ ONLY; set work_mem = '1GB'; ROLLBACK;",
236
+ "BEGIN READ ONLY; vacuum full t; ROLLBACK;",
237
+ ):
238
+ with self.assertRaises(ValueError):
239
+ MODULE.assert_read_only(bad)
240
+
241
+ def test_run_psql_refuses_before_spawning_a_process(self) -> None:
242
+ with mock.patch.object(MODULE.subprocess, "run") as run:
243
+ with self.assertRaises(ValueError):
244
+ MODULE.run_psql("postgres://h/db", "truncate table t;", 20)
245
+ run.assert_not_called()
246
+
247
+
248
+ class ParsingTests(unittest.TestCase):
249
+ def test_sections_split_on_markers(self) -> None:
250
+ parsed = MODULE.parse_sections(psql_output({"a": [["1", "x"]], "b": []}))
251
+ self.assertEqual(parsed["a"], [["1", "x"]])
252
+ self.assertEqual(parsed["b"], [])
253
+
254
+ def test_shared_buffers_blocks_convert_to_bytes(self) -> None:
255
+ self.assertEqual(
256
+ MODULE._setting_bytes({"setting": "32768", "unit": "8kB"}), 256 * MB
257
+ )
258
+
259
+
260
+ class LivenessTests(unittest.TestCase):
261
+ def setUp(self) -> None:
262
+ self.data = build_fixture_map()
263
+ self.tables = {t["key"]: t for t in self.data["tables"]}
264
+
265
+ def test_index_scans_beat_a_zero_live_tuple_estimate(self) -> None:
266
+ table = self.tables["article_categories"]
267
+ self.assertEqual(table["n_live_tup"], 0)
268
+ self.assertEqual(table["liveness"], "live")
269
+ self.assertIn("222,793", table["liveness_reason"])
270
+
271
+ def test_written_and_drained_reads_as_written_only(self) -> None:
272
+ table = self.tables["ingest_queue"]
273
+ self.assertEqual(table["liveness"], "written-only")
274
+ self.assertIn("drained queue", table["liveness_reason"])
275
+
276
+ def test_populated_but_untouched_reads_as_idle(self) -> None:
277
+ self.assertEqual(self.tables["reference_lookup"]["liveness"], "idle")
278
+
279
+ def test_no_reads_and_no_writes_reads_as_never_written(self) -> None:
280
+ self.assertEqual(self.tables["legacy_import_staging"]["liveness"], "never-written")
281
+
282
+ def test_estimates_are_labelled_as_estimates(self) -> None:
283
+ self.assertIn("estimates", self.tables["ingest_queue"]["estimates_note"])
284
+
285
+
286
+ class CollectionTests(unittest.TestCase):
287
+ def test_table_facts_indexes_columns_and_keys_are_attached(self) -> None:
288
+ data = build_fixture_map()
289
+ tables = {t["key"]: t for t in data["tables"]}
290
+ chunks = tables["article_embedding_chunks"]
291
+ self.assertEqual(data["table_count"], 5)
292
+ self.assertEqual(chunks["domain"], "embedding")
293
+ self.assertEqual(chunks["indexes"][0]["access_method"], "hnsw")
294
+ self.assertEqual(chunks["vector_columns"], [{"column": "embedding", "type": "vector"}])
295
+ self.assertEqual(
296
+ tables["article_categories"]["tsvector_columns"],
297
+ [{"column": "search_vector", "type": "tsvector"}],
298
+ )
299
+ self.assertEqual(
300
+ tables["article_categories"]["fk_out"][0]["references"], "article_embedding_chunks"
301
+ )
302
+ self.assertEqual(
303
+ tables["article_embedding_chunks"]["fk_in"][0]["from"], "article_categories"
304
+ )
305
+ self.assertEqual(tables["article_categories"]["domain"], "taxonomy")
306
+ # "staging" must not match the taxonomy rule through the substring "tag".
307
+ self.assertEqual(tables["legacy_import_staging"]["domain"], MODULE.DEFAULT_DOMAIN)
308
+ self.assertEqual(tables["ingest_queue"]["reloptions"], "fillfactor=70")
309
+ self.assertEqual(data["shared_buffers_bytes"], 256 * MB)
310
+
311
+ def test_functions_carry_their_proconfig(self) -> None:
312
+ data = build_fixture_map()
313
+ function = next(
314
+ f for f in data["functions"] if f["name"] == "similarity_search_article_chunks"
315
+ )
316
+ self.assertEqual(function["proconfig"], ["enable_seqscan=off"])
317
+ self.assertTrue(function["uses_vector_ops"])
318
+
319
+ def test_custom_domain_rules_override_the_defaults(self) -> None:
320
+ runner = fake_runner()
321
+ with mock.patch.object(MODULE, "run_psql", runner):
322
+ sections, flag = MODULE.collect("postgres://h/db", 20)
323
+ data = MODULE.build_map(sections, [["ingestion", "queue|staging"]], "dsn", flag)
324
+ tables = {t["key"]: t for t in data["tables"]}
325
+ self.assertEqual(tables["ingest_queue"]["domain"], "ingestion")
326
+ self.assertEqual(tables["article_categories"]["domain"], MODULE.DEFAULT_DOMAIN)
327
+
328
+ def test_missing_pg_stat_statements_degrades_instead_of_failing(self) -> None:
329
+ data = build_fixture_map(has_pgss=False)
330
+ self.assertFalse(data["pg_stat_statements"])
331
+ self.assertIsNone(data["statements"])
332
+ markdown = MODULE.render_markdown(data)
333
+ self.assertIn("`pg_stat_statements` is not installed", markdown)
334
+
335
+ def test_load_domain_rules_rejects_a_malformed_file(self) -> None:
336
+ with tempfile.TemporaryDirectory() as tmp:
337
+ path = Path(tmp) / "rules.json"
338
+ path.write_text(json.dumps([["only-one-field"]]), encoding="utf-8")
339
+ with self.assertRaises(SystemExit):
340
+ MODULE.load_domain_rules(str(path))
341
+
342
+
343
+ class ShapeTests(unittest.TestCase):
344
+ def setUp(self) -> None:
345
+ self.shapes = build_fixture_map()["shapes"]
346
+ self.by_shape: dict[str, list[dict]] = {}
347
+ for finding in self.shapes:
348
+ self.by_shape.setdefault(finding["shape"], []).append(finding)
349
+
350
+ def test_cold_hnsw_index_matches_index_maintenance_on_writes(self) -> None:
351
+ finding = self.by_shape["index-maintenance-on-writes"][0]
352
+ self.assertIn("idx_embedding_chunks_hnsw", finding["object"])
353
+ self.assertIn("33 lifetime scans", finding["evidence"])
354
+ self.assertIn("402,158", finding["evidence"])
355
+
356
+ def test_oversized_hnsw_index_matches_the_insert_shape(self) -> None:
357
+ finding = self.by_shape["vector-insert-above-cache"][0]
358
+ self.assertIn("2.2 GB", finding["evidence"])
359
+ self.assertIn("256.0 MB", finding["evidence"])
360
+
361
+ def test_vector_function_without_hnsw_gucs_is_flagged_as_drift(self) -> None:
362
+ drift = [
363
+ f for f in self.by_shape["vector-read-above-cache"]
364
+ if "similarity_search_article_chunks" in f["object"]
365
+ ]
366
+ self.assertEqual(len(drift), 1)
367
+ self.assertIn("enable_seqscan=off", drift[0]["evidence"])
368
+ self.assertIn("ALTER FUNCTION", drift[0]["action"])
369
+
370
+ def test_a_platform_function_with_no_vector_ops_is_never_flagged(self) -> None:
371
+ flagged = [f["object"] for f in self.shapes]
372
+ self.assertNotIn("storage.search", flagged)
373
+ self.assertNotIn("public.refresh_counters", flagged)
374
+
375
+ def test_jsonb_plus_trigram_statement_matches_the_per_row_shape(self) -> None:
376
+ finding = self.by_shape["per-row-jsonb-trigram"][0]
377
+ self.assertIn("5,000 calls", finding["evidence"])
378
+
379
+ def test_every_finding_names_a_known_shape(self) -> None:
380
+ for finding in self.shapes:
381
+ self.assertIn(finding["shape"], MODULE.SHAPES)
382
+
383
+
384
+ class MarkdownTests(unittest.TestCase):
385
+ def test_every_section_is_rendered(self) -> None:
386
+ markdown = MODULE.render_markdown(build_fixture_map())
387
+ for heading in MODULE.MD_SECTIONS:
388
+ self.assertIn(f"## {heading}", markdown)
389
+ self.assertNotIn(f"## {MODULE.MD_DIFF_SECTION}", markdown)
390
+
391
+ def test_body_carries_the_counter_window_and_the_estimate_warning(self) -> None:
392
+ markdown = MODULE.render_markdown(build_fixture_map())
393
+ self.assertIn("2026-02-01 00:00:00+00", markdown)
394
+ self.assertIn("never reset", markdown)
395
+ self.assertIn("planner estimates", markdown)
396
+ self.assertIn("no DDL or DML was issued", markdown)
397
+
398
+ def test_liveness_section_lists_the_non_live_tables(self) -> None:
399
+ markdown = MODULE.render_markdown(build_fixture_map())
400
+ self.assertIn("`legacy_import_staging`", markdown)
401
+ self.assertIn("`ingest_queue`", markdown)
402
+
403
+ def test_diff_section_appears_only_with_a_previous_map(self) -> None:
404
+ current = build_fixture_map()
405
+ markdown = MODULE.render_markdown(current, MODULE.diff_maps(current, current))
406
+ self.assertIn(f"## {MODULE.MD_DIFF_SECTION}", markdown)
407
+
408
+
409
+ class DiffTests(unittest.TestCase):
410
+ def setUp(self) -> None:
411
+ self.current = build_fixture_map()
412
+ self.previous = copy.deepcopy(self.current)
413
+ self.previous["generated_at"] = "2026-08-01T00:00:00Z"
414
+
415
+ def _diff(self) -> dict:
416
+ return MODULE.diff_maps(self.previous, self.current)
417
+
418
+ def test_identical_maps_report_no_change(self) -> None:
419
+ diff = self._diff()
420
+ self.assertEqual(diff["new_tables"], [])
421
+ self.assertEqual(diff["removed_tables"], [])
422
+ self.assertEqual(diff["size_deltas"], [])
423
+ self.assertEqual(diff["stalled_indexes"], [])
424
+ self.assertEqual(diff["liveness_changes"], [])
425
+
426
+ def test_new_and_removed_tables_are_named(self) -> None:
427
+ self.previous["tables"] = [
428
+ t for t in self.previous["tables"] if t["key"] != "ingest_queue"
429
+ ]
430
+ self.previous["tables"].append(dict(self.current["tables"][0], key="dropped_table"))
431
+ diff = self._diff()
432
+ self.assertEqual(diff["new_tables"], ["ingest_queue"])
433
+ self.assertEqual(diff["removed_tables"], ["dropped_table"])
434
+
435
+ def test_size_moves_over_ten_percent_are_reported(self) -> None:
436
+ for table in self.previous["tables"]:
437
+ if table["key"] == "article_embedding_chunks":
438
+ table["total_bytes"] = int(table["total_bytes"] / 2)
439
+ if table["key"] == "article_categories":
440
+ table["total_bytes"] = int(table["total_bytes"] * 1.05)
441
+ diff = self._diff()
442
+ moved = {entry["table"] for entry in diff["size_deltas"]}
443
+ self.assertIn("article_embedding_chunks", moved)
444
+ self.assertNotIn("article_categories", moved)
445
+ self.assertGreater(diff["size_deltas"][0]["pct"], 10)
446
+
447
+ def test_an_index_read_flat_while_inserts_grew_is_flagged(self) -> None:
448
+ for table in self.previous["tables"]:
449
+ if table["key"] == "article_embedding_chunks":
450
+ table["n_tup_ins"] = 300_000
451
+ diff = self._diff()
452
+ stalled = diff["stalled_indexes"]
453
+ self.assertEqual(len(stalled), 1)
454
+ self.assertEqual(stalled[0]["index"], "idx_embedding_chunks_hnsw")
455
+ self.assertEqual(stalled[0]["scan_delta"], 0)
456
+ self.assertEqual(stalled[0]["insert_delta"], 102_158)
457
+
458
+ def test_liveness_flips_are_reported(self) -> None:
459
+ for table in self.previous["tables"]:
460
+ if table["key"] == "ingest_queue":
461
+ table["liveness"] = "live"
462
+ diff = self._diff()
463
+ self.assertEqual(diff["liveness_changes"][0]["table"], "ingest_queue")
464
+ self.assertEqual(diff["liveness_changes"][0]["after"], "written-only")
465
+
466
+ def test_a_counter_reset_between_maps_invalidates_the_deltas(self) -> None:
467
+ self.previous["window"]["stats_reset"] = "2026-07-01 00:00:00+00"
468
+ diff = self._diff()
469
+ self.assertTrue(diff["counters_reset_between_maps"])
470
+ rendered = "\n".join(MODULE._render_diff(diff)).lower()
471
+ self.assertIn("counters reset between the two maps", rendered)
472
+
473
+
474
+ class CliTests(unittest.TestCase):
475
+ def test_main_writes_both_artifacts_and_the_diff(self) -> None:
476
+ with tempfile.TemporaryDirectory() as tmp:
477
+ prev_path = Path(tmp) / "prev.json"
478
+ previous = build_fixture_map()
479
+ for table in previous["tables"]:
480
+ if table["key"] == "article_embedding_chunks":
481
+ table["n_tup_ins"] = 300_000
482
+ prev_path.write_text(json.dumps(previous), encoding="utf-8")
483
+
484
+ out_json = Path(tmp) / "audits" / "map.json"
485
+ out_md = Path(tmp) / "audits" / "map.md"
486
+ runner = fake_runner()
487
+ with mock.patch.object(MODULE, "run_psql", runner):
488
+ code = MODULE.main(
489
+ [
490
+ "--dsn",
491
+ "postgres://u:secret@h/db?pgbouncer=true",
492
+ "--out-json",
493
+ str(out_json),
494
+ "--out-md",
495
+ str(out_md),
496
+ "--prev",
497
+ str(prev_path),
498
+ "--statement-timeout",
499
+ "10",
500
+ ]
501
+ )
502
+ self.assertEqual(code, 0)
503
+ data = json.loads(out_json.read_text(encoding="utf-8"))
504
+ markdown = out_md.read_text(encoding="utf-8")
505
+
506
+ self.assertNotIn("secret", data["dsn"])
507
+ self.assertNotIn("secret", markdown)
508
+ self.assertEqual(data["diff"]["stalled_indexes"][0]["index"], "idx_embedding_chunks_hnsw")
509
+ self.assertIn(f"## {MODULE.MD_DIFF_SECTION}", markdown)
510
+ self.assertIn("SET LOCAL statement_timeout = '10s';", runner.calls[-1])
511
+
512
+
513
+ if __name__ == "__main__": # pragma: no cover
514
+ unittest.main()
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: debug-loop
3
- description: Use when a fix didn't hold, `/build-loop:debug` is invoked, the user asks for root cause analysis, memory lookup returns LIKELY_MATCH/WEAK_SIGNAL/NO_MATCH, or Review-B Validate fails. Deep iterative debugging loop — not for known fixes or trivial issues. NOT for blameless postmortem system-lever analysis (use `root-cause-analysis`) or memory search/store (use `debugging-memory`).
3
+ description: Iteratively investigate and fix a live, currently-broken bug. Use when a fix didn't hold, `/build-loop:debug` is invoked, or the user wants it debugged right now. Not for checking whether this bug happened before (use `debugging-memory` first) or a post-fix blameless analysis (use `root-cause-analysis`).
4
4
  version: 1.1.0
5
- user-invocable: true
5
+ user-invocable: false
6
6
  ---
7
7
 
8
8
  <!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
@@ -29,7 +29,7 @@ Before entering the loop, assess whether it's warranted. The trigger is the **ve
29
29
 
30
30
  **Goal**: Understand what's actually failing and why, not just what it looks like.
31
31
 
32
- 1. **Search debugging memory** — invoke `build-loop:debugging-memory` with the symptom. Note related incidents from local build-loop memory and optional standalone Coding Debugger memory when available.
32
+ 1. **Search debugging memory** — invoke `build-loop:debugging-memory` with the symptom. Its native search reads the project's structured `.claude/memory/` store.
33
33
  2. **Reproduce the issue** — identify exact steps, commands, or conditions that trigger the bug
34
34
  3. **Deploy root-cause-investigator agent** — pass the symptom and reproduction steps for causal tree analysis. The agent explores multiple branches (not a single chain), prioritizes by evidence strength, and prunes with evidence
35
35
  4. **Research gate** — if the investigator flags unfamiliar error codes, library behavior, or version-specific issues:
@@ -37,9 +37,36 @@ Before entering the loop, assess whether it's warranted. The trigger is the **ve
37
37
  - Document what was searched and what was found
38
38
  - If search is unavailable, document what SHOULD be searched
39
39
  5. **Assess completeness** — does the investigation explain ALL reported symptoms? Check for multi-causal bugs (2+ independent root causes)
40
+ 6. **Qualify every observation before it becomes evidence** — the gate below. Run it on each number and each instrument reading, INCLUDING the ones that agree with you
40
41
 
41
42
  **Output**: Causal tree (with confirmed and pruned branches), reproduction steps, evidence gathered, research performed
42
43
 
44
+ ### Evidence qualification gate (run per observation, before it enters the report)
45
+
46
+ Three questions. They take seconds and they catch the failure class that survives every
47
+ other check — a *correct* instrument, read correctly, generalized one step too far.
48
+
49
+ | # | Question | Catches |
50
+ |---|---|---|
51
+ | 1 | **Unit** — one unit of this number is one *what*? | `zcard(queue:prioritized)=20,823` quoted as "20,823 articles". Job ids were `<entity>-<retry-bucket>`, so each retry minted a new id for the same entity. Walk it: 20,823 jobs ÷ **5,001 distinct ids** = 4.17x duplication; of those 5,001, **2,178 were already deleted** from the DB, leaving real work ≈2,704. **Overstated 7x** (20,823 ÷ 2,704). The two ratios measure different things — keep the distinct count between them or they read as contradictory |
52
+ | 2 | **Instance** — is the thing I checked the only one of its kind? Enumerate before generalizing. | "Redis is healthy" from one connection, when the code built **two** — the healthy singleton, and a separate BullMQ-owned connection that the hanging call path actually used |
53
+ | 3 | **Second source** — what else can answer this same question? Run it and diff. | Queue said 20,823, database said 3,531. Both were queried; neither was reconciled until a human asked |
54
+
55
+ **Disagreement between two sources is not noise to resolve — it IS the finding.** Write the
56
+ reconciliation as a script so it stays runnable and re-runnable, not as a one-off query.
57
+
58
+ **The gate has a known hole: it fires on conclusions, not on narration.** Observations that
59
+ merely *support* the current hypothesis slip through unexamined, because they never feel like
60
+ claims. Apply the gate hardest to the numbers that agree with you — a measurement that
61
+ confirms the story is exactly the one nobody re-derives. Any number crossing from an
62
+ instrument into a sentence is a claim, whatever its grammatical role.
63
+
64
+ Corollary for tools: a listing/introspection API returning empty is not proof of absence
65
+ (`Queue.getWorkers()` returned `[]` for a worker that was provably alive and heartbeating).
66
+ Confirm absence against a second, independent observation channel — see
67
+ `skills/build-loop/references/verify-dispatch.md` and the `verify_the_instrument_before_the_finding`
68
+ lesson.
69
+
43
70
  ### Root-Cause Frameworks
44
71
 
45
72
  Use the lightest framework that fits the failure. Stack frameworks only when the current one stalls.
@@ -170,8 +197,7 @@ Every item in the report gets one marker:
170
197
 
171
198
  > **Durable post-failure RCA:** for the blameless durable-lever pass (creation+escape paths, action-strength hierarchy, lever+actuator, regression artifact, spread check), delegate to the shared `references/root-cause-analysis/` suite. This skill/agent finds and fixes the live issue; that suite is the post-failure prevention layer.
172
199
 
173
- - **Store the incident** as a native `.build-loop/issues/*.md` note for future retrieval
174
- - **Record the outcome** through standalone Coding Debugger only if that optional plugin supplied the prior incident
200
+ - **Store the incident** through `build-loop:debugging-memory` `{op:"store"}` so it lands in the same structured store used by search
175
201
  - **Write state** to `.build-loop/debug-loop/scorecard.md`
176
202
 
177
203
  ## Iteration Rules
@@ -220,7 +246,7 @@ If the bundled assessor coverage isn't enough (e.g., the failure crosses a domai
220
246
  Skill("build-loop:debugging-memory") with input { op: "assess", symptom, scope: "global", calledBy: "debug-loop", reason: "stuck-iteration" }
221
247
  ```
222
248
 
223
- The native skill includes domain-specific assessors (api / database / frontend / performance). It uses build-loop local memory by default and may use standalone Coding Debugger for cross-build memory when available; otherwise it falls back to grep across `.build-loop/issues/` and `.build-loop/feedback.md` with narrower coverage.
249
+ The native skill includes domain-specific assessors (api / database / frontend / performance) and uses Build Loop's project-local structured debugger memory.
224
250
 
225
251
  ### State Tracking
226
252
 
@@ -278,3 +304,7 @@ MEMORY SEARCH → INVESTIGATE → HYPOTHESIZE → FIX → VERIFY → SCORE
278
304
  | Declaring victory without evidence | Every claim needs a ✅/⚠️/❓ marker |
279
305
  | Skipping research when stuck | If you don't know why something behaves this way, search for it |
280
306
  | Hiding uncertainty | ⚠️ and ❓ are not failures — they're honest. Hiding them is the failure |
307
+ | Quoting a counter without its unit | Name the unit before the magnitude. "20,823 *what*?" A queue depth is jobs; a composite job id defeats dedup and inflates it |
308
+ | Scrutinizing only the evidence that challenges you | Run the qualification gate on confirming observations too — those are the ones that ship wrong |
309
+ | Treating an empty listing as proof of absence | Absence needs a second observation channel; introspection APIs return `[]` for live things |
310
+ | Two sources, pick the convenient one | Diff them. The gap is the finding, and the diff belongs in a script |