@tyroneross/build-loop 0.36.1 → 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 +10 -4
  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 +55 -1
  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 +22 -9
  167. package/skills/security-methodology/references/agentic-handoff-templates.md +220 -0
  168. package/skills/security-methodology/references/cross-source-matrix.md +1 -1
  169. package/skills/security-methodology/references/owasp-agentic-top-10.md +1 -1
  170. package/skills/security-scan/SKILL.md +55 -15
  171. package/skills/self-improve/SKILL.md +70 -50
  172. package/skills/silent-assumptions/SKILL.md +341 -0
  173. package/skills/silent-assumptions/references/elicitation-detectors.md +342 -0
  174. package/skills/spec-writing/SKILL.md +128 -24
  175. package/skills/spec-writing/scripts/check_checklist.py +114 -15
  176. package/skills/ui-design/SKILL.md +6 -4
  177. package/skills/ui-design/references/color-engine.md +132 -0
  178. package/skills/ui-design/references/design-preferences-from-owned-apps.md +8 -8
  179. package/skills/ui-design/references/ui-guidance-sources.md +1 -1
  180. package/skills/ui-design/references/universal-design-principles.alt.md +2 -2
  181. package/plugin-artifacts/codex/.codex-plugin/plugin.json +0 -41
  182. package/plugin-artifacts/codex/AGENTS.md +0 -560
  183. package/plugin-artifacts/codex/BUILD-ARTIFACT.md +0 -5
  184. package/plugin-artifacts/codex/LICENSE +0 -202
  185. package/plugin-artifacts/codex/README.md +0 -313
  186. package/plugin-artifacts/codex/assets/build-loop-plugin-icon.png +0 -0
  187. package/plugin-artifacts/codex/docs/agent-surface-policy.md +0 -63
  188. package/plugin-artifacts/codex/references/advisor-dispatch-ladder.md +0 -62
  189. package/plugin-artifacts/codex/references/agent-role-taxonomy.md +0 -135
  190. package/plugin-artifacts/codex/references/autonomous-and-per-commit-modes.md +0 -161
  191. package/plugin-artifacts/codex/references/autonomy-config.md +0 -231
  192. package/plugin-artifacts/codex/references/backlog-system.md +0 -285
  193. package/plugin-artifacts/codex/references/capability-routing.md +0 -231
  194. package/plugin-artifacts/codex/references/codex-subagents.md +0 -106
  195. package/plugin-artifacts/codex/references/coordination-file-template.md +0 -181
  196. package/plugin-artifacts/codex/references/coordination-rules.md +0 -552
  197. package/plugin-artifacts/codex/references/dogfood-reload-checkpoint.md +0 -112
  198. package/plugin-artifacts/codex/references/halt-and-ask-protocol.md +0 -102
  199. package/plugin-artifacts/codex/references/implementer-envelope-schema.md +0 -302
  200. package/plugin-artifacts/codex/references/intent-capability-pack.md +0 -257
  201. package/plugin-artifacts/codex/references/intent-exploration-prompts.md +0 -96
  202. package/plugin-artifacts/codex/references/leadership.md +0 -72
  203. package/plugin-artifacts/codex/references/memory-systems.md +0 -261
  204. package/plugin-artifacts/codex/references/memory.md +0 -313
  205. package/plugin-artifacts/codex/references/model-tier-mapping.md +0 -296
  206. package/plugin-artifacts/codex/references/modular-systems-pack.md +0 -96
  207. package/plugin-artifacts/codex/references/phase-1-assess.md +0 -249
  208. package/plugin-artifacts/codex/references/phase-2-plan.md +0 -86
  209. package/plugin-artifacts/codex/references/phase-3-execute.md +0 -49
  210. package/plugin-artifacts/codex/references/phase-4-review.md +0 -341
  211. package/plugin-artifacts/codex/references/phase-5-iterate.md +0 -72
  212. package/plugin-artifacts/codex/references/phase-6-learn.md +0 -58
  213. package/plugin-artifacts/codex/references/recent-design-structures.md +0 -274
  214. package/plugin-artifacts/codex/references/research-trigger-policy.md +0 -140
  215. package/plugin-artifacts/codex/references/runtime-smoke-triggers.md +0 -42
  216. package/plugin-artifacts/codex/references/self-review.md +0 -234
  217. package/plugin-artifacts/codex/references/single-writer-commit-protocol.md +0 -90
  218. package/plugin-artifacts/codex/references/task-capture-policy.md +0 -68
  219. package/plugin-artifacts/codex/references/ui-io-contract.md +0 -116
  220. package/plugin-artifacts/codex/references/ui-spotcheck-protocol.md +0 -65
  221. package/plugin-artifacts/codex/references/verify-dispatch.md +0 -85
  222. package/plugin-artifacts/codex/skills/build-loop/SKILL.md +0 -381
  223. package/plugin-artifacts/codex/skills/build-loop/detect-plugins.mjs +0 -82
  224. package/plugin-artifacts/codex/skills/build-loop/eval-guide.md +0 -65
  225. package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +0 -549
  226. package/plugin-artifacts/codex/skills/build-loop/phases/fact-check.md +0 -42
  227. package/plugin-artifacts/codex/skills/build-loop/phases/ui-validation.md +0 -267
  228. package/plugin-artifacts/codex/skills/build-loop/references/advisor-dispatch-ladder.md +0 -62
  229. package/plugin-artifacts/codex/skills/build-loop/references/apple-native-planning.md +0 -439
  230. package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md +0 -161
  231. package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +0 -231
  232. package/plugin-artifacts/codex/skills/build-loop/references/codex-subagents.md +0 -106
  233. package/plugin-artifacts/codex/skills/build-loop/references/coordination.md +0 -161
  234. package/plugin-artifacts/codex/skills/build-loop/references/correction-aware-capture.md +0 -177
  235. package/plugin-artifacts/codex/skills/build-loop/references/experiment-results-template.md +0 -101
  236. package/plugin-artifacts/codex/skills/build-loop/references/independent-auditor.md +0 -72
  237. package/plugin-artifacts/codex/skills/build-loop/references/intent-capability-pack.md +0 -257
  238. package/plugin-artifacts/codex/skills/build-loop/references/intent-exploration-prompts.md +0 -96
  239. package/plugin-artifacts/codex/skills/build-loop/references/leadership.md +0 -72
  240. package/plugin-artifacts/codex/skills/build-loop/references/memory.md +0 -313
  241. package/plugin-artifacts/codex/skills/build-loop/references/modular-systems-pack.md +0 -96
  242. package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +0 -222
  243. package/plugin-artifacts/codex/skills/build-loop/references/pay-it-forward-arch.md +0 -98
  244. package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +0 -249
  245. package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +0 -86
  246. package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +0 -49
  247. package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +0 -341
  248. package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +0 -72
  249. package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +0 -58
  250. package/plugin-artifacts/codex/skills/build-loop/references/recent-design-structures.md +0 -274
  251. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/ASSESSMENT.md +0 -85
  252. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +0 -149
  253. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +0 -32
  254. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +0 -48
  255. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +0 -60
  256. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +0 -51
  257. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +0 -71
  258. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +0 -52
  259. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/trace-comparison.md +0 -202
  260. package/plugin-artifacts/codex/skills/build-loop/references/self-recursive-dev.md +0 -77
  261. package/plugin-artifacts/codex/skills/build-loop/references/self-review.md +0 -234
  262. package/plugin-artifacts/codex/skills/build-loop/references/ui-io-contract.md +0 -116
  263. package/plugin-artifacts/codex/skills/build-loop/references/verify-dispatch.md +0 -85
  264. package/plugin-artifacts/codex/skills/build-loop/scanners/audit-design-rules.mjs +0 -476
  265. package/plugin-artifacts/codex/skills/build-loop/scanners/require-visual-evidence.mjs +0 -239
  266. package/plugin-artifacts/codex/skills/build-loop/templates/backlog-item.md +0 -35
  267. package/plugin-artifacts/codex/skills/build-loop/templates/codex-worker-prompt.md +0 -100
  268. package/plugin-artifacts/codex/skills/build-loop/templates/ui-subagent-prompt.md +0 -179
  269. package/plugin-artifacts/codex/skills/build-loop/templates/ux-fix-plan.md +0 -40
  270. package/scripts/build_codex_plugin_artifact.py +0 -321
@@ -0,0 +1,506 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for the read-only repository maintenance inventory."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import importlib.util
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ import time
12
+ import unittest
13
+ from pathlib import Path
14
+ from unittest import mock
15
+
16
+
17
+ MODULE_PATH = Path(__file__).with_name("audit_repo_maintenance.py")
18
+ SPEC = importlib.util.spec_from_file_location("audit_repo_maintenance", MODULE_PATH)
19
+ assert SPEC and SPEC.loader
20
+ MODULE = importlib.util.module_from_spec(SPEC)
21
+ SPEC.loader.exec_module(MODULE)
22
+
23
+
24
+ def git(repo: Path, *args: str) -> str:
25
+ result = subprocess.run(
26
+ ["git", "-C", str(repo), *args],
27
+ text=True,
28
+ stdout=subprocess.PIPE,
29
+ stderr=subprocess.PIPE,
30
+ check=True,
31
+ )
32
+ return result.stdout.strip()
33
+
34
+
35
+ class AuditRepoMaintenanceTests(unittest.TestCase):
36
+ def setUp(self) -> None:
37
+ self.tempdir = tempfile.TemporaryDirectory()
38
+ self.addCleanup(self.tempdir.cleanup)
39
+ self.repo = Path(self.tempdir.name) / "repo"
40
+ self.repo.mkdir()
41
+ git(self.repo, "init", "-b", "main")
42
+ git(self.repo, "config", "user.email", "maintenance@example.test")
43
+ git(self.repo, "config", "user.name", "Maintenance Test")
44
+ (self.repo / "tracked.txt").write_text("base\n", encoding="utf-8")
45
+ git(self.repo, "add", "tracked.txt")
46
+ git(self.repo, "commit", "-m", "base")
47
+
48
+ def test_inventory_finds_unique_branch_worktree_and_stash(self) -> None:
49
+ git(self.repo, "checkout", "-b", "feature")
50
+ (self.repo / "feature.txt").write_text("feature\n", encoding="utf-8")
51
+ git(self.repo, "add", "feature.txt")
52
+ git(self.repo, "commit", "-m", "feature")
53
+ git(self.repo, "checkout", "main")
54
+ worktree = Path(self.tempdir.name) / "feature-worktree"
55
+ git(self.repo, "worktree", "add", str(worktree), "feature")
56
+ (self.repo / "untracked.txt").write_text("stash me\n", encoding="utf-8")
57
+ git(self.repo, "stash", "push", "--include-untracked", "-m", "test stash")
58
+
59
+ report = MODULE.audit(self.repo)
60
+
61
+ self.assertEqual(report["base"], "main")
62
+ self.assertEqual(len(report["worktrees"]), 2)
63
+ self.assertEqual(len(report["stashes"]), 1)
64
+ self.assertTrue(report["stashes"][0]["has_untracked_parent"])
65
+ self.assertIn("feature", report["unmerged_candidates"])
66
+ feature = next(item for item in report["branches"] if item["name"] == "feature")
67
+ self.assertEqual(feature["ahead_base"], 1)
68
+ self.assertFalse(feature["merged_into_base"])
69
+
70
+ def test_missing_registered_worktree_is_reported_without_aborting(self) -> None:
71
+ worktree = Path(self.tempdir.name) / "missing-worktree"
72
+ git(self.repo, "worktree", "add", "-b", "missing-lane", str(worktree))
73
+ git(self.repo, "worktree", "lock", "--reason", "test missing path", str(worktree))
74
+ shutil.rmtree(worktree)
75
+
76
+ report = MODULE.audit(self.repo)
77
+
78
+ missing = next(
79
+ item
80
+ for item in report["worktrees"]
81
+ if Path(item["worktree"]).resolve() == worktree.resolve()
82
+ )
83
+ self.assertFalse(missing["exists"])
84
+ self.assertFalse(missing["dirty"])
85
+ self.assertEqual(missing["dirty_paths"], [])
86
+ self.assertEqual(
87
+ [Path(path).resolve() for path in report["missing_worktrees"]],
88
+ [worktree.resolve()],
89
+ )
90
+
91
+ def test_prunable_worktree_directory_with_broken_git_pointer_does_not_abort(self) -> None:
92
+ worktree = Path(self.tempdir.name) / "prunable-worktree"
93
+ git(self.repo, "worktree", "add", "-b", "prunable-lane", str(worktree))
94
+ (worktree / ".git").unlink()
95
+
96
+ report = MODULE.audit(self.repo)
97
+
98
+ prunable = next(
99
+ item
100
+ for item in report["worktrees"]
101
+ if Path(item["worktree"]).resolve() == worktree.resolve()
102
+ )
103
+ self.assertTrue(prunable["exists"])
104
+ self.assertFalse(prunable["git_available"])
105
+ self.assertFalse(prunable["dirty"])
106
+ self.assertEqual(prunable["dirty_paths"], [])
107
+ self.assertIn(
108
+ worktree.resolve(),
109
+ [Path(path).resolve() for path in report["missing_worktrees"]],
110
+ )
111
+
112
+ def test_merged_branch_becomes_removal_candidate(self) -> None:
113
+ git(self.repo, "checkout", "-b", "feature")
114
+ (self.repo / "feature.txt").write_text("feature\n", encoding="utf-8")
115
+ git(self.repo, "add", "feature.txt")
116
+ git(self.repo, "commit", "-m", "feature")
117
+ git(self.repo, "checkout", "main")
118
+ git(self.repo, "merge", "--ff-only", "feature")
119
+
120
+ report = MODULE.audit(self.repo)
121
+
122
+ self.assertIn("feature", report["merged_branch_candidates"])
123
+ self.assertNotIn("feature", report["unmerged_candidates"])
124
+
125
+ def test_artifact_inventory_protects_canonical_and_marks_stale_ignored_cache(self) -> None:
126
+ (self.repo / ".gitignore").write_text("build*/\n", encoding="utf-8")
127
+ git(self.repo, "add", ".gitignore")
128
+ git(self.repo, "commit", "-m", "ignore builds")
129
+ canonical = self.repo / "build"
130
+ stale = self.repo / "build-old-lane"
131
+ canonical.mkdir()
132
+ stale.mkdir()
133
+ (canonical / "current.bin").write_bytes(b"current")
134
+ old_file = stale / "old.bin"
135
+ old_file.write_bytes(b"old-cache")
136
+ old = time.time() - (10 * 86_400)
137
+ os.utime(old_file, (old, old))
138
+ os.utime(stale, (old, old))
139
+
140
+ report = MODULE.audit(
141
+ self.repo,
142
+ include_artifacts=True,
143
+ stale_days=7,
144
+ )
145
+
146
+ artifacts = {item["path"]: item for item in report["artifacts"]["artifacts"]}
147
+ self.assertEqual(artifacts["build"]["disposition"], "protected")
148
+ self.assertFalse(artifacts["build"]["cleanup_candidate"])
149
+ self.assertEqual(artifacts["build-old-lane"]["disposition"], "cleanup-candidate")
150
+ self.assertTrue(artifacts["build-old-lane"]["ignored_by_git"])
151
+ self.assertGreater(report["artifacts"]["cleanup_candidate_bytes"], 0)
152
+ self.assertEqual(report["artifacts"]["default_protected"], ["build", "build-rust"])
153
+
154
+ def test_artifact_inventory_discovers_nested_cache_without_double_counting(self) -> None:
155
+ (self.repo / ".gitignore").write_text(".build/\n", encoding="utf-8")
156
+ git(self.repo, "add", ".gitignore")
157
+ git(self.repo, "commit", "-m", "ignore nested builds")
158
+ nested = self.repo / "Tests" / "FeatureTests" / ".build"
159
+ descendant = nested / "target"
160
+ descendant.mkdir(parents=True)
161
+ old_file = descendant / "cache.bin"
162
+ old_file.write_bytes(b"nested-cache")
163
+ old = time.time() - (10 * 86_400)
164
+ os.utime(old_file, (old, old))
165
+ os.utime(descendant, (old, old))
166
+ os.utime(nested, (old, old))
167
+
168
+ report = MODULE.audit(self.repo, include_artifacts=True, stale_days=7)
169
+
170
+ artifacts = report["artifacts"]["artifacts"]
171
+ self.assertEqual([item["path"] for item in artifacts], ["Tests/FeatureTests/.build"])
172
+ self.assertTrue(artifacts[0]["cleanup_candidate"])
173
+
174
+ def test_artifact_inventory_prunes_python_environments(self) -> None:
175
+ false_artifact = (
176
+ self.repo
177
+ / ".venv"
178
+ / "lib"
179
+ / "python3.13"
180
+ / "site-packages"
181
+ / "build_loop-0.12.16.dist-info"
182
+ )
183
+ false_artifact.mkdir(parents=True)
184
+ custom_env = self.repo / ".python-runtime"
185
+ custom_env.mkdir()
186
+ (custom_env / "pyvenv.cfg").write_text("home = /usr/bin\n", encoding="utf-8")
187
+ (custom_env / "build_generated").mkdir()
188
+
189
+ paths = MODULE.discover_artifact_paths(self.repo.resolve(), MODULE.DEFAULT_ARTIFACT_PATTERNS)
190
+
191
+ self.assertEqual(paths, [])
192
+
193
+ def test_release_artifact_requires_explicit_disposition(self) -> None:
194
+ (self.repo / ".gitignore").write_text("dist/\n", encoding="utf-8")
195
+ git(self.repo, "add", ".gitignore")
196
+ git(self.repo, "commit", "-m", "ignore dist")
197
+ dist = self.repo / "dist"
198
+ dist.mkdir()
199
+ dmg = dist / "Example-1.0.dmg"
200
+ dmg.write_bytes(b"release")
201
+ old = time.time() - (10 * 86_400)
202
+ os.utime(dmg, (old, old))
203
+ os.utime(dist, (old, old))
204
+
205
+ report = MODULE.audit(self.repo, include_artifacts=True, stale_days=7)
206
+
207
+ artifact = report["artifacts"]["artifacts"][0]
208
+ self.assertEqual(artifact["disposition"], "release-artifact")
209
+ self.assertFalse(artifact["cleanup_candidate"])
210
+ self.assertEqual(artifact["release_artifact_evidence"], ["dist/Example-1.0.dmg"])
211
+
212
+ def test_build_product_app_bundle_remains_a_cache_candidate(self) -> None:
213
+ (self.repo / ".gitignore").write_text("build-lane/\n", encoding="utf-8")
214
+ git(self.repo, "add", ".gitignore")
215
+ git(self.repo, "commit", "-m", "ignore lane build")
216
+ app = self.repo / "build-lane" / "Build" / "Products" / "Release" / "Example.app"
217
+ app.mkdir(parents=True)
218
+ binary = app / "Example"
219
+ binary.write_bytes(b"generated-product")
220
+ old = time.time() - (10 * 86_400)
221
+ os.utime(binary, (old, old))
222
+ for parent in (
223
+ app,
224
+ app.parent,
225
+ app.parent.parent,
226
+ app.parent.parent.parent,
227
+ app.parent.parent.parent.parent,
228
+ ):
229
+ os.utime(parent, (old, old))
230
+
231
+ report = MODULE.audit(self.repo, include_artifacts=True, stale_days=7)
232
+
233
+ artifact = report["artifacts"]["artifacts"][0]
234
+ self.assertEqual(artifact["path"], "build-lane")
235
+ self.assertEqual(artifact["disposition"], "cleanup-candidate")
236
+ self.assertFalse(artifact["contains_release_artifact"])
237
+
238
+ def test_existing_and_missing_process_artifacts_are_protected(self) -> None:
239
+ (self.repo / ".gitignore").write_text("build-*/\n", encoding="utf-8")
240
+ git(self.repo, "add", ".gitignore")
241
+ git(self.repo, "commit", "-m", "ignore lane builds")
242
+ active = self.repo / "build-live"
243
+ active.mkdir()
244
+ active_file = active / "ptyd"
245
+ active_file.write_bytes(b"daemon")
246
+ old = time.time() - (10 * 86_400)
247
+ os.utime(active_file, (old, old))
248
+ os.utime(active, (old, old))
249
+ resolved_repo = self.repo.resolve()
250
+ processes = [
251
+ (101, f"{active.resolve()}/ptyd --socket /tmp/live.sock"),
252
+ (202, f"{resolved_repo}/build-deleted/daemon/ptyd --socket /tmp/deleted.sock"),
253
+ ]
254
+
255
+ with mock.patch.object(MODULE, "process_snapshot", return_value=processes):
256
+ report = MODULE.audit(self.repo, include_artifacts=True, stale_days=7)
257
+
258
+ artifact = report["artifacts"]["artifacts"][0]
259
+ self.assertEqual(artifact["path"], "build-live")
260
+ self.assertEqual(artifact["disposition"], "active")
261
+ self.assertFalse(artifact["cleanup_candidate"])
262
+ missing = report["artifacts"]["active_missing_artifacts"]
263
+ self.assertEqual([item["path"] for item in missing], ["build-deleted"])
264
+ self.assertEqual(missing[0]["disposition"], "active-missing-artifact")
265
+
266
+ def test_source_comparison_matches_sibling_tree_to_target_prefix(self) -> None:
267
+ sibling = Path(self.tempdir.name) / "source"
268
+ sibling.mkdir()
269
+ git(sibling, "init", "-b", "main")
270
+ git(sibling, "config", "user.email", "source@example.test")
271
+ git(sibling, "config", "user.name", "Source Test")
272
+ (sibling / "lib.txt").write_text("shared\n", encoding="utf-8")
273
+ git(sibling, "add", "lib.txt")
274
+ git(sibling, "commit", "-m", "source")
275
+
276
+ target = self.repo / "daemon" / "source"
277
+ target.mkdir(parents=True)
278
+ (target / "lib.txt").write_text("shared\n", encoding="utf-8")
279
+ git(self.repo, "add", "daemon/source/lib.txt")
280
+ git(self.repo, "commit", "-m", "import source")
281
+
282
+ report = MODULE.audit(
283
+ self.repo,
284
+ compare_repo=sibling,
285
+ compare_prefix="daemon/source",
286
+ )
287
+
288
+ comparison = report["source_comparison"]
289
+ self.assertTrue(comparison["exact_tree_match"])
290
+ self.assertEqual(comparison["matching_paths"], 1)
291
+ self.assertEqual(comparison["changed_paths"], [])
292
+ self.assertFalse(comparison["source_head_known_to_target_repo"])
293
+
294
+ def test_profile_signals_detect_mixed_apple_rust_workspace(self) -> None:
295
+ (self.repo / "Cargo.toml").write_text(
296
+ '[workspace]\nmembers = ["daemon/ptyd"]\n', encoding="utf-8"
297
+ )
298
+ daemon = self.repo / "daemon" / "ptyd"
299
+ (daemon / "src").mkdir(parents=True)
300
+ (daemon / "Cargo.toml").write_text(
301
+ '[package]\nname = "ptyd"\nversion = "0.1.0"\n', encoding="utf-8"
302
+ )
303
+ (daemon / "src" / "main.rs").write_text("fn main() {}\n", encoding="utf-8")
304
+ swift = self.repo / "Sources" / "App"
305
+ swift.mkdir(parents=True)
306
+ (swift / "App.swift").write_text("import SwiftUI\n", encoding="utf-8")
307
+ (self.repo / "project.yml").write_text("name: TestApp\n", encoding="utf-8")
308
+ (self.repo / "build.sh").write_text("#!/bin/sh\n", encoding="utf-8")
309
+ git(self.repo, "add", ".")
310
+ git(self.repo, "commit", "-m", "add mixed stack")
311
+
312
+ report = MODULE.audit(self.repo)
313
+ profile = report["profile_signals"]
314
+
315
+ self.assertEqual(report["schema_version"], 5)
316
+ self.assertEqual(profile["source_ref"], "main")
317
+ self.assertTrue(
318
+ {"Swift", "Rust"}.issubset({item["name"] for item in profile["languages"]})
319
+ )
320
+ self.assertTrue(
321
+ {"cargo", "xcodegen", "xcodebuild", "shell-build"}.issubset(
322
+ {item["name"] for item in profile["build_systems"]}
323
+ )
324
+ )
325
+ self.assertTrue(
326
+ {"apple-native", "service-or-daemon", "mixed-native-product"}.issubset(
327
+ {item["name"] for item in profile["application_signals"]}
328
+ )
329
+ )
330
+ self.assertTrue(
331
+ {"workspace", "runtime-layout", "mixed-language"}.issubset(
332
+ {item["name"] for item in profile["composition_signals"]}
333
+ )
334
+ )
335
+
336
+ def test_profile_signals_detect_node_web_workspace(self) -> None:
337
+ (self.repo / "app").mkdir()
338
+ (self.repo / "app" / "page.tsx").write_text(
339
+ "export default function Page() { return null }\n", encoding="utf-8"
340
+ )
341
+ (self.repo / "package.json").write_text(
342
+ """{
343
+ "private": true,
344
+ "workspaces": ["packages/*"],
345
+ "scripts": {"build": "next build"},
346
+ "dependencies": {"next": "15.0.0", "react": "19.0.0"}
347
+ }
348
+ """,
349
+ encoding="utf-8",
350
+ )
351
+ (self.repo / "pnpm-lock.yaml").write_text("lockfileVersion: '9.0'\n", encoding="utf-8")
352
+ git(self.repo, "add", ".")
353
+ git(self.repo, "commit", "-m", "add web stack")
354
+
355
+ profile = MODULE.audit(self.repo)["profile_signals"]
356
+
357
+ self.assertIn("TypeScript", {item["name"] for item in profile["languages"]})
358
+ self.assertTrue(
359
+ {"nextjs", "node-scripts", "pnpm"}.issubset(
360
+ {item["name"] for item in profile["build_systems"]}
361
+ )
362
+ )
363
+ self.assertIn(
364
+ "web-application", {item["name"] for item in profile["application_signals"]}
365
+ )
366
+ self.assertIn(
367
+ "workspace", {item["name"] for item in profile["composition_signals"]}
368
+ )
369
+ self.assertEqual(profile["scan_summary"]["package_manifests_scanned"], 1)
370
+ self.assertFalse(profile["scan_summary"]["package_manifest_scan_truncated"])
371
+
372
+ def test_profile_signals_read_canonical_ref_not_uncommitted_files(self) -> None:
373
+ (self.repo / "package.json").write_text(
374
+ '{"dependencies": {"next": "15.0.0"}}\n', encoding="utf-8"
375
+ )
376
+
377
+ profile = MODULE.audit(self.repo)["profile_signals"]
378
+
379
+ self.assertNotIn(
380
+ "web-application", {item["name"] for item in profile["application_signals"]}
381
+ )
382
+ self.assertNotIn("nextjs", {item["name"] for item in profile["build_systems"]})
383
+
384
+ def test_profile_signals_support_repository_without_commits(self) -> None:
385
+ empty = Path(self.tempdir.name) / "empty"
386
+ empty.mkdir()
387
+ git(empty, "init", "-b", "main")
388
+
389
+ report = MODULE.audit(empty)
390
+
391
+ self.assertFalse(report["base_exists"])
392
+ self.assertIsNone(report["profile_signals"]["source_ref"])
393
+ self.assertEqual(report["profile_signals"]["languages"], [])
394
+ self.assertEqual(report["profile_signals"]["scan_summary"]["tracked_paths"], 0)
395
+
396
+ def test_profile_signals_cover_common_build_system_roles(self) -> None:
397
+ files = {
398
+ "AndroidManifest.xml": "<manifest />\n",
399
+ "settings.gradle.kts": 'rootProject.name = "sample"\n',
400
+ "pyproject.toml": "[build-system]\nrequires = []\n",
401
+ "go.mod": "module example.test/sample\n",
402
+ "pom.xml": "<project />\n",
403
+ "CMakeLists.txt": "cmake_minimum_required(VERSION 3.20)\n",
404
+ "Dockerfile": "FROM scratch\n",
405
+ "main.tf": "terraform {}\n",
406
+ "Sample.csproj": "<Project />\n",
407
+ ".github/workflows/ci.yml": "name: CI\n",
408
+ "src/main.kt": "fun main() {}\n",
409
+ "src/main.py": "print('ok')\n",
410
+ "src/main.go": "package main\n",
411
+ "src/main.cs": "class Program {}\n",
412
+ "src/main.cpp": "int main() { return 0; }\n",
413
+ }
414
+ for relative, content in files.items():
415
+ path = self.repo / relative
416
+ path.parent.mkdir(parents=True, exist_ok=True)
417
+ path.write_text(content, encoding="utf-8")
418
+ git(self.repo, "add", ".")
419
+ git(self.repo, "commit", "-m", "add polyglot build fixtures")
420
+
421
+ profile = MODULE.audit(self.repo)["profile_signals"]
422
+ build_names = {item["name"] for item in profile["build_systems"]}
423
+
424
+ self.assertTrue(
425
+ {
426
+ "gradle",
427
+ "python-build",
428
+ "go",
429
+ "maven",
430
+ "cmake",
431
+ "docker",
432
+ "terraform",
433
+ "msbuild",
434
+ "github-actions",
435
+ }.issubset(build_names)
436
+ )
437
+ self.assertIn(
438
+ "android-native", {item["name"] for item in profile["application_signals"]}
439
+ )
440
+ self.assertIn(
441
+ "infrastructure", {item["name"] for item in profile["application_signals"]}
442
+ )
443
+
444
+
445
+ if __name__ == "__main__":
446
+ unittest.main()
447
+
448
+
449
+ class ScanHeadMovementTests(unittest.TestCase):
450
+ """A count taken from a tree that moved under the scan is a snapshot, not a fact.
451
+
452
+ 2026-08-28: an audit of this repo ran while a peer session was committing.
453
+ HEAD advanced three times mid-scan, so every number the report gave was a
454
+ blend of three trees, and nothing in the output said so. The reader had no
455
+ way to know the counts needed re-taking.
456
+ """
457
+
458
+ def setUp(self) -> None:
459
+ self.tempdir = tempfile.TemporaryDirectory()
460
+ self.addCleanup(self.tempdir.cleanup)
461
+ self.repo = Path(self.tempdir.name) / "repo"
462
+ self.repo.mkdir()
463
+ git(self.repo, "init", "-b", "main")
464
+ git(self.repo, "config", "user.email", "maintenance@example.test")
465
+ git(self.repo, "config", "user.name", "Maintenance Test")
466
+ (self.repo / "tracked.txt").write_text("base\n", encoding="utf-8")
467
+ git(self.repo, "add", "tracked.txt")
468
+ git(self.repo, "commit", "-m", "base")
469
+
470
+ def test_stable_head_reports_start_equals_end_and_not_moved(self) -> None:
471
+ report = MODULE.audit(self.repo)
472
+ scan_head = report["scan_head"]
473
+ self.assertFalse(scan_head["moved"])
474
+ self.assertEqual(scan_head["start"], scan_head["end"])
475
+ self.assertEqual(scan_head["start"], git(self.repo, "rev-parse", "HEAD"))
476
+
477
+ def test_a_peer_commit_mid_scan_is_reported_with_both_shas(self) -> None:
478
+ """Genuinely move HEAD partway through the scan, as a live peer would."""
479
+ before = git(self.repo, "rev-parse", "HEAD")
480
+ real_tracked_structure = MODULE.tracked_structure
481
+
482
+ def commit_then_delegate(root, *args, **kwargs):
483
+ # simulate the peer session landing a commit mid-audit
484
+ (self.repo / "peer.txt").write_text("peer wrote this\n", encoding="utf-8")
485
+ git(self.repo, "add", "peer.txt")
486
+ git(self.repo, "commit", "-m", "peer commit during scan")
487
+ return real_tracked_structure(root, *args, **kwargs)
488
+
489
+ with mock.patch.object(MODULE, "tracked_structure", commit_then_delegate):
490
+ report = MODULE.audit(self.repo)
491
+
492
+ after = git(self.repo, "rev-parse", "HEAD")
493
+ self.assertNotEqual(before, after, "fixture failed to move HEAD")
494
+
495
+ scan_head = report["scan_head"]
496
+ self.assertTrue(scan_head["moved"], "a mid-scan commit must be reported")
497
+ self.assertEqual(scan_head["start"], before)
498
+ self.assertEqual(scan_head["end"], after)
499
+
500
+ def test_text_render_names_both_shas_when_head_moved(self) -> None:
501
+ report = MODULE.audit(self.repo)
502
+ report["scan_head"] = {"start": "aaaa111", "end": "bbbb222", "moved": True}
503
+ text = MODULE.render_text(report)
504
+ self.assertIn("HEAD MOVED DURING SCAN", text)
505
+ self.assertIn("aaaa111", text)
506
+ self.assertIn("bbbb222", text)