@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.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/build-loop-surface.mdc +12 -11
- package/.cursor/rules/skill-index.mdc +33 -0
- package/AGENTS.md +213 -34
- package/README.md +99 -31
- package/agents/advisor.md +4 -4
- package/agents/alignment-checker.md +2 -2
- package/agents/architecture-scout.md +4 -4
- package/agents/build-orchestrator.md +38 -36
- package/agents/database-assessor.md +11 -5
- package/agents/design-contract-specialist.md +8 -8
- package/agents/fact-checker.md +13 -3
- package/agents/fix-critique.md +2 -2
- package/agents/independent-auditor.md +60 -7
- package/agents/leak-scanner.md +82 -0
- package/agents/overfitting-reviewer.md +2 -2
- package/agents/plan-critic.md +1 -1
- package/agents/promotion-reviewer.md +5 -5
- package/agents/retrospective-synthesizer.md +138 -35
- package/agents/scope-auditor.md +82 -11
- package/agents/security-reviewer.md +56 -2
- package/agents/self-improvement-architect.md +17 -3
- package/agents/transcript-pattern-miner.md +5 -5
- package/agents/ui-validator.md +1 -1
- package/bin/build-loop-debugger.js +143 -0
- package/bin/build-loop-install.js +1 -4
- package/bin/build-loop-load-probe.js +345 -0
- package/codex-skills/build-loop/SKILL.md +28 -6
- package/commands/feedback.md +37 -0
- package/dist/src/interactive-verifier.d.ts +1 -14
- package/dist/src/interactive-verifier.d.ts.map +1 -1
- package/dist/src/interactive-verifier.js +6 -113
- package/dist/src/interactive-verifier.js.map +1 -1
- package/dist/src/quality.d.ts +5 -0
- package/dist/src/quality.d.ts.map +1 -0
- package/dist/src/quality.js +81 -0
- package/dist/src/quality.js.map +1 -0
- package/dist/src/storage.d.ts.map +1 -1
- package/dist/src/storage.js +37 -3
- package/dist/src/storage.js.map +1 -1
- package/docs/agent-surface-policy.md +35 -31
- package/docs/memory-setup.md +19 -0
- package/hooks/git/pre-push +65 -4
- package/hooks/hooks.json +95 -38
- package/hooks/pre-commit +20 -1
- package/hooks/pre-edit-rally-point.sh +10 -3
- package/hooks/session-start-codex-hook-trust.sh +30 -0
- package/hooks/session-start-git-hooks.sh +3 -1
- package/hooks/session-start-rally-point.sh +52 -4
- package/hooks/session-start-worktree-gc.sh +47 -94
- package/hooks/stop-transcript-sweep.sh +173 -0
- package/hooks/test_closeout.sh +14 -2
- package/package.json +8 -7
- package/scripts/README.md +1 -1
- package/scripts/_paths.py +65 -0
- package/scripts/groundwork_exchange.py +1012 -0
- package/scripts/install_memory.py +33 -1
- package/scripts/lessons_index/ingest.py +13 -2
- package/scripts/lessons_index/query.py +36 -13
- package/scripts/memory_context/__init__.py +108 -14
- package/scripts/memory_graph/__init__.py +5 -1
- package/scripts/project_resolver.py +42 -36
- package/scripts/sync_plugin_cache.py +37 -2
- package/skills/agent-rally-point/SKILL.md +46 -0
- package/skills/api-registry-bridge/SKILL.md +1 -1
- package/skills/architecture/dead/SKILL.md +1 -1
- package/skills/architecture/impact/SKILL.md +1 -1
- package/skills/architecture/review/SKILL.md +1 -1
- package/skills/architecture/rules/SKILL.md +3 -3
- package/skills/architecture/scan/SKILL.md +1 -1
- package/skills/architecture/trace/SKILL.md +1 -1
- package/skills/attribution-standard/SKILL.md +6 -6
- package/skills/auto-decision-capture/SKILL.md +31 -2
- package/skills/auto-finding-capture/SKILL.md +28 -1
- package/skills/build-loop/SKILL.md +131 -23
- package/skills/build-loop/fallbacks.md +16 -21
- package/skills/build-loop/phases/ui-validation.md +2 -2
- package/skills/build-loop/references/advisor-dispatch-ladder.md +1 -1
- package/skills/build-loop/references/apple-native-planning.md +1 -1
- package/skills/build-loop/references/autonomous-and-per-commit-modes.md +11 -5
- package/skills/build-loop/references/autonomy-dashboard.md +115 -0
- package/skills/build-loop/references/capability-routing.md +24 -2
- package/skills/build-loop/references/coordination.md +24 -6
- package/skills/build-loop/references/experiment-results-template.md +15 -3
- package/skills/build-loop/references/leadership.md +1 -1
- package/skills/build-loop/references/memory.md +14 -3
- package/skills/build-loop/references/modular-systems-pack.md +8 -0
- package/skills/build-loop/references/output-style.md +86 -0
- package/skills/build-loop/references/phase-1-assess.md +102 -2
- package/skills/build-loop/references/phase-2-plan.md +9 -1
- package/skills/build-loop/references/phase-3-execute.md +5 -2
- package/skills/build-loop/references/phase-4-review.md +85 -8
- package/skills/build-loop/references/phase-5-iterate.md +76 -8
- package/skills/build-loop/references/phase-6-learn.md +10 -17
- package/skills/build-loop/references/privileged-request-broker.md +254 -0
- package/skills/build-loop/references/resource-aware-execution.md +183 -0
- package/skills/build-loop/references/self-recursive-dev.md +2 -2
- package/skills/build-loop/references/status-output-format.md +207 -0
- package/skills/build-loop/references/verify-dispatch.md +56 -2
- package/skills/building-with-deepagents/SKILL.md +1 -1
- package/skills/claim-scope/SKILL.md +185 -0
- package/skills/color-engine/SKILL.md +103 -0
- package/skills/color-engine/_core.py +464 -0
- package/skills/color-engine/color_engine.py +175 -0
- package/skills/cost-rca/SKILL.md +61 -0
- package/skills/data-plane-worktrees/SKILL.md +139 -0
- package/skills/data-plane-worktrees/agents/openai.yaml +4 -0
- package/skills/database-practice/SKILL.md +200 -0
- package/skills/database-practice/references/diagnostic-queries.sql +126 -0
- package/skills/database-practice/references/vector-and-graph-tuning.md +208 -0
- package/skills/database-practice/scripts/db_table_map.py +1244 -0
- package/skills/database-practice/scripts/test_db_table_map.py +514 -0
- package/skills/debug-loop/SKILL.md +36 -6
- package/skills/debugging-memory/SKILL.md +32 -430
- package/skills/debugging-memory/references/pattern-extraction.md +4 -4
- package/skills/debugging-memory/references/search.md +32 -120
- package/skills/debugging-memory/references/store.md +32 -126
- package/skills/debugging-memory/references/subagent-integration.md +1 -1
- package/skills/decision-queue/SKILL.md +251 -0
- package/skills/decision-queue/assets/template.html +1242 -0
- package/skills/decision-queue/references/example-large-queue-batching.md +164 -0
- package/skills/decision-queue/scripts/regen_template_constants.py +160 -0
- package/skills/defenseclaw-bridge/SKILL.md +2 -2
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +2 -9
- package/skills/drain-proposals/SKILL.md +53 -0
- package/skills/focused-loop-builder/SKILL.md +31 -0
- package/skills/focused-loop-builder/references/spec-format.md +27 -0
- package/skills/handoff/SKILL.md +169 -8
- package/skills/ibr-bridge/SKILL.md +4 -1
- package/skills/knowledge/SKILL.md +26 -14
- package/skills/knowledge/references/review-mode.md +2 -3
- package/skills/knowledge/templates/madr-minimal.md +1 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/model-bakeoff/SKILL.md +48 -10
- package/skills/model-tiering/SKILL.md +92 -31
- package/skills/native-ax-driver/SKILL.md +38 -5
- package/skills/native-ax-driver/scripts/native_driver.py +278 -22
- package/skills/native-ax-driver/scripts/test_native_driver.py +227 -0
- package/skills/optimize/SKILL.md +1 -1
- package/skills/plugin-builder/SKILL.md +48 -1
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +3 -4
- package/skills/plugin-builder/references/distribution.md +13 -2
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +2 -2
- package/skills/plugin-tests/SKILL.md +2 -2
- package/skills/recursive-retrospective/SKILL.md +1 -1
- package/skills/repo-closeout/SKILL.md +17 -0
- package/skills/repo-closeout/agents/openai.yaml +4 -0
- package/skills/repo-maintenance/SKILL.md +179 -0
- package/skills/repo-maintenance/agents/openai.yaml +4 -0
- package/skills/repo-maintenance/references/pre-public-hygiene.md +134 -0
- package/skills/repo-maintenance/references/repository-taxonomy.md +161 -0
- package/skills/repo-maintenance/references/safety-protocol.md +106 -0
- package/skills/repo-maintenance/references/stack-profiles.md +138 -0
- package/skills/repo-maintenance/scripts/audit_repo_maintenance.py +1198 -0
- package/skills/repo-maintenance/scripts/test_audit_repo_maintenance.py +506 -0
- package/skills/repository-intelligence/SKILL.md +189 -0
- package/skills/repository-intelligence/agents/openai.yaml +4 -0
- package/skills/repository-intelligence/references/assessment-rubric.md +88 -0
- package/skills/repository-intelligence/scripts/repository_inventory.py +347 -0
- package/skills/research/SKILL.md +12 -2
- package/skills/root-cause-analysis/SKILL.md +1 -1
- package/skills/runtime-parity-verification/SKILL.md +36 -1
- package/skills/security-methodology/SKILL.md +23 -10
- package/skills/security-methodology/references/agentic-handoff-templates.md +220 -0
- package/skills/security-methodology/references/cross-source-matrix.md +1 -1
- package/skills/security-methodology/references/owasp-agentic-top-10.md +1 -1
- package/skills/security-scan/SKILL.md +55 -15
- package/skills/self-improve/SKILL.md +70 -50
- package/skills/silent-assumptions/SKILL.md +341 -0
- package/skills/silent-assumptions/references/elicitation-detectors.md +342 -0
- package/skills/spec-writing/SKILL.md +128 -24
- package/skills/spec-writing/scripts/check_checklist.py +114 -15
- package/skills/ui-design/SKILL.md +6 -4
- package/skills/ui-design/references/color-engine.md +132 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +8 -8
- package/skills/ui-design/references/ui-guidance-sources.md +1 -1
- package/skills/ui-design/references/universal-design-principles.alt.md +2 -2
- package/plugin-artifacts/codex/.codex-plugin/plugin.json +0 -41
- package/plugin-artifacts/codex/AGENTS.md +0 -560
- package/plugin-artifacts/codex/BUILD-ARTIFACT.md +0 -5
- package/plugin-artifacts/codex/LICENSE +0 -202
- package/plugin-artifacts/codex/README.md +0 -313
- package/plugin-artifacts/codex/assets/build-loop-plugin-icon.png +0 -0
- package/plugin-artifacts/codex/docs/agent-surface-policy.md +0 -63
- package/plugin-artifacts/codex/references/advisor-dispatch-ladder.md +0 -62
- package/plugin-artifacts/codex/references/agent-role-taxonomy.md +0 -135
- package/plugin-artifacts/codex/references/autonomous-and-per-commit-modes.md +0 -161
- package/plugin-artifacts/codex/references/autonomy-config.md +0 -231
- package/plugin-artifacts/codex/references/backlog-system.md +0 -285
- package/plugin-artifacts/codex/references/capability-routing.md +0 -231
- package/plugin-artifacts/codex/references/codex-subagents.md +0 -106
- package/plugin-artifacts/codex/references/coordination-file-template.md +0 -181
- package/plugin-artifacts/codex/references/coordination-rules.md +0 -552
- package/plugin-artifacts/codex/references/dogfood-reload-checkpoint.md +0 -112
- package/plugin-artifacts/codex/references/halt-and-ask-protocol.md +0 -102
- package/plugin-artifacts/codex/references/implementer-envelope-schema.md +0 -302
- package/plugin-artifacts/codex/references/intent-capability-pack.md +0 -257
- package/plugin-artifacts/codex/references/intent-exploration-prompts.md +0 -96
- package/plugin-artifacts/codex/references/leadership.md +0 -72
- package/plugin-artifacts/codex/references/memory-systems.md +0 -261
- package/plugin-artifacts/codex/references/memory.md +0 -313
- package/plugin-artifacts/codex/references/model-tier-mapping.md +0 -296
- package/plugin-artifacts/codex/references/modular-systems-pack.md +0 -96
- package/plugin-artifacts/codex/references/phase-1-assess.md +0 -249
- package/plugin-artifacts/codex/references/phase-2-plan.md +0 -86
- package/plugin-artifacts/codex/references/phase-3-execute.md +0 -49
- package/plugin-artifacts/codex/references/phase-4-review.md +0 -341
- package/plugin-artifacts/codex/references/phase-5-iterate.md +0 -72
- package/plugin-artifacts/codex/references/phase-6-learn.md +0 -58
- package/plugin-artifacts/codex/references/recent-design-structures.md +0 -274
- package/plugin-artifacts/codex/references/research-trigger-policy.md +0 -140
- package/plugin-artifacts/codex/references/runtime-smoke-triggers.md +0 -42
- package/plugin-artifacts/codex/references/self-review.md +0 -234
- package/plugin-artifacts/codex/references/single-writer-commit-protocol.md +0 -90
- package/plugin-artifacts/codex/references/task-capture-policy.md +0 -68
- package/plugin-artifacts/codex/references/ui-io-contract.md +0 -116
- package/plugin-artifacts/codex/references/ui-spotcheck-protocol.md +0 -65
- package/plugin-artifacts/codex/references/verify-dispatch.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/SKILL.md +0 -381
- package/plugin-artifacts/codex/skills/build-loop/detect-plugins.mjs +0 -82
- package/plugin-artifacts/codex/skills/build-loop/eval-guide.md +0 -65
- package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +0 -549
- package/plugin-artifacts/codex/skills/build-loop/phases/fact-check.md +0 -42
- package/plugin-artifacts/codex/skills/build-loop/phases/ui-validation.md +0 -267
- package/plugin-artifacts/codex/skills/build-loop/references/advisor-dispatch-ladder.md +0 -62
- package/plugin-artifacts/codex/skills/build-loop/references/apple-native-planning.md +0 -439
- package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md +0 -161
- package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +0 -231
- package/plugin-artifacts/codex/skills/build-loop/references/codex-subagents.md +0 -106
- package/plugin-artifacts/codex/skills/build-loop/references/coordination.md +0 -161
- package/plugin-artifacts/codex/skills/build-loop/references/correction-aware-capture.md +0 -177
- package/plugin-artifacts/codex/skills/build-loop/references/experiment-results-template.md +0 -101
- package/plugin-artifacts/codex/skills/build-loop/references/independent-auditor.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/intent-capability-pack.md +0 -257
- package/plugin-artifacts/codex/skills/build-loop/references/intent-exploration-prompts.md +0 -96
- package/plugin-artifacts/codex/skills/build-loop/references/leadership.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/memory.md +0 -313
- package/plugin-artifacts/codex/skills/build-loop/references/modular-systems-pack.md +0 -96
- package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +0 -222
- package/plugin-artifacts/codex/skills/build-loop/references/pay-it-forward-arch.md +0 -98
- package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +0 -249
- package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +0 -86
- package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +0 -49
- package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +0 -341
- package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +0 -58
- package/plugin-artifacts/codex/skills/build-loop/references/recent-design-structures.md +0 -274
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/ASSESSMENT.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +0 -149
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +0 -32
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +0 -48
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +0 -60
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +0 -51
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +0 -71
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +0 -52
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/trace-comparison.md +0 -202
- package/plugin-artifacts/codex/skills/build-loop/references/self-recursive-dev.md +0 -77
- package/plugin-artifacts/codex/skills/build-loop/references/self-review.md +0 -234
- package/plugin-artifacts/codex/skills/build-loop/references/ui-io-contract.md +0 -116
- package/plugin-artifacts/codex/skills/build-loop/references/verify-dispatch.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/scanners/audit-design-rules.mjs +0 -476
- package/plugin-artifacts/codex/skills/build-loop/scanners/require-visual-evidence.mjs +0 -239
- package/plugin-artifacts/codex/skills/build-loop/templates/backlog-item.md +0 -35
- package/plugin-artifacts/codex/skills/build-loop/templates/codex-worker-prompt.md +0 -100
- package/plugin-artifacts/codex/skills/build-loop/templates/ui-subagent-prompt.md +0 -179
- package/plugin-artifacts/codex/skills/build-loop/templates/ux-fix-plan.md +0 -40
- 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.
|
|
197
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
154
|
-
candidates
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
|
130
|
-
|
|
131
|
-
Resolution
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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 —
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 = {
|
|
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
|
-
|
|
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 (
|
|
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", "
|
|
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:
|
|
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
|