@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
@@ -306,7 +306,15 @@ def _project_relpath(project: str) -> Path:
306
306
 
307
307
 
308
308
  def ensure_project_scaffold(dest: Path, project: str) -> Path:
309
- """Create the per-project raw-source scaffold and return the project dir."""
309
+ """Create the per-project raw-source scaffold and return the project dir.
310
+
311
+ Stable-ID-at-init hook (memory-identity-graph Phase 1): the first time a
312
+ project folder is scaffolded, freeze ``id == project`` in the v2 project
313
+ registry (``<dest>/config/projects.yaml``). This is what makes a later
314
+ repo rename a no-op — the id is registered once and never re-derived; the
315
+ old dirname becomes an alias instead of orphaning the store. Best-effort:
316
+ a registry-write failure never blocks scaffolding.
317
+ """
310
318
  project_dir = dest / "projects" / _project_relpath(project)
311
319
  raw_dir = project_dir / "raw"
312
320
  for subdir in PROJECT_RAW_SUBDIRS:
@@ -315,9 +323,33 @@ def ensure_project_scaffold(dest: Path, project: str) -> Path:
315
323
  keep = leaf / KEEP_FILENAME
316
324
  if not keep.exists():
317
325
  keep.write_text("", encoding="utf-8")
326
+ _register_stable_id(dest, project)
318
327
  return project_dir
319
328
 
320
329
 
330
+ def _register_stable_id(dest: Path, project: str) -> None:
331
+ """Freeze ``id == project`` in the v2 registry under ``dest`` (best-effort).
332
+
333
+ Skips the ``_unscoped`` sentinel and any sub-component slug (``a/b``) —
334
+ those are not standalone project identities.
335
+ """
336
+ if not project or project == "_unscoped" or "/" in project:
337
+ return
338
+ try:
339
+ scripts_dir = Path(__file__).resolve().parent
340
+ if str(scripts_dir) not in sys.path:
341
+ sys.path.insert(0, str(scripts_dir))
342
+ import project_registry # type: ignore # noqa: PLC0415
343
+
344
+ project_registry.register_project(
345
+ project,
346
+ slug=project,
347
+ registry_path=dest / "config" / "projects.yaml",
348
+ )
349
+ except Exception: # noqa: BLE001 — init hook must never break scaffolding
350
+ pass
351
+
352
+
321
353
  def link_private_repo(dest: Path, repo_url: str) -> tuple[bool, str]:
322
354
  """Clone a private repo INTO the dest path. Requires dest to be empty or non-existent.
323
355
 
@@ -146,17 +146,24 @@ def _lane_dirs(project: str | None) -> list[tuple[str, Path]]:
146
146
  project_decisions_dir,
147
147
  project_lessons_dir,
148
148
  project_product_dir,
149
+ project_research_dir,
150
+ project_root,
149
151
  top_level_lessons_dir,
152
+ memory_store_root,
150
153
  )
151
154
  if project:
152
155
  return [
153
156
  ("lessons", project_lessons_dir(project)),
154
157
  ("decisions", project_decisions_dir(project)),
155
158
  ("product", project_product_dir(project)),
159
+ ("research", project_research_dir(project)),
160
+ ("references", project_root(project) / "references"),
156
161
  ]
157
162
  else:
158
163
  return [
159
164
  ("lessons", top_level_lessons_dir()),
165
+ ("research", memory_store_root() / "research"),
166
+ ("references", memory_store_root() / "references"),
160
167
  ]
161
168
 
162
169
 
@@ -193,8 +200,12 @@ def ingest(
193
200
  for lane_name, lane_dir in lanes:
194
201
  if not lane_dir.exists():
195
202
  continue
196
- for md_path in sorted(lane_dir.glob("*.md")):
197
- if md_path.name in {"INDEX.md", "MEMORY.md", "README.md"}:
203
+ for md_path in sorted(lane_dir.rglob("*.md")):
204
+ relative_parts = md_path.relative_to(lane_dir).parts[:-1]
205
+ if md_path.name in {"INDEX.md", "MEMORY.md", "README.md"} or any(
206
+ part in {"raw-originals", "archive", "indexes", "raw"}
207
+ for part in relative_parts
208
+ ):
198
209
  continue
199
210
  summary["total_scanned"] += 1
200
211
  try:
@@ -74,16 +74,22 @@ def _unpack_floats(blob: bytes) -> list[float]:
74
74
  return list(struct.unpack(f"{n}f", blob))
75
75
 
76
76
 
77
- def _try_embed(text: str) -> list[float] | None:
78
- """Return embedding vector or None if backend unavailable."""
77
+ def _try_embed(text: str) -> tuple[list[float] | None, str | None]:
78
+ """Return (embedding vector, model name), or (None, None) if unavailable.
79
+
80
+ The model name is returned so the reranker can refuse to score stored
81
+ vectors that came from a DIFFERENT embedding model. Two models can share a
82
+ dimension (bge-m3 and mxbai-embed-large-v1 are both 1024), so a mismatch
83
+ raises no error — it silently produces meaningless cosine scores.
84
+ """
79
85
  if os.environ.get("EMBED_BACKEND_UNAVAILABLE"):
80
- return None
86
+ return None, None
81
87
  try:
82
88
  import embed_backend as _eb # type: ignore # noqa: PLC0415
83
89
  vec = _eb.embed(text)
84
- return list(vec)
90
+ return list(vec), _eb.active_model()
85
91
  except Exception: # noqa: BLE001
86
- return None
92
+ return None, None
87
93
 
88
94
 
89
95
  def query(
@@ -146,9 +152,9 @@ def _run_query(
146
152
  return []
147
153
 
148
154
  # Hybrid rerank if embeddings are available.
149
- goal_vec = _try_embed(goal_text)
155
+ goal_vec, goal_model = _try_embed(goal_text)
150
156
  if goal_vec is not None and _embeddings_populated(conn):
151
- candidates = _cosine_rerank(conn, candidates, goal_vec)
157
+ candidates = _cosine_rerank(conn, candidates, goal_vec, goal_model)
152
158
 
153
159
  # Trim to limit.
154
160
  results = candidates[:limit]
@@ -248,18 +254,35 @@ def _embeddings_populated(conn) -> bool:
248
254
  return count > 0
249
255
 
250
256
 
251
- def _cosine_rerank(conn, candidates: list[dict], goal_vec: list[float]) -> list[dict]:
257
+ def _cosine_rerank(
258
+ conn,
259
+ candidates: list[dict],
260
+ goal_vec: list[float],
261
+ goal_model: str | None = None,
262
+ ) -> list[dict]:
252
263
  """Rerank BM25 candidates by cosine similarity of stored embeddings.
253
264
 
254
265
  Returns candidates sorted by cosine similarity (descending). Candidates
255
- without an embedding retain their BM25 rank at the end.
266
+ without a *same-model* embedding retain their BM25 rank at the end.
267
+
268
+ Vectors from a different embedding model are excluded rather than scored.
269
+ Cross-model cosine is meaningless, and because models can share a dimension
270
+ the mismatch is silent — excluding degrades to BM25 instead of ranking on
271
+ noise. `goal_model=None` (backend didn't report one) keeps the legacy
272
+ unfiltered behavior.
256
273
  """
257
274
  ids = [r["id"] for r in candidates]
258
275
  placeholders = ",".join("?" * len(ids))
259
- emb_rows = conn.execute(
260
- f"SELECT fact_id, vec FROM embeddings WHERE fact_id IN ({placeholders})", # nosec: only ?-placeholders / constant fragments interpolated; values bound as params
261
- ids,
262
- ).fetchall()
276
+ if goal_model:
277
+ emb_rows = conn.execute(
278
+ f"SELECT fact_id, vec FROM embeddings WHERE fact_id IN ({placeholders}) AND model = ?", # nosec: only ?-placeholders / constant fragments interpolated; values bound as params
279
+ [*ids, goal_model],
280
+ ).fetchall()
281
+ else:
282
+ emb_rows = conn.execute(
283
+ f"SELECT fact_id, vec FROM embeddings WHERE fact_id IN ({placeholders})", # nosec: only ?-placeholders / constant fragments interpolated; values bound as params
284
+ ids,
285
+ ).fetchall()
263
286
  emb_map = {row["fact_id"]: _unpack_floats(row["vec"]) for row in emb_rows}
264
287
 
265
288
  scored = []
@@ -29,6 +29,7 @@ from _paths import ( # type: ignore # noqa: E402
29
29
  memory_store_root,
30
30
  project_decisions_dir,
31
31
  project_lessons_dir,
32
+ project_research_dir,
32
33
  project_root,
33
34
  top_level_lessons_dir,
34
35
  )
@@ -41,6 +42,7 @@ KIND_STATUS = "build-loop-memory-status"
41
42
  VALID_MODES = {"fast", "expand"}
42
43
  DEFAULT_LIMIT = 5
43
44
  DEFAULT_MAX_CHARS = 900
45
+ CAPSULE_MAX_AGE_DAYS = 7
44
46
 
45
47
 
46
48
  def utc_now() -> str:
@@ -147,11 +149,29 @@ def _recent_markdown(
147
149
  prefix: str,
148
150
  limit: int,
149
151
  max_chars: int,
152
+ query: str = "",
150
153
  ) -> list[dict[str, Any]]:
151
154
  if not directory.is_dir():
152
155
  return []
153
- candidates = [p for p in directory.glob("*.md") if p.name not in {"INDEX.md", "README.md", "MEMORY.md"}]
154
- candidates.sort(key=lambda p: p.stat().st_mtime if p.exists() else 0, reverse=True)
156
+ excluded_dirs = {"raw-originals", "archive", "indexes", "raw"}
157
+ candidates = [
158
+ p for p in directory.rglob("*.md")
159
+ if p.name not in {"INDEX.md", "README.md", "MEMORY.md"}
160
+ and not any(part in excluded_dirs for part in p.relative_to(directory).parts[:-1])
161
+ ]
162
+ query_tokens = {token.lower() for token in re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", query)}
163
+
164
+ def rank(path: Path) -> tuple[int, float]:
165
+ if not query_tokens:
166
+ return (0, path.stat().st_mtime if path.exists() else 0)
167
+ try:
168
+ text = path.read_text(encoding="utf-8", errors="ignore").lower()
169
+ except OSError:
170
+ text = ""
171
+ score = sum(1 for token in query_tokens if token in text)
172
+ return (score, path.stat().st_mtime if path.exists() else 0)
173
+
174
+ candidates.sort(key=rank, reverse=True)
155
175
  rows: list[dict[str, Any]] = []
156
176
  for path in candidates[:limit]:
157
177
  text, reason = _read_text(path, max_chars=max_chars * 2)
@@ -233,6 +253,33 @@ def _freshness(workdir: Path, project: str, project_dir: Path, warnings: list[st
233
253
  validity = "stale"
234
254
  if not project_dir.exists():
235
255
  validity = "unknown"
256
+ capsule_reasons: list[str] = []
257
+ capsule_path = project_dir / "context" / "freshness.json"
258
+ capsule: dict[str, Any] = {}
259
+ try:
260
+ if capsule_path.is_file():
261
+ capsule = json.loads(capsule_path.read_text(encoding="utf-8"))
262
+ stored_source = str(capsule.get("source_commit") or "")
263
+ stored_memory = str(capsule.get("memory_commit") or "")
264
+ if stored_source and source_git.get("commit") and stored_source != source_git.get("commit"):
265
+ capsule_reasons.append("source_commit_changed")
266
+ if stored_memory and memory_git.get("commit") and stored_memory != memory_git.get("commit"):
267
+ capsule_reasons.append("memory_commit_changed")
268
+ generated_at = str(capsule.get("generated_at") or "")
269
+ if generated_at:
270
+ try:
271
+ age_seconds = (datetime.now(timezone.utc) - datetime.fromisoformat(generated_at.replace("Z", "+00:00"))).total_seconds()
272
+ if age_seconds > CAPSULE_MAX_AGE_DAYS * 86400:
273
+ capsule_reasons.append("capsule_older_than_7_days")
274
+ except ValueError:
275
+ capsule_reasons.append("capsule_timestamp_invalid")
276
+ elif project_dir.exists():
277
+ capsule_reasons.append("capsule_missing")
278
+ except (OSError, json.JSONDecodeError):
279
+ capsule_reasons.append("capsule_unreadable")
280
+ if capsule_reasons:
281
+ validity = "stale" if validity == "clean" else validity
282
+ warnings.extend(f"memory_capsule_{reason}" for reason in capsule_reasons)
236
283
  return {
237
284
  "validity": validity,
238
285
  "generated_at": utc_now(),
@@ -246,6 +293,10 @@ def _freshness(workdir: Path, project: str, project_dir: Path, warnings: list[st
246
293
  "memory_branch": memory_git.get("branch", ""),
247
294
  "memory_commit": memory_git.get("commit", ""),
248
295
  "memory_dirty_count": memory_git.get("dirty_count"),
296
+ "capsule_generated_at": capsule.get("generated_at"),
297
+ "capsule_source_commit": capsule.get("source_commit"),
298
+ "capsule_memory_commit": capsule.get("memory_commit"),
299
+ "capsule_stale_reasons": capsule_reasons,
249
300
  "index": _index_state(),
250
301
  }
251
302
 
@@ -255,10 +306,16 @@ def _evidence_from_current(current: dict[str, Any]) -> list[dict[str, str]]:
255
306
  ctx = current.get("context") or {}
256
307
  if ctx.get("exists") and ctx.get("path"):
257
308
  evidence.append({"id": ctx.get("id", "context:CONTEXT"), "path": ctx["path"], "type": "context"})
258
- for lane in ("decisions", "lessons"):
309
+ lane_types = {
310
+ "decisions": "decision",
311
+ "lessons": "lesson",
312
+ "research": "research",
313
+ "references": "reference",
314
+ }
315
+ for lane, evidence_type in lane_types.items():
259
316
  for item in current.get(lane, []):
260
317
  if item.get("id") and item.get("path"):
261
- evidence.append({"id": str(item["id"]), "path": str(item["path"]), "type": lane[:-1]})
318
+ evidence.append({"id": str(item["id"]), "path": str(item["path"]), "type": evidence_type})
262
319
  return evidence
263
320
 
264
321
 
@@ -287,21 +344,53 @@ def build_current(
287
344
  prefix="decision",
288
345
  limit=limit,
289
346
  max_chars=max_chars,
347
+ query=query,
290
348
  )
291
349
  project_lessons = _recent_markdown(
292
350
  project_lessons_dir(project_tag),
293
351
  prefix="lesson",
294
352
  limit=limit,
295
353
  max_chars=max_chars,
354
+ query=query,
296
355
  )
297
356
  global_lessons = _recent_markdown(
298
357
  top_level_lessons_dir(),
299
358
  prefix="lesson",
300
359
  limit=max(0, limit - len(project_lessons)),
301
360
  max_chars=max_chars,
361
+ query=query,
302
362
  )
303
363
  lessons = project_lessons + global_lessons
304
364
 
365
+ project_research = _recent_markdown(
366
+ project_research_dir(project_tag),
367
+ prefix="research",
368
+ limit=limit,
369
+ max_chars=max_chars,
370
+ query=query,
371
+ )
372
+ project_references = _recent_markdown(
373
+ proj_dir / "references",
374
+ prefix="reference",
375
+ limit=limit,
376
+ max_chars=max_chars,
377
+ query=query,
378
+ )
379
+ global_research = _recent_markdown(
380
+ memory_store_root() / "research",
381
+ prefix="research",
382
+ limit=max(0, limit - len(project_research)),
383
+ max_chars=max_chars,
384
+ query=query,
385
+ )
386
+ global_references = _recent_markdown(
387
+ memory_store_root() / "references",
388
+ prefix="reference",
389
+ limit=max(0, limit - len(project_references)),
390
+ max_chars=max_chars,
391
+ query=query,
392
+ )
393
+
305
394
  current: dict[str, Any] = {
306
395
  "schema_version": SCHEMA_VERSION,
307
396
  "kind": KIND_CURRENT,
@@ -312,6 +401,8 @@ def build_current(
312
401
  "context": context,
313
402
  "decisions": project_decisions,
314
403
  "lessons": lessons[:limit],
404
+ "research": (project_research + global_research)[:limit],
405
+ "references": (project_references + global_references)[:limit],
315
406
  "next_commands": [
316
407
  'blm context --workdir "$PWD" --mode fast --json',
317
408
  'blm context --workdir "$PWD" --mode expand --json',
@@ -354,11 +445,12 @@ def render_current_markdown(current: dict[str, Any]) -> str:
354
445
  for item in current["decisions"]:
355
446
  lines.append(f"- {item.get('title') or item.get('id')} ({item.get('id')})")
356
447
  lines.append("")
357
- if current.get("lessons"):
358
- lines.extend(["## Relevant Lessons", ""])
359
- for item in current["lessons"]:
360
- lines.append(f"- {item.get('title') or item.get('id')} ({item.get('id')})")
361
- lines.append("")
448
+ for lane, heading in (("lessons", "Relevant Lessons"), ("research", "Relevant Research"), ("references", "Relevant References")):
449
+ if current.get(lane):
450
+ lines.extend([f"## {heading}", ""])
451
+ for item in current[lane]:
452
+ lines.append(f"- {item.get('title') or item.get('id')} ({item.get('id')})")
453
+ lines.append("")
362
454
  if current.get("warnings"):
363
455
  lines.extend(["## Warnings", ""])
364
456
  for warning in current["warnings"]:
@@ -469,11 +561,13 @@ def _expand_with_lessons(query: str, project: str, limit: int) -> dict[str, Any]
469
561
  def _expand_with_graph(current: dict[str, Any], limit: int) -> dict[str, Any]:
470
562
  project = str(current.get("project") or "")
471
563
  seeds = [f"project:{project}"] if project else []
472
- if not seeds:
473
- for item in current.get("evidence", []):
474
- item_id = str(item.get("id") or "")
475
- if item_id and not item_id.startswith("context:"):
476
- seeds.append(item_id)
564
+ # Seed with hot evidence as well as the project node. This makes research
565
+ # and reference lanes reachable instead of relying on a project-only
566
+ # traversal whose bounded result can be dominated by lessons/decisions.
567
+ for item in current.get("evidence", []):
568
+ item_id = str(item.get("id") or "")
569
+ if item_id and item.get("type") in {"research", "reference"}:
570
+ seeds.append(item_id)
477
571
  # Preserve order while deduping.
478
572
  seeds = list(dict.fromkeys(seeds))
479
573
  if not seeds:
@@ -120,8 +120,11 @@ class SQLiteEdgesStore:
120
120
  self.index_dir = root / "indexes"
121
121
  self.nodes_path = self.index_dir / "graph-nodes.jsonl"
122
122
  self.edges_path = self.index_dir / "graph-edges.jsonl"
123
- self.db_path = db_path or self.index_dir / "graph.sqlite"
123
+ # SQLite is a rebuildable runtime cache. Keep it out of the tracked
124
+ # ``indexes/`` surface so expand-mode reads never dirty memory git.
125
+ self.db_path = db_path or root / "db" / "runtime" / "graph.sqlite"
124
126
  self.reasons = list(reasons)
127
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
125
128
  self._conn = sqlite3.connect(str(self.db_path))
126
129
  self._conn.row_factory = sqlite3.Row
127
130
  self._sync_if_needed()
@@ -160,6 +163,7 @@ class SQLiteEdgesStore:
160
163
 
161
164
  def _sync_if_needed(self) -> None:
162
165
  self.index_dir.mkdir(parents=True, exist_ok=True)
166
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
163
167
  self._ensure_schema()
164
168
  signature = _source_signature(self.nodes_path, self.edges_path)
165
169
  current = self._conn.execute(
@@ -28,6 +28,7 @@ import sys
28
28
  if str(HERE) not in sys.path:
29
29
  sys.path.insert(0, str(HERE))
30
30
  from _paths import derive_slug_from_cwd, memory_store_root # type: ignore # noqa: E402
31
+ import project_registry # type: ignore # noqa: E402
31
32
 
32
33
  DEFAULT_PROJECT_TAG = "_unscoped"
33
34
 
@@ -126,44 +127,49 @@ def _normalize(p: str | Path) -> str:
126
127
 
127
128
 
128
129
  def resolve_project(cwd: Path | str) -> str:
129
- """Return the project tag for ``cwd``.
130
-
131
- Resolution order (post memory-consolidation PR 1):
132
- 1. ``derive_slug_from_cwd(cwd)`` filesystem-driven slug from git
133
- toplevel + ``_safe_project_tag`` normalization. The PRIMARY path.
134
- Returns ``_unscoped`` when ``cwd`` is outside any git repo.
135
- 2. ``projects.yaml`` lookup (exact then longest-prefix) — used ONLY
136
- when (a) the filesystem rule returned ``_unscoped`` AND (b) the
137
- operator has registered an explicit alias for this cwd. This
138
- covers the rare case where the working directory isn't a git
139
- repo but should still map to a known project tag.
140
- 3. ``projects.yaml`` ``default:`` key, else ``_unscoped``.
141
-
130
+ """Return the canonical project id for ``cwd``.
131
+
132
+ Resolution contract (memory-identity-graph Phase 1 — stable-ID + alias
133
+ walking; supersedes the single-value ``memoryProjectSlug`` pin, which
134
+ is now one alias case):
135
+
136
+ 1. ``candidate = derive_slug_from_cwd(cwd)``unchanged deriver:
137
+ 1a. A durable ``memoryProjectSlug`` PIN in the enclosing repo's
138
+ ``.build-loop/config.json`` (``_paths._read_pinned_slug``),
139
+ used verbatim when present; the pin still SETS the candidate
140
+ and the registry can still map it further.
141
+ 1b. Filesystem derivation: ``basename(git toplevel)`` normalized.
142
+ Returns ``_unscoped`` outside any git repo and unpinned.
143
+ 2. Registry lookup (``project_registry.resolve``): find the project
144
+ where ``candidate`` ∈ {id, canonical_slug, *aliases} OR ``cwd``
145
+ matches one of ``paths``, then a BOUNDED, CYCLE-GUARDED alias walk
146
+ returns that project's terminal canonical id. A hit wins.
147
+ 3. Else, if ``candidate != _unscoped`` → return ``candidate`` verbatim.
148
+ An unregistered / new / unmigrated repo IS its own id and keeps
149
+ working with zero registry entry.
150
+ 4. Else (``_unscoped``, no registry hit) → the registry ``default:``
151
+ (else ``_unscoped``). The registry's ``paths`` seed IS the old
152
+ ``projects.yaml`` path-fallback, consulted inside step 2.
153
+
154
+ Deterministic, no LLM, never raises (degrades to the candidate/_unscoped).
142
155
  Aligns by construction with Postgres ``semantic_facts.project`` —
143
- ``derive_slug_from_cwd`` routes through ``_safe_project_tag``, which
144
- is the same validator used by ``decisions_dir_for_project``.
156
+ ``derive_slug_from_cwd`` routes through ``_safe_project_tag``.
145
157
  """
146
- # Step 1 — filesystem-driven derivation (the new primary path).
147
- slug = derive_slug_from_cwd(cwd)
148
- if slug != DEFAULT_PROJECT_TAG:
149
- return slug
150
-
151
- # Step 2 — fall back to projects.yaml only when filesystem returned
152
- # _unscoped. This preserves backward-compat for explicit aliases.
153
- data = load_projects_yaml()
154
- cwd_norm = _normalize(cwd)
155
- best_match: tuple[int, str] | None = None
156
- for entry in data["projects"]:
157
- path_norm = _normalize(entry["path"])
158
- if path_norm == cwd_norm:
159
- return entry["project"]
160
- if cwd_norm.startswith(path_norm + os.sep):
161
- length = len(path_norm)
162
- if best_match is None or length > best_match[0]:
163
- best_match = (length, entry["project"])
164
- if best_match is not None:
165
- return best_match[1]
166
- return data["default"]
158
+ # Step 1 — pinned-or-derived candidate slug.
159
+ candidate = derive_slug_from_cwd(cwd)
160
+
161
+ # Step 2 — registry lookup + alias walk (returns terminal canonical id).
162
+ registry = project_registry.load_registry()
163
+ hit = project_registry.resolve(candidate, cwd, registry)
164
+ if hit is not None:
165
+ return hit
166
+
167
+ # Step 3 an unregistered git repo is its own id.
168
+ if candidate != DEFAULT_PROJECT_TAG:
169
+ return candidate
170
+
171
+ # Step 4 — _unscoped with no registry hit → default.
172
+ return registry.get("default", DEFAULT_PROJECT_TAG)
167
173
 
168
174
 
169
175
  if __name__ == "__main__": # pragma: no cover - manual smoke tool
@@ -33,7 +33,20 @@ if str(HERE) not in sys.path:
33
33
  import check_cache_sync as cache_sync # type: ignore # noqa: E402
34
34
 
35
35
 
36
- EXCLUDE_DIRS = {".git", "node_modules", "__pycache__", ".venv", ".build-loop", ".rally"}
36
+ EXCLUDE_DIRS = {
37
+ ".git", "node_modules", "__pycache__", ".venv", ".build-loop", ".rally",
38
+ # Per-tool runtime state (the `.<toolname>/` convention). All gitignored, so
39
+ # none of it belongs in a plugin cache. Named, observed failure (2026-08-11):
40
+ # `.ibr/browser-profile/` held a stopped Chrome's SingletonSocket /
41
+ # SingletonLock / SingletonCookie / RunningChromeVersion. shutil.copytree
42
+ # raised on those four dangling entries, so EVERY `build-loop-install
43
+ # --host claude` failed while the installer still printed
44
+ # "claude: OK build-loop@<version>". The Claude cache sat on 0.37.0 for
45
+ # multiple bumps.
46
+ ".ibr", ".navgator", ".spectra", ".bookmark", ".episodic", ".procedural",
47
+ ".in_use", ".pytest_cache", ".claude-code-debugger", ".localsmartz",
48
+ ".ci-rally-apps",
49
+ }
37
50
  HOOK_NAMES = ("post-commit", "post-merge", "post-checkout", "post-rewrite")
38
51
  HOOK_MARKER = "# --- BEGIN build-loop plugin-cache-sync ---"
39
52
  HOOK_MARKER_END = "# --- END build-loop plugin-cache-sync ---"
@@ -101,7 +114,29 @@ def materialize_head(source: Path) -> tempfile.TemporaryDirectory[str]:
101
114
 
102
115
 
103
116
  def ignore_dirty_tree(_dir: str, names: list[str]) -> set[str]:
104
- return {name for name in names if name in EXCLUDE_DIRS}
117
+ """Skip excluded dirs, and anything copytree cannot copy.
118
+
119
+ The exclude list handles the runtime dirs we know about. This second clause
120
+ handles the ones we don't: a socket, a FIFO, or a symlink whose target has
121
+ gone away raises inside shutil.copytree and aborts the whole sync. A plugin
122
+ cache should skip an unreadable runtime artifact, not fail the install.
123
+ """
124
+ skip = {name for name in names if name in EXCLUDE_DIRS}
125
+ base = Path(_dir)
126
+ for name in names:
127
+ if name in skip:
128
+ continue
129
+ p = base / name
130
+ try:
131
+ if p.is_symlink() and not p.exists():
132
+ skip.add(name) # dangling symlink
133
+ continue
134
+ st = p.lstat()
135
+ if stat.S_ISSOCK(st.st_mode) or stat.S_ISFIFO(st.st_mode):
136
+ skip.add(name) # socket / named pipe
137
+ except OSError:
138
+ skip.add(name) # vanished mid-walk
139
+ return skip
105
140
 
106
141
 
107
142
  def copy_tree_to_temp(source: Path, target_parent: Path) -> Path:
@@ -50,12 +50,22 @@ surfaced on Rally Point records as top-level `build_loop_id` and
50
50
  `scripts/agent_rally.py where`; do not hardcode legacy channel roots.
51
51
  - Write cross-session events through `scripts/rally_point/post.py::post()`;
52
52
  do not append directly to `changes.jsonl`.
53
+ - Correct a wrong fact by appending a retraction (`agent_rally.py retract`),
54
+ never by editing the log. See "Retracting a wrong fact" below.
53
55
  - Keep `build_loop_id` separate from `producer_metadata`.
54
56
  - Keep substrate files independent from build-loop memory/orchestration
55
57
  internals. If a build-loop-specific integration is needed, put it in a thin
56
58
  adapter named in `plugin_boundary.json`.
57
59
  - Use stable tool ids: `claude_code`, `codex`, `cursor`, `gemini`, or another
58
60
  lowercase id agreed by the adapter.
61
+ - When the work produces a handoff or retrospective **document**, post its
62
+ **absolute** path (`rally say handoff ... --evidence "file:/abs/path"`) and
63
+ state that same path in the terminal reply. A receiver runs in a different
64
+ working directory, so a relative path resolves to nothing or to the wrong
65
+ file, and a retrospective written to the memory store is unreachable from the
66
+ repo unless its path is said out loud. Post first, then go on writing; the
67
+ post needs no reading and nothing should delay it. The common failure is not
68
+ a thin handoff, it is a thorough one nobody located.
59
69
 
60
70
  ## Commands
61
71
 
@@ -74,6 +84,42 @@ session-ack aware; read `inbox/<tool>.jsonl` or `inbox/all.jsonl` before acting
74
84
  on a full message, then run `ack-inbox` after handling it. Ack cursors live
75
85
  under `inbox/.acks/` and never rewrite the append-only inbox payloads.
76
86
 
87
+ ## Retracting a wrong fact
88
+
89
+ The ledger is append-only, so a posted fact is never edited or deleted. Withdraw
90
+ one by appending a **retraction** that names its `event_id` — the `[fact_...]`
91
+ id shown in `rally room`:
92
+
93
+ ```bash
94
+ python3 ${CLAUDE_PLUGIN_ROOT}/scripts/agent_rally.py retract \
95
+ --workdir "$PWD" --session-id "$SESSION_ID" --tool "$TOOL_ID" \
96
+ --fact fact_f4c9_18c99988bcb40648 \
97
+ --reason "wrong sha — read from a stale worktree" \
98
+ --superseded-by fact_a1b2_0f3e4d5c6b7a8901 # optional: the corrected fact
99
+ ```
100
+
101
+ From then on every build-loop read path drops the withdrawn fact and keeps the
102
+ retraction, so peers see the correction instead of the bad claim. `--superseded-by`
103
+ is optional: use it when a corrected fact replaces the retracted one, omit it when
104
+ the fact was simply wrong.
105
+
106
+ Guardrails: an unknown `--fact` id exits 1 rather than posting a retraction that
107
+ neutralizes nothing (`--force` overrides, for a fact in a store build-loop cannot
108
+ read back); a second retraction of the same fact exits 1 as `retract-noop`; and
109
+ retracting a retraction is refused, because that would erase the correction trail
110
+ — post a new corrective fact instead.
111
+
112
+ Limits worth knowing before you rely on it:
113
+
114
+ - **Resolution is build-loop-side.** `rally room` is the Rust binary and does not
115
+ yet honor retractions, so a retracted fact still appears there (as does the
116
+ retraction next to it). Native `rally retract` is filed against the
117
+ `agent-rally-point` repo.
118
+ - **A fact a peer already read cannot be un-read.** A retraction arriving in a
119
+ later poll surfaces as its own record; it cannot reach back into context a peer
120
+ already consumed.
121
+ - **Pre-`fact.v1` records carry no `event_id`** and therefore cannot be targeted.
122
+
77
123
  ## Task Heartbeat — still on the claimed task
78
124
 
79
125
  Presence is process liveness: it tells peers this session can still write to
@@ -48,7 +48,7 @@ When Assess detects a newly added third-party package:
48
48
  2. If `cooldown.install_blocked: true` (latest version released < 7 days ago, service not `author_owned`):
49
49
  - Surface the `cooldown.reason` to the user.
50
50
  - The `pre_bash_dependency_cooldown.sh` PreToolUse hook is the enforcement point — it already rewrites/denies fresh installs at the Bash boundary. The registry verdict is the *advisory* signal that explains *why* a hook rewrite happened; it does not replace the hook.
51
- - `author_owned: true` services (`@tyroneross/*` scope + the user's own projects) are exempt — `cooldown.install_blocked` is always `false` for them, matching the hook's allowlist.
51
+ - `author_owned: true` services (the scope(s)/repos configured in `dependencyCooldown.allowlist`, plus the user's own projects) are exempt — `cooldown.install_blocked` is always `false` for them, matching the hook's allowlist. The default allowlist ships pinned to the maintainer's own npm scope; set `dependencyCooldown.allowlist` to your own scope(s) so this exemption applies to packages you own rather than the maintainer's.
52
52
  3. The 7-day registry cooldown window and the hook's 7-day install cooldown are deliberately the same number (supply-chain dwell time). If they ever diverge, the hook is authoritative for *enforcement*; the registry is authoritative for *explanation*.
53
53
 
54
54
  ## What this does NOT do
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: build-loop:architecture-dead
3
- description: Use when the user asks to "find dead code", "scan for orphaned components", or "clean up unused files", or during Phase 4 Review before a release. Scans for unreachable components and dead code; build-loop's native scan from NavGator's CLI.
3
+ description: Find code/components with zero remaining callers — dead, unreachable, or orphaned files to delete. Use when the user asks to "find dead code", "clean up unused files", or "scan for orphaned components". Not for code that's still called but breaks a layer boundary or forms a cycle (use `architecture-rules`).
4
4
  version: 0.1.0
5
5
  user-invocable: false
6
6
  source: NavGator/commands/dead.md
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: build-loop:architecture-impact
3
- description: Use when Phase 1 Assess evaluates top-risk components, Phase 5 Iterate precedes a cross-layer fix, or the user asks "what does changing X break". Blast-radius analysis tracing upstream/downstream dependencies for a component or file before refactor.
3
+ description: Blast-radius analysis trace upstream/downstream dependencies for one component or file before changing it. Use when the user asks "what does changing X break" or before a cross-layer fix.
4
4
  version: 0.1.0
5
5
  user-invocable: false
6
6
  source: NavGator/skills/impact-analysis/SKILL.md