@mc-and-his-agents/loom-installer 0.1.1

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 (856) hide show
  1. package/README.md +47 -0
  2. package/dist/src/claude.js +157 -0
  3. package/dist/src/cli.js +35 -0
  4. package/dist/src/codex.js +191 -0
  5. package/dist/src/index.js +175 -0
  6. package/dist/src/payload.js +42 -0
  7. package/dist/src/types.js +1 -0
  8. package/dist/src/utils.js +84 -0
  9. package/dist/test/installer.test.js +167 -0
  10. package/package.json +46 -0
  11. package/payload/manifest.json +4294 -0
  12. package/payload/plugin/loom/.codex-plugin/plugin.json +37 -0
  13. package/payload/plugin/loom/skills/README.md +128 -0
  14. package/payload/plugin/loom/skills/distribution-and-adapter-contract.md +359 -0
  15. package/payload/plugin/loom/skills/install-layout.json +91 -0
  16. package/payload/plugin/loom/skills/loom-adopt/SKILL.md +80 -0
  17. package/payload/plugin/loom/skills/loom-adopt/agents/openai.yaml +4 -0
  18. package/payload/plugin/loom/skills/loom-adopt/contract.json +45 -0
  19. package/payload/plugin/loom/skills/loom-adopt/references/input-signals.md +17 -0
  20. package/payload/plugin/loom/skills/loom-adopt/references/output-contract.md +17 -0
  21. package/payload/plugin/loom/skills/loom-adopt/scripts/loom-adopt.py +14 -0
  22. package/payload/plugin/loom/skills/loom-handoff/SKILL.md +23 -0
  23. package/payload/plugin/loom/skills/loom-handoff/agents/openai.yaml +4 -0
  24. package/payload/plugin/loom/skills/loom-handoff/contract.json +53 -0
  25. package/payload/plugin/loom/skills/loom-handoff/references/input-signals.md +7 -0
  26. package/payload/plugin/loom/skills/loom-handoff/references/output-contract.md +42 -0
  27. package/payload/plugin/loom/skills/loom-handoff/scripts/loom-handoff.py +14 -0
  28. package/payload/plugin/loom/skills/loom-init/SKILL.md +246 -0
  29. package/payload/plugin/loom/skills/loom-init/agents/openai.yaml +4 -0
  30. package/payload/plugin/loom/skills/loom-init/contract.json +76 -0
  31. package/payload/plugin/loom/skills/loom-init/references/input-signals.md +16 -0
  32. package/payload/plugin/loom/skills/loom-init/references/intake-signals.md +155 -0
  33. package/payload/plugin/loom/skills/loom-init/references/output-contract.md +208 -0
  34. package/payload/plugin/loom/skills/loom-init/scripts/loom-init.py +14 -0
  35. package/payload/plugin/loom/skills/loom-merge-ready/SKILL.md +23 -0
  36. package/payload/plugin/loom/skills/loom-merge-ready/agents/openai.yaml +4 -0
  37. package/payload/plugin/loom/skills/loom-merge-ready/contract.json +49 -0
  38. package/payload/plugin/loom/skills/loom-merge-ready/references/input-signals.md +7 -0
  39. package/payload/plugin/loom/skills/loom-merge-ready/references/output-contract.md +34 -0
  40. package/payload/plugin/loom/skills/loom-merge-ready/scripts/loom-merge-ready.py +14 -0
  41. package/payload/plugin/loom/skills/loom-pre-review/SKILL.md +69 -0
  42. package/payload/plugin/loom/skills/loom-pre-review/agents/openai.yaml +4 -0
  43. package/payload/plugin/loom/skills/loom-pre-review/contract.json +41 -0
  44. package/payload/plugin/loom/skills/loom-pre-review/references/input-signals.md +14 -0
  45. package/payload/plugin/loom/skills/loom-pre-review/references/output-contract.md +20 -0
  46. package/payload/plugin/loom/skills/loom-pre-review/scripts/loom-pre-review.py +14 -0
  47. package/payload/plugin/loom/skills/loom-resume/SKILL.md +72 -0
  48. package/payload/plugin/loom/skills/loom-resume/agents/openai.yaml +4 -0
  49. package/payload/plugin/loom/skills/loom-resume/contract.json +47 -0
  50. package/payload/plugin/loom/skills/loom-resume/references/input-signals.md +15 -0
  51. package/payload/plugin/loom/skills/loom-resume/references/output-contract.md +58 -0
  52. package/payload/plugin/loom/skills/loom-resume/scripts/loom-resume.py +14 -0
  53. package/payload/plugin/loom/skills/loom-retire/SKILL.md +26 -0
  54. package/payload/plugin/loom/skills/loom-retire/agents/openai.yaml +4 -0
  55. package/payload/plugin/loom/skills/loom-retire/contract.json +41 -0
  56. package/payload/plugin/loom/skills/loom-retire/references/input-signals.md +7 -0
  57. package/payload/plugin/loom/skills/loom-retire/references/output-contract.md +13 -0
  58. package/payload/plugin/loom/skills/loom-retire/scripts/loom-retire.py +14 -0
  59. package/payload/plugin/loom/skills/loom-review/SKILL.md +95 -0
  60. package/payload/plugin/loom/skills/loom-review/agents/openai.yaml +4 -0
  61. package/payload/plugin/loom/skills/loom-review/contract.json +49 -0
  62. package/payload/plugin/loom/skills/loom-review/references/input-signals.md +15 -0
  63. package/payload/plugin/loom/skills/loom-review/references/output-contract.md +57 -0
  64. package/payload/plugin/loom/skills/loom-review/scripts/loom-review.py +14 -0
  65. package/payload/plugin/loom/skills/registry.json +64 -0
  66. package/payload/plugin/loom/skills/route-matrix.md +63 -0
  67. package/payload/plugin/loom/skills/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  68. package/payload/plugin/loom/skills/shared/assets/review/loom-review-result-schema.json +93 -0
  69. package/payload/plugin/loom/skills/shared/assets/templates/scaffold/plan.md +51 -0
  70. package/payload/plugin/loom/skills/shared/assets/templates/scaffold/spec.md +48 -0
  71. package/payload/plugin/loom/skills/shared/references/adoption/deep-existing-repo-default.md +64 -0
  72. package/payload/plugin/loom/skills/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  73. package/payload/plugin/loom/skills/shared/references/adoption/routing-and-checkpoints.md +88 -0
  74. package/payload/plugin/loom/skills/shared/references/governance/host-object-taxonomy.md +148 -0
  75. package/payload/plugin/loom/skills/shared/references/governance/issue-model.md +127 -0
  76. package/payload/plugin/loom/skills/shared/references/governance/maturity-and-closing.md +69 -0
  77. package/payload/plugin/loom/skills/shared/references/governance/principles.md +180 -0
  78. package/payload/plugin/loom/skills/shared/references/governance/review-model.md +109 -0
  79. package/payload/plugin/loom/skills/shared/references/governance/state-machine.md +163 -0
  80. package/payload/plugin/loom/skills/shared/references/governance/truth-and-sync-boundary.md +161 -0
  81. package/payload/plugin/loom/skills/shared/references/harness/automation-frontload.md +139 -0
  82. package/payload/plugin/loom/skills/shared/references/harness/closeout-gate.md +97 -0
  83. package/payload/plugin/loom/skills/shared/references/harness/execution-chain.md +56 -0
  84. package/payload/plugin/loom/skills/shared/references/harness/execution-context.md +71 -0
  85. package/payload/plugin/loom/skills/shared/references/harness/fact-chain-contract.md +160 -0
  86. package/payload/plugin/loom/skills/shared/references/harness/host-action-contract.md +128 -0
  87. package/payload/plugin/loom/skills/shared/references/harness/host-issue-binding.md +90 -0
  88. package/payload/plugin/loom/skills/shared/references/harness/host-lifecycle-boundary.md +56 -0
  89. package/payload/plugin/loom/skills/shared/references/harness/merge-checkpoint.md +95 -0
  90. package/payload/plugin/loom/skills/shared/references/harness/reconciliation-audit.md +64 -0
  91. package/payload/plugin/loom/skills/shared/references/harness/recovery-model.md +101 -0
  92. package/payload/plugin/loom/skills/shared/references/harness/review-execution.md +99 -0
  93. package/payload/plugin/loom/skills/shared/references/harness/runtime-state.md +103 -0
  94. package/payload/plugin/loom/skills/shared/references/harness/status-surface.md +121 -0
  95. package/payload/plugin/loom/skills/shared/references/harness/work-item-contract.md +104 -0
  96. package/payload/plugin/loom/skills/shared/references/harness/workspace-and-purity.md +73 -0
  97. package/payload/plugin/loom/skills/shared/references/harness/workspace-model.md +56 -0
  98. package/payload/plugin/loom/skills/shared/references/templates/pull-request.md +44 -0
  99. package/payload/plugin/loom/skills/shared/references/templates/review-record.md +35 -0
  100. package/payload/plugin/loom/skills/shared/references/templates/spec-suite.md +57 -0
  101. package/payload/plugin/loom/skills/shared/scripts/fact_chain_support.py +509 -0
  102. package/payload/plugin/loom/skills/shared/scripts/governance_surface.py +869 -0
  103. package/payload/plugin/loom/skills/shared/scripts/loom_check.py +4724 -0
  104. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +5390 -0
  105. package/payload/plugin/loom/skills/shared/scripts/loom_init.py +1966 -0
  106. package/payload/plugin/loom/skills/shared/scripts/runtime_paths.py +116 -0
  107. package/payload/plugin/loom/skills/shared/scripts/runtime_state.py +405 -0
  108. package/payload/plugin/loom/skills/upgrade-contract.json +29 -0
  109. package/payload/skills/loom-adopt/.loom-runtime/install-layout.json +77 -0
  110. package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/SKILL.md +80 -0
  111. package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/agents/openai.yaml +4 -0
  112. package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/contract.json +45 -0
  113. package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/references/input-signals.md +17 -0
  114. package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/references/output-contract.md +17 -0
  115. package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/scripts/loom-adopt.py +14 -0
  116. package/payload/skills/loom-adopt/.loom-runtime/loom-init/references/input-signals.md +16 -0
  117. package/payload/skills/loom-adopt/.loom-runtime/loom-init/references/intake-signals.md +155 -0
  118. package/payload/skills/loom-adopt/.loom-runtime/loom-init/references/output-contract.md +208 -0
  119. package/payload/skills/loom-adopt/.loom-runtime/registry.json +15 -0
  120. package/payload/skills/loom-adopt/.loom-runtime/route-matrix.md +63 -0
  121. package/payload/skills/loom-adopt/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  122. package/payload/skills/loom-adopt/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
  123. package/payload/skills/loom-adopt/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
  124. package/payload/skills/loom-adopt/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
  125. package/payload/skills/loom-adopt/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
  126. package/payload/skills/loom-adopt/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  127. package/payload/skills/loom-adopt/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
  128. package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
  129. package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/issue-model.md +127 -0
  130. package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
  131. package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/principles.md +180 -0
  132. package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/review-model.md +109 -0
  133. package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/state-machine.md +163 -0
  134. package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
  135. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
  136. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
  137. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
  138. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/execution-context.md +71 -0
  139. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
  140. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
  141. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
  142. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
  143. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
  144. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
  145. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
  146. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/review-execution.md +99 -0
  147. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
  148. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/status-surface.md +121 -0
  149. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
  150. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
  151. package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
  152. package/payload/skills/loom-adopt/.loom-runtime/shared/references/templates/pull-request.md +44 -0
  153. package/payload/skills/loom-adopt/.loom-runtime/shared/references/templates/review-record.md +35 -0
  154. package/payload/skills/loom-adopt/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
  155. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
  156. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/governance_surface.py +879 -0
  157. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +4683 -0
  158. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
  159. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py +1966 -0
  160. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
  161. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/runtime_state.py +405 -0
  162. package/payload/skills/loom-adopt/.loom-runtime/upgrade-contract.json +29 -0
  163. package/payload/skills/loom-adopt/SKILL.md +80 -0
  164. package/payload/skills/loom-adopt/agents/openai.yaml +4 -0
  165. package/payload/skills/loom-adopt/contract.json +45 -0
  166. package/payload/skills/loom-adopt/references/input-signals.md +17 -0
  167. package/payload/skills/loom-adopt/references/loom-init/input-signals.md +16 -0
  168. package/payload/skills/loom-adopt/references/loom-init/intake-signals.md +155 -0
  169. package/payload/skills/loom-adopt/references/loom-init/output-contract.md +208 -0
  170. package/payload/skills/loom-adopt/references/output-contract.md +17 -0
  171. package/payload/skills/loom-adopt/references/route-matrix.md +63 -0
  172. package/payload/skills/loom-adopt/references/shared/adoption/deep-existing-repo-default.md +64 -0
  173. package/payload/skills/loom-adopt/references/shared/adoption/lightweight-retrofit-default.md +71 -0
  174. package/payload/skills/loom-adopt/references/shared/adoption/routing-and-checkpoints.md +88 -0
  175. package/payload/skills/loom-adopt/references/shared/governance/host-object-taxonomy.md +148 -0
  176. package/payload/skills/loom-adopt/references/shared/governance/issue-model.md +127 -0
  177. package/payload/skills/loom-adopt/references/shared/governance/maturity-and-closing.md +69 -0
  178. package/payload/skills/loom-adopt/references/shared/governance/principles.md +180 -0
  179. package/payload/skills/loom-adopt/references/shared/governance/review-model.md +109 -0
  180. package/payload/skills/loom-adopt/references/shared/governance/state-machine.md +163 -0
  181. package/payload/skills/loom-adopt/references/shared/governance/truth-and-sync-boundary.md +161 -0
  182. package/payload/skills/loom-adopt/references/shared/harness/automation-frontload.md +139 -0
  183. package/payload/skills/loom-adopt/references/shared/harness/closeout-gate.md +97 -0
  184. package/payload/skills/loom-adopt/references/shared/harness/execution-chain.md +56 -0
  185. package/payload/skills/loom-adopt/references/shared/harness/execution-context.md +71 -0
  186. package/payload/skills/loom-adopt/references/shared/harness/fact-chain-contract.md +160 -0
  187. package/payload/skills/loom-adopt/references/shared/harness/host-action-contract.md +128 -0
  188. package/payload/skills/loom-adopt/references/shared/harness/host-issue-binding.md +90 -0
  189. package/payload/skills/loom-adopt/references/shared/harness/host-lifecycle-boundary.md +56 -0
  190. package/payload/skills/loom-adopt/references/shared/harness/merge-checkpoint.md +95 -0
  191. package/payload/skills/loom-adopt/references/shared/harness/reconciliation-audit.md +64 -0
  192. package/payload/skills/loom-adopt/references/shared/harness/recovery-model.md +101 -0
  193. package/payload/skills/loom-adopt/references/shared/harness/review-execution.md +99 -0
  194. package/payload/skills/loom-adopt/references/shared/harness/runtime-state.md +103 -0
  195. package/payload/skills/loom-adopt/references/shared/harness/status-surface.md +121 -0
  196. package/payload/skills/loom-adopt/references/shared/harness/work-item-contract.md +104 -0
  197. package/payload/skills/loom-adopt/references/shared/harness/workspace-and-purity.md +73 -0
  198. package/payload/skills/loom-adopt/references/shared/harness/workspace-model.md +56 -0
  199. package/payload/skills/loom-adopt/references/shared/templates/pull-request.md +44 -0
  200. package/payload/skills/loom-adopt/references/shared/templates/review-record.md +35 -0
  201. package/payload/skills/loom-adopt/references/shared/templates/spec-suite.md +57 -0
  202. package/payload/skills/loom-adopt/scripts/loom-adopt.py +16 -0
  203. package/payload/skills/loom-handoff/.loom-runtime/install-layout.json +77 -0
  204. package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/SKILL.md +23 -0
  205. package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/agents/openai.yaml +4 -0
  206. package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/contract.json +53 -0
  207. package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/references/input-signals.md +7 -0
  208. package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/references/output-contract.md +42 -0
  209. package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/scripts/loom-handoff.py +14 -0
  210. package/payload/skills/loom-handoff/.loom-runtime/loom-init/references/input-signals.md +16 -0
  211. package/payload/skills/loom-handoff/.loom-runtime/loom-init/references/intake-signals.md +155 -0
  212. package/payload/skills/loom-handoff/.loom-runtime/loom-init/references/output-contract.md +208 -0
  213. package/payload/skills/loom-handoff/.loom-runtime/registry.json +15 -0
  214. package/payload/skills/loom-handoff/.loom-runtime/route-matrix.md +63 -0
  215. package/payload/skills/loom-handoff/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  216. package/payload/skills/loom-handoff/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
  217. package/payload/skills/loom-handoff/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
  218. package/payload/skills/loom-handoff/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
  219. package/payload/skills/loom-handoff/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
  220. package/payload/skills/loom-handoff/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  221. package/payload/skills/loom-handoff/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
  222. package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
  223. package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/issue-model.md +127 -0
  224. package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
  225. package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/principles.md +180 -0
  226. package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/review-model.md +109 -0
  227. package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/state-machine.md +163 -0
  228. package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
  229. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
  230. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
  231. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
  232. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/execution-context.md +71 -0
  233. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
  234. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
  235. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
  236. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
  237. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
  238. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
  239. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
  240. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/review-execution.md +99 -0
  241. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
  242. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/status-surface.md +121 -0
  243. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
  244. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
  245. package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
  246. package/payload/skills/loom-handoff/.loom-runtime/shared/references/templates/pull-request.md +44 -0
  247. package/payload/skills/loom-handoff/.loom-runtime/shared/references/templates/review-record.md +35 -0
  248. package/payload/skills/loom-handoff/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
  249. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
  250. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/governance_surface.py +879 -0
  251. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +4683 -0
  252. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
  253. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py +1966 -0
  254. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
  255. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/runtime_state.py +405 -0
  256. package/payload/skills/loom-handoff/.loom-runtime/upgrade-contract.json +29 -0
  257. package/payload/skills/loom-handoff/SKILL.md +23 -0
  258. package/payload/skills/loom-handoff/agents/openai.yaml +4 -0
  259. package/payload/skills/loom-handoff/contract.json +53 -0
  260. package/payload/skills/loom-handoff/references/input-signals.md +7 -0
  261. package/payload/skills/loom-handoff/references/loom-init/input-signals.md +16 -0
  262. package/payload/skills/loom-handoff/references/loom-init/intake-signals.md +155 -0
  263. package/payload/skills/loom-handoff/references/loom-init/output-contract.md +208 -0
  264. package/payload/skills/loom-handoff/references/output-contract.md +42 -0
  265. package/payload/skills/loom-handoff/references/route-matrix.md +63 -0
  266. package/payload/skills/loom-handoff/references/shared/adoption/deep-existing-repo-default.md +64 -0
  267. package/payload/skills/loom-handoff/references/shared/adoption/lightweight-retrofit-default.md +71 -0
  268. package/payload/skills/loom-handoff/references/shared/adoption/routing-and-checkpoints.md +88 -0
  269. package/payload/skills/loom-handoff/references/shared/governance/host-object-taxonomy.md +148 -0
  270. package/payload/skills/loom-handoff/references/shared/governance/issue-model.md +127 -0
  271. package/payload/skills/loom-handoff/references/shared/governance/maturity-and-closing.md +69 -0
  272. package/payload/skills/loom-handoff/references/shared/governance/principles.md +180 -0
  273. package/payload/skills/loom-handoff/references/shared/governance/review-model.md +109 -0
  274. package/payload/skills/loom-handoff/references/shared/governance/state-machine.md +163 -0
  275. package/payload/skills/loom-handoff/references/shared/governance/truth-and-sync-boundary.md +161 -0
  276. package/payload/skills/loom-handoff/references/shared/harness/automation-frontload.md +139 -0
  277. package/payload/skills/loom-handoff/references/shared/harness/closeout-gate.md +97 -0
  278. package/payload/skills/loom-handoff/references/shared/harness/execution-chain.md +56 -0
  279. package/payload/skills/loom-handoff/references/shared/harness/execution-context.md +71 -0
  280. package/payload/skills/loom-handoff/references/shared/harness/fact-chain-contract.md +160 -0
  281. package/payload/skills/loom-handoff/references/shared/harness/host-action-contract.md +128 -0
  282. package/payload/skills/loom-handoff/references/shared/harness/host-issue-binding.md +90 -0
  283. package/payload/skills/loom-handoff/references/shared/harness/host-lifecycle-boundary.md +56 -0
  284. package/payload/skills/loom-handoff/references/shared/harness/merge-checkpoint.md +95 -0
  285. package/payload/skills/loom-handoff/references/shared/harness/reconciliation-audit.md +64 -0
  286. package/payload/skills/loom-handoff/references/shared/harness/recovery-model.md +101 -0
  287. package/payload/skills/loom-handoff/references/shared/harness/review-execution.md +99 -0
  288. package/payload/skills/loom-handoff/references/shared/harness/runtime-state.md +103 -0
  289. package/payload/skills/loom-handoff/references/shared/harness/status-surface.md +121 -0
  290. package/payload/skills/loom-handoff/references/shared/harness/work-item-contract.md +104 -0
  291. package/payload/skills/loom-handoff/references/shared/harness/workspace-and-purity.md +73 -0
  292. package/payload/skills/loom-handoff/references/shared/harness/workspace-model.md +56 -0
  293. package/payload/skills/loom-handoff/references/shared/templates/pull-request.md +44 -0
  294. package/payload/skills/loom-handoff/references/shared/templates/review-record.md +35 -0
  295. package/payload/skills/loom-handoff/references/shared/templates/spec-suite.md +57 -0
  296. package/payload/skills/loom-handoff/scripts/loom-handoff.py +16 -0
  297. package/payload/skills/loom-init/.loom-runtime/install-layout.json +77 -0
  298. package/payload/skills/loom-init/.loom-runtime/loom-init/SKILL.md +246 -0
  299. package/payload/skills/loom-init/.loom-runtime/loom-init/agents/openai.yaml +4 -0
  300. package/payload/skills/loom-init/.loom-runtime/loom-init/contract.json +76 -0
  301. package/payload/skills/loom-init/.loom-runtime/loom-init/references/input-signals.md +16 -0
  302. package/payload/skills/loom-init/.loom-runtime/loom-init/references/intake-signals.md +155 -0
  303. package/payload/skills/loom-init/.loom-runtime/loom-init/references/output-contract.md +208 -0
  304. package/payload/skills/loom-init/.loom-runtime/loom-init/scripts/loom-init.py +14 -0
  305. package/payload/skills/loom-init/.loom-runtime/registry.json +15 -0
  306. package/payload/skills/loom-init/.loom-runtime/route-matrix.md +63 -0
  307. package/payload/skills/loom-init/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  308. package/payload/skills/loom-init/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
  309. package/payload/skills/loom-init/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
  310. package/payload/skills/loom-init/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
  311. package/payload/skills/loom-init/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
  312. package/payload/skills/loom-init/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  313. package/payload/skills/loom-init/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
  314. package/payload/skills/loom-init/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
  315. package/payload/skills/loom-init/.loom-runtime/shared/references/governance/issue-model.md +127 -0
  316. package/payload/skills/loom-init/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
  317. package/payload/skills/loom-init/.loom-runtime/shared/references/governance/principles.md +180 -0
  318. package/payload/skills/loom-init/.loom-runtime/shared/references/governance/review-model.md +109 -0
  319. package/payload/skills/loom-init/.loom-runtime/shared/references/governance/state-machine.md +163 -0
  320. package/payload/skills/loom-init/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
  321. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
  322. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
  323. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
  324. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/execution-context.md +71 -0
  325. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
  326. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
  327. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
  328. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
  329. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
  330. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
  331. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
  332. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/review-execution.md +99 -0
  333. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
  334. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/status-surface.md +121 -0
  335. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
  336. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
  337. package/payload/skills/loom-init/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
  338. package/payload/skills/loom-init/.loom-runtime/shared/references/templates/pull-request.md +44 -0
  339. package/payload/skills/loom-init/.loom-runtime/shared/references/templates/review-record.md +35 -0
  340. package/payload/skills/loom-init/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
  341. package/payload/skills/loom-init/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
  342. package/payload/skills/loom-init/.loom-runtime/shared/scripts/governance_surface.py +879 -0
  343. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +4683 -0
  344. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
  345. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_init.py +1966 -0
  346. package/payload/skills/loom-init/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
  347. package/payload/skills/loom-init/.loom-runtime/shared/scripts/runtime_state.py +405 -0
  348. package/payload/skills/loom-init/.loom-runtime/upgrade-contract.json +29 -0
  349. package/payload/skills/loom-init/SKILL.md +246 -0
  350. package/payload/skills/loom-init/agents/openai.yaml +4 -0
  351. package/payload/skills/loom-init/contract.json +76 -0
  352. package/payload/skills/loom-init/references/input-signals.md +16 -0
  353. package/payload/skills/loom-init/references/intake-signals.md +155 -0
  354. package/payload/skills/loom-init/references/output-contract.md +208 -0
  355. package/payload/skills/loom-init/references/route-matrix.md +63 -0
  356. package/payload/skills/loom-init/references/shared/adoption/deep-existing-repo-default.md +64 -0
  357. package/payload/skills/loom-init/references/shared/adoption/lightweight-retrofit-default.md +71 -0
  358. package/payload/skills/loom-init/references/shared/adoption/routing-and-checkpoints.md +88 -0
  359. package/payload/skills/loom-init/references/shared/governance/host-object-taxonomy.md +148 -0
  360. package/payload/skills/loom-init/references/shared/governance/issue-model.md +127 -0
  361. package/payload/skills/loom-init/references/shared/governance/maturity-and-closing.md +69 -0
  362. package/payload/skills/loom-init/references/shared/governance/principles.md +180 -0
  363. package/payload/skills/loom-init/references/shared/governance/review-model.md +109 -0
  364. package/payload/skills/loom-init/references/shared/governance/state-machine.md +163 -0
  365. package/payload/skills/loom-init/references/shared/governance/truth-and-sync-boundary.md +161 -0
  366. package/payload/skills/loom-init/references/shared/harness/automation-frontload.md +139 -0
  367. package/payload/skills/loom-init/references/shared/harness/closeout-gate.md +97 -0
  368. package/payload/skills/loom-init/references/shared/harness/execution-chain.md +56 -0
  369. package/payload/skills/loom-init/references/shared/harness/execution-context.md +71 -0
  370. package/payload/skills/loom-init/references/shared/harness/fact-chain-contract.md +160 -0
  371. package/payload/skills/loom-init/references/shared/harness/host-action-contract.md +128 -0
  372. package/payload/skills/loom-init/references/shared/harness/host-issue-binding.md +90 -0
  373. package/payload/skills/loom-init/references/shared/harness/host-lifecycle-boundary.md +56 -0
  374. package/payload/skills/loom-init/references/shared/harness/merge-checkpoint.md +95 -0
  375. package/payload/skills/loom-init/references/shared/harness/reconciliation-audit.md +64 -0
  376. package/payload/skills/loom-init/references/shared/harness/recovery-model.md +101 -0
  377. package/payload/skills/loom-init/references/shared/harness/review-execution.md +99 -0
  378. package/payload/skills/loom-init/references/shared/harness/runtime-state.md +103 -0
  379. package/payload/skills/loom-init/references/shared/harness/status-surface.md +121 -0
  380. package/payload/skills/loom-init/references/shared/harness/work-item-contract.md +104 -0
  381. package/payload/skills/loom-init/references/shared/harness/workspace-and-purity.md +73 -0
  382. package/payload/skills/loom-init/references/shared/harness/workspace-model.md +56 -0
  383. package/payload/skills/loom-init/references/shared/templates/pull-request.md +44 -0
  384. package/payload/skills/loom-init/references/shared/templates/review-record.md +35 -0
  385. package/payload/skills/loom-init/references/shared/templates/spec-suite.md +57 -0
  386. package/payload/skills/loom-init/scripts/loom-init.py +16 -0
  387. package/payload/skills/loom-merge-ready/.loom-runtime/install-layout.json +77 -0
  388. package/payload/skills/loom-merge-ready/.loom-runtime/loom-init/references/input-signals.md +16 -0
  389. package/payload/skills/loom-merge-ready/.loom-runtime/loom-init/references/intake-signals.md +155 -0
  390. package/payload/skills/loom-merge-ready/.loom-runtime/loom-init/references/output-contract.md +208 -0
  391. package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/SKILL.md +23 -0
  392. package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/agents/openai.yaml +4 -0
  393. package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/contract.json +49 -0
  394. package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/references/input-signals.md +7 -0
  395. package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/references/output-contract.md +34 -0
  396. package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/scripts/loom-merge-ready.py +14 -0
  397. package/payload/skills/loom-merge-ready/.loom-runtime/registry.json +15 -0
  398. package/payload/skills/loom-merge-ready/.loom-runtime/route-matrix.md +63 -0
  399. package/payload/skills/loom-merge-ready/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  400. package/payload/skills/loom-merge-ready/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
  401. package/payload/skills/loom-merge-ready/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
  402. package/payload/skills/loom-merge-ready/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
  403. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
  404. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  405. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
  406. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
  407. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/issue-model.md +127 -0
  408. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
  409. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/principles.md +180 -0
  410. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/review-model.md +109 -0
  411. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/state-machine.md +163 -0
  412. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
  413. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
  414. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
  415. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
  416. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/execution-context.md +71 -0
  417. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
  418. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
  419. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
  420. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
  421. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
  422. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
  423. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
  424. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/review-execution.md +99 -0
  425. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
  426. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/status-surface.md +121 -0
  427. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
  428. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
  429. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
  430. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/templates/pull-request.md +44 -0
  431. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/templates/review-record.md +35 -0
  432. package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
  433. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
  434. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/governance_surface.py +879 -0
  435. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +4683 -0
  436. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
  437. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py +1966 -0
  438. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
  439. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/runtime_state.py +405 -0
  440. package/payload/skills/loom-merge-ready/.loom-runtime/upgrade-contract.json +29 -0
  441. package/payload/skills/loom-merge-ready/SKILL.md +23 -0
  442. package/payload/skills/loom-merge-ready/agents/openai.yaml +4 -0
  443. package/payload/skills/loom-merge-ready/contract.json +49 -0
  444. package/payload/skills/loom-merge-ready/references/input-signals.md +7 -0
  445. package/payload/skills/loom-merge-ready/references/loom-init/input-signals.md +16 -0
  446. package/payload/skills/loom-merge-ready/references/loom-init/intake-signals.md +155 -0
  447. package/payload/skills/loom-merge-ready/references/loom-init/output-contract.md +208 -0
  448. package/payload/skills/loom-merge-ready/references/output-contract.md +34 -0
  449. package/payload/skills/loom-merge-ready/references/route-matrix.md +63 -0
  450. package/payload/skills/loom-merge-ready/references/shared/adoption/deep-existing-repo-default.md +64 -0
  451. package/payload/skills/loom-merge-ready/references/shared/adoption/lightweight-retrofit-default.md +71 -0
  452. package/payload/skills/loom-merge-ready/references/shared/adoption/routing-and-checkpoints.md +88 -0
  453. package/payload/skills/loom-merge-ready/references/shared/governance/host-object-taxonomy.md +148 -0
  454. package/payload/skills/loom-merge-ready/references/shared/governance/issue-model.md +127 -0
  455. package/payload/skills/loom-merge-ready/references/shared/governance/maturity-and-closing.md +69 -0
  456. package/payload/skills/loom-merge-ready/references/shared/governance/principles.md +180 -0
  457. package/payload/skills/loom-merge-ready/references/shared/governance/review-model.md +109 -0
  458. package/payload/skills/loom-merge-ready/references/shared/governance/state-machine.md +163 -0
  459. package/payload/skills/loom-merge-ready/references/shared/governance/truth-and-sync-boundary.md +161 -0
  460. package/payload/skills/loom-merge-ready/references/shared/harness/automation-frontload.md +139 -0
  461. package/payload/skills/loom-merge-ready/references/shared/harness/closeout-gate.md +97 -0
  462. package/payload/skills/loom-merge-ready/references/shared/harness/execution-chain.md +56 -0
  463. package/payload/skills/loom-merge-ready/references/shared/harness/execution-context.md +71 -0
  464. package/payload/skills/loom-merge-ready/references/shared/harness/fact-chain-contract.md +160 -0
  465. package/payload/skills/loom-merge-ready/references/shared/harness/host-action-contract.md +128 -0
  466. package/payload/skills/loom-merge-ready/references/shared/harness/host-issue-binding.md +90 -0
  467. package/payload/skills/loom-merge-ready/references/shared/harness/host-lifecycle-boundary.md +56 -0
  468. package/payload/skills/loom-merge-ready/references/shared/harness/merge-checkpoint.md +95 -0
  469. package/payload/skills/loom-merge-ready/references/shared/harness/reconciliation-audit.md +64 -0
  470. package/payload/skills/loom-merge-ready/references/shared/harness/recovery-model.md +101 -0
  471. package/payload/skills/loom-merge-ready/references/shared/harness/review-execution.md +99 -0
  472. package/payload/skills/loom-merge-ready/references/shared/harness/runtime-state.md +103 -0
  473. package/payload/skills/loom-merge-ready/references/shared/harness/status-surface.md +121 -0
  474. package/payload/skills/loom-merge-ready/references/shared/harness/work-item-contract.md +104 -0
  475. package/payload/skills/loom-merge-ready/references/shared/harness/workspace-and-purity.md +73 -0
  476. package/payload/skills/loom-merge-ready/references/shared/harness/workspace-model.md +56 -0
  477. package/payload/skills/loom-merge-ready/references/shared/templates/pull-request.md +44 -0
  478. package/payload/skills/loom-merge-ready/references/shared/templates/review-record.md +35 -0
  479. package/payload/skills/loom-merge-ready/references/shared/templates/spec-suite.md +57 -0
  480. package/payload/skills/loom-merge-ready/scripts/loom-merge-ready.py +16 -0
  481. package/payload/skills/loom-pre-review/.loom-runtime/install-layout.json +77 -0
  482. package/payload/skills/loom-pre-review/.loom-runtime/loom-init/references/input-signals.md +16 -0
  483. package/payload/skills/loom-pre-review/.loom-runtime/loom-init/references/intake-signals.md +155 -0
  484. package/payload/skills/loom-pre-review/.loom-runtime/loom-init/references/output-contract.md +208 -0
  485. package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/SKILL.md +69 -0
  486. package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/agents/openai.yaml +4 -0
  487. package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/contract.json +41 -0
  488. package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/references/input-signals.md +14 -0
  489. package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/references/output-contract.md +20 -0
  490. package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/scripts/loom-pre-review.py +14 -0
  491. package/payload/skills/loom-pre-review/.loom-runtime/registry.json +15 -0
  492. package/payload/skills/loom-pre-review/.loom-runtime/route-matrix.md +63 -0
  493. package/payload/skills/loom-pre-review/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  494. package/payload/skills/loom-pre-review/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
  495. package/payload/skills/loom-pre-review/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
  496. package/payload/skills/loom-pre-review/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
  497. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
  498. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  499. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
  500. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
  501. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/issue-model.md +127 -0
  502. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
  503. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/principles.md +180 -0
  504. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/review-model.md +109 -0
  505. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/state-machine.md +163 -0
  506. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
  507. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
  508. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
  509. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
  510. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/execution-context.md +71 -0
  511. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
  512. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
  513. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
  514. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
  515. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
  516. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
  517. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
  518. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/review-execution.md +99 -0
  519. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
  520. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/status-surface.md +121 -0
  521. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
  522. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
  523. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
  524. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/templates/pull-request.md +44 -0
  525. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/templates/review-record.md +35 -0
  526. package/payload/skills/loom-pre-review/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
  527. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
  528. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/governance_surface.py +879 -0
  529. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +4683 -0
  530. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
  531. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py +1966 -0
  532. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
  533. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/runtime_state.py +405 -0
  534. package/payload/skills/loom-pre-review/.loom-runtime/upgrade-contract.json +29 -0
  535. package/payload/skills/loom-pre-review/SKILL.md +69 -0
  536. package/payload/skills/loom-pre-review/agents/openai.yaml +4 -0
  537. package/payload/skills/loom-pre-review/contract.json +41 -0
  538. package/payload/skills/loom-pre-review/references/input-signals.md +14 -0
  539. package/payload/skills/loom-pre-review/references/loom-init/input-signals.md +16 -0
  540. package/payload/skills/loom-pre-review/references/loom-init/intake-signals.md +155 -0
  541. package/payload/skills/loom-pre-review/references/loom-init/output-contract.md +208 -0
  542. package/payload/skills/loom-pre-review/references/output-contract.md +20 -0
  543. package/payload/skills/loom-pre-review/references/route-matrix.md +63 -0
  544. package/payload/skills/loom-pre-review/references/shared/adoption/deep-existing-repo-default.md +64 -0
  545. package/payload/skills/loom-pre-review/references/shared/adoption/lightweight-retrofit-default.md +71 -0
  546. package/payload/skills/loom-pre-review/references/shared/adoption/routing-and-checkpoints.md +88 -0
  547. package/payload/skills/loom-pre-review/references/shared/governance/host-object-taxonomy.md +148 -0
  548. package/payload/skills/loom-pre-review/references/shared/governance/issue-model.md +127 -0
  549. package/payload/skills/loom-pre-review/references/shared/governance/maturity-and-closing.md +69 -0
  550. package/payload/skills/loom-pre-review/references/shared/governance/principles.md +180 -0
  551. package/payload/skills/loom-pre-review/references/shared/governance/review-model.md +109 -0
  552. package/payload/skills/loom-pre-review/references/shared/governance/state-machine.md +163 -0
  553. package/payload/skills/loom-pre-review/references/shared/governance/truth-and-sync-boundary.md +161 -0
  554. package/payload/skills/loom-pre-review/references/shared/harness/automation-frontload.md +139 -0
  555. package/payload/skills/loom-pre-review/references/shared/harness/closeout-gate.md +97 -0
  556. package/payload/skills/loom-pre-review/references/shared/harness/execution-chain.md +56 -0
  557. package/payload/skills/loom-pre-review/references/shared/harness/execution-context.md +71 -0
  558. package/payload/skills/loom-pre-review/references/shared/harness/fact-chain-contract.md +160 -0
  559. package/payload/skills/loom-pre-review/references/shared/harness/host-action-contract.md +128 -0
  560. package/payload/skills/loom-pre-review/references/shared/harness/host-issue-binding.md +90 -0
  561. package/payload/skills/loom-pre-review/references/shared/harness/host-lifecycle-boundary.md +56 -0
  562. package/payload/skills/loom-pre-review/references/shared/harness/merge-checkpoint.md +95 -0
  563. package/payload/skills/loom-pre-review/references/shared/harness/reconciliation-audit.md +64 -0
  564. package/payload/skills/loom-pre-review/references/shared/harness/recovery-model.md +101 -0
  565. package/payload/skills/loom-pre-review/references/shared/harness/review-execution.md +99 -0
  566. package/payload/skills/loom-pre-review/references/shared/harness/runtime-state.md +103 -0
  567. package/payload/skills/loom-pre-review/references/shared/harness/status-surface.md +121 -0
  568. package/payload/skills/loom-pre-review/references/shared/harness/work-item-contract.md +104 -0
  569. package/payload/skills/loom-pre-review/references/shared/harness/workspace-and-purity.md +73 -0
  570. package/payload/skills/loom-pre-review/references/shared/harness/workspace-model.md +56 -0
  571. package/payload/skills/loom-pre-review/references/shared/templates/pull-request.md +44 -0
  572. package/payload/skills/loom-pre-review/references/shared/templates/review-record.md +35 -0
  573. package/payload/skills/loom-pre-review/references/shared/templates/spec-suite.md +57 -0
  574. package/payload/skills/loom-pre-review/scripts/loom-pre-review.py +16 -0
  575. package/payload/skills/loom-resume/.loom-runtime/install-layout.json +77 -0
  576. package/payload/skills/loom-resume/.loom-runtime/loom-init/references/input-signals.md +16 -0
  577. package/payload/skills/loom-resume/.loom-runtime/loom-init/references/intake-signals.md +155 -0
  578. package/payload/skills/loom-resume/.loom-runtime/loom-init/references/output-contract.md +208 -0
  579. package/payload/skills/loom-resume/.loom-runtime/loom-resume/SKILL.md +72 -0
  580. package/payload/skills/loom-resume/.loom-runtime/loom-resume/agents/openai.yaml +4 -0
  581. package/payload/skills/loom-resume/.loom-runtime/loom-resume/contract.json +47 -0
  582. package/payload/skills/loom-resume/.loom-runtime/loom-resume/references/input-signals.md +15 -0
  583. package/payload/skills/loom-resume/.loom-runtime/loom-resume/references/output-contract.md +58 -0
  584. package/payload/skills/loom-resume/.loom-runtime/loom-resume/scripts/loom-resume.py +14 -0
  585. package/payload/skills/loom-resume/.loom-runtime/registry.json +15 -0
  586. package/payload/skills/loom-resume/.loom-runtime/route-matrix.md +63 -0
  587. package/payload/skills/loom-resume/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  588. package/payload/skills/loom-resume/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
  589. package/payload/skills/loom-resume/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
  590. package/payload/skills/loom-resume/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
  591. package/payload/skills/loom-resume/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
  592. package/payload/skills/loom-resume/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  593. package/payload/skills/loom-resume/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
  594. package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
  595. package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/issue-model.md +127 -0
  596. package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
  597. package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/principles.md +180 -0
  598. package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/review-model.md +109 -0
  599. package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/state-machine.md +163 -0
  600. package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
  601. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
  602. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
  603. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
  604. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/execution-context.md +71 -0
  605. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
  606. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
  607. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
  608. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
  609. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
  610. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
  611. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
  612. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/review-execution.md +99 -0
  613. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
  614. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/status-surface.md +121 -0
  615. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
  616. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
  617. package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
  618. package/payload/skills/loom-resume/.loom-runtime/shared/references/templates/pull-request.md +44 -0
  619. package/payload/skills/loom-resume/.loom-runtime/shared/references/templates/review-record.md +35 -0
  620. package/payload/skills/loom-resume/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
  621. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
  622. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/governance_surface.py +879 -0
  623. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +4683 -0
  624. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
  625. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py +1966 -0
  626. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
  627. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/runtime_state.py +405 -0
  628. package/payload/skills/loom-resume/.loom-runtime/upgrade-contract.json +29 -0
  629. package/payload/skills/loom-resume/SKILL.md +72 -0
  630. package/payload/skills/loom-resume/agents/openai.yaml +4 -0
  631. package/payload/skills/loom-resume/contract.json +47 -0
  632. package/payload/skills/loom-resume/references/input-signals.md +15 -0
  633. package/payload/skills/loom-resume/references/loom-init/input-signals.md +16 -0
  634. package/payload/skills/loom-resume/references/loom-init/intake-signals.md +155 -0
  635. package/payload/skills/loom-resume/references/loom-init/output-contract.md +208 -0
  636. package/payload/skills/loom-resume/references/output-contract.md +58 -0
  637. package/payload/skills/loom-resume/references/route-matrix.md +63 -0
  638. package/payload/skills/loom-resume/references/shared/adoption/deep-existing-repo-default.md +64 -0
  639. package/payload/skills/loom-resume/references/shared/adoption/lightweight-retrofit-default.md +71 -0
  640. package/payload/skills/loom-resume/references/shared/adoption/routing-and-checkpoints.md +88 -0
  641. package/payload/skills/loom-resume/references/shared/governance/host-object-taxonomy.md +148 -0
  642. package/payload/skills/loom-resume/references/shared/governance/issue-model.md +127 -0
  643. package/payload/skills/loom-resume/references/shared/governance/maturity-and-closing.md +69 -0
  644. package/payload/skills/loom-resume/references/shared/governance/principles.md +180 -0
  645. package/payload/skills/loom-resume/references/shared/governance/review-model.md +109 -0
  646. package/payload/skills/loom-resume/references/shared/governance/state-machine.md +163 -0
  647. package/payload/skills/loom-resume/references/shared/governance/truth-and-sync-boundary.md +161 -0
  648. package/payload/skills/loom-resume/references/shared/harness/automation-frontload.md +139 -0
  649. package/payload/skills/loom-resume/references/shared/harness/closeout-gate.md +97 -0
  650. package/payload/skills/loom-resume/references/shared/harness/execution-chain.md +56 -0
  651. package/payload/skills/loom-resume/references/shared/harness/execution-context.md +71 -0
  652. package/payload/skills/loom-resume/references/shared/harness/fact-chain-contract.md +160 -0
  653. package/payload/skills/loom-resume/references/shared/harness/host-action-contract.md +128 -0
  654. package/payload/skills/loom-resume/references/shared/harness/host-issue-binding.md +90 -0
  655. package/payload/skills/loom-resume/references/shared/harness/host-lifecycle-boundary.md +56 -0
  656. package/payload/skills/loom-resume/references/shared/harness/merge-checkpoint.md +95 -0
  657. package/payload/skills/loom-resume/references/shared/harness/reconciliation-audit.md +64 -0
  658. package/payload/skills/loom-resume/references/shared/harness/recovery-model.md +101 -0
  659. package/payload/skills/loom-resume/references/shared/harness/review-execution.md +99 -0
  660. package/payload/skills/loom-resume/references/shared/harness/runtime-state.md +103 -0
  661. package/payload/skills/loom-resume/references/shared/harness/status-surface.md +121 -0
  662. package/payload/skills/loom-resume/references/shared/harness/work-item-contract.md +104 -0
  663. package/payload/skills/loom-resume/references/shared/harness/workspace-and-purity.md +73 -0
  664. package/payload/skills/loom-resume/references/shared/harness/workspace-model.md +56 -0
  665. package/payload/skills/loom-resume/references/shared/templates/pull-request.md +44 -0
  666. package/payload/skills/loom-resume/references/shared/templates/review-record.md +35 -0
  667. package/payload/skills/loom-resume/references/shared/templates/spec-suite.md +57 -0
  668. package/payload/skills/loom-resume/scripts/loom-resume.py +16 -0
  669. package/payload/skills/loom-retire/.loom-runtime/install-layout.json +77 -0
  670. package/payload/skills/loom-retire/.loom-runtime/loom-init/references/input-signals.md +16 -0
  671. package/payload/skills/loom-retire/.loom-runtime/loom-init/references/intake-signals.md +155 -0
  672. package/payload/skills/loom-retire/.loom-runtime/loom-init/references/output-contract.md +208 -0
  673. package/payload/skills/loom-retire/.loom-runtime/loom-retire/SKILL.md +26 -0
  674. package/payload/skills/loom-retire/.loom-runtime/loom-retire/agents/openai.yaml +4 -0
  675. package/payload/skills/loom-retire/.loom-runtime/loom-retire/contract.json +41 -0
  676. package/payload/skills/loom-retire/.loom-runtime/loom-retire/references/input-signals.md +7 -0
  677. package/payload/skills/loom-retire/.loom-runtime/loom-retire/references/output-contract.md +13 -0
  678. package/payload/skills/loom-retire/.loom-runtime/loom-retire/scripts/loom-retire.py +14 -0
  679. package/payload/skills/loom-retire/.loom-runtime/registry.json +15 -0
  680. package/payload/skills/loom-retire/.loom-runtime/route-matrix.md +63 -0
  681. package/payload/skills/loom-retire/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  682. package/payload/skills/loom-retire/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
  683. package/payload/skills/loom-retire/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
  684. package/payload/skills/loom-retire/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
  685. package/payload/skills/loom-retire/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
  686. package/payload/skills/loom-retire/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  687. package/payload/skills/loom-retire/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
  688. package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
  689. package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/issue-model.md +127 -0
  690. package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
  691. package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/principles.md +180 -0
  692. package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/review-model.md +109 -0
  693. package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/state-machine.md +163 -0
  694. package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
  695. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
  696. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
  697. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
  698. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/execution-context.md +71 -0
  699. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
  700. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
  701. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
  702. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
  703. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
  704. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
  705. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
  706. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/review-execution.md +99 -0
  707. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
  708. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/status-surface.md +121 -0
  709. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
  710. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
  711. package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
  712. package/payload/skills/loom-retire/.loom-runtime/shared/references/templates/pull-request.md +44 -0
  713. package/payload/skills/loom-retire/.loom-runtime/shared/references/templates/review-record.md +35 -0
  714. package/payload/skills/loom-retire/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
  715. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
  716. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/governance_surface.py +879 -0
  717. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +4683 -0
  718. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
  719. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py +1966 -0
  720. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
  721. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/runtime_state.py +405 -0
  722. package/payload/skills/loom-retire/.loom-runtime/upgrade-contract.json +29 -0
  723. package/payload/skills/loom-retire/SKILL.md +26 -0
  724. package/payload/skills/loom-retire/agents/openai.yaml +4 -0
  725. package/payload/skills/loom-retire/contract.json +41 -0
  726. package/payload/skills/loom-retire/references/input-signals.md +7 -0
  727. package/payload/skills/loom-retire/references/loom-init/input-signals.md +16 -0
  728. package/payload/skills/loom-retire/references/loom-init/intake-signals.md +155 -0
  729. package/payload/skills/loom-retire/references/loom-init/output-contract.md +208 -0
  730. package/payload/skills/loom-retire/references/output-contract.md +13 -0
  731. package/payload/skills/loom-retire/references/route-matrix.md +63 -0
  732. package/payload/skills/loom-retire/references/shared/adoption/deep-existing-repo-default.md +64 -0
  733. package/payload/skills/loom-retire/references/shared/adoption/lightweight-retrofit-default.md +71 -0
  734. package/payload/skills/loom-retire/references/shared/adoption/routing-and-checkpoints.md +88 -0
  735. package/payload/skills/loom-retire/references/shared/governance/host-object-taxonomy.md +148 -0
  736. package/payload/skills/loom-retire/references/shared/governance/issue-model.md +127 -0
  737. package/payload/skills/loom-retire/references/shared/governance/maturity-and-closing.md +69 -0
  738. package/payload/skills/loom-retire/references/shared/governance/principles.md +180 -0
  739. package/payload/skills/loom-retire/references/shared/governance/review-model.md +109 -0
  740. package/payload/skills/loom-retire/references/shared/governance/state-machine.md +163 -0
  741. package/payload/skills/loom-retire/references/shared/governance/truth-and-sync-boundary.md +161 -0
  742. package/payload/skills/loom-retire/references/shared/harness/automation-frontload.md +139 -0
  743. package/payload/skills/loom-retire/references/shared/harness/closeout-gate.md +97 -0
  744. package/payload/skills/loom-retire/references/shared/harness/execution-chain.md +56 -0
  745. package/payload/skills/loom-retire/references/shared/harness/execution-context.md +71 -0
  746. package/payload/skills/loom-retire/references/shared/harness/fact-chain-contract.md +160 -0
  747. package/payload/skills/loom-retire/references/shared/harness/host-action-contract.md +128 -0
  748. package/payload/skills/loom-retire/references/shared/harness/host-issue-binding.md +90 -0
  749. package/payload/skills/loom-retire/references/shared/harness/host-lifecycle-boundary.md +56 -0
  750. package/payload/skills/loom-retire/references/shared/harness/merge-checkpoint.md +95 -0
  751. package/payload/skills/loom-retire/references/shared/harness/reconciliation-audit.md +64 -0
  752. package/payload/skills/loom-retire/references/shared/harness/recovery-model.md +101 -0
  753. package/payload/skills/loom-retire/references/shared/harness/review-execution.md +99 -0
  754. package/payload/skills/loom-retire/references/shared/harness/runtime-state.md +103 -0
  755. package/payload/skills/loom-retire/references/shared/harness/status-surface.md +121 -0
  756. package/payload/skills/loom-retire/references/shared/harness/work-item-contract.md +104 -0
  757. package/payload/skills/loom-retire/references/shared/harness/workspace-and-purity.md +73 -0
  758. package/payload/skills/loom-retire/references/shared/harness/workspace-model.md +56 -0
  759. package/payload/skills/loom-retire/references/shared/templates/pull-request.md +44 -0
  760. package/payload/skills/loom-retire/references/shared/templates/review-record.md +35 -0
  761. package/payload/skills/loom-retire/references/shared/templates/spec-suite.md +57 -0
  762. package/payload/skills/loom-retire/scripts/loom-retire.py +16 -0
  763. package/payload/skills/loom-review/.loom-runtime/install-layout.json +77 -0
  764. package/payload/skills/loom-review/.loom-runtime/loom-init/references/input-signals.md +16 -0
  765. package/payload/skills/loom-review/.loom-runtime/loom-init/references/intake-signals.md +155 -0
  766. package/payload/skills/loom-review/.loom-runtime/loom-init/references/output-contract.md +208 -0
  767. package/payload/skills/loom-review/.loom-runtime/loom-review/SKILL.md +95 -0
  768. package/payload/skills/loom-review/.loom-runtime/loom-review/agents/openai.yaml +4 -0
  769. package/payload/skills/loom-review/.loom-runtime/loom-review/contract.json +49 -0
  770. package/payload/skills/loom-review/.loom-runtime/loom-review/references/input-signals.md +15 -0
  771. package/payload/skills/loom-review/.loom-runtime/loom-review/references/output-contract.md +57 -0
  772. package/payload/skills/loom-review/.loom-runtime/loom-review/scripts/loom-review.py +14 -0
  773. package/payload/skills/loom-review/.loom-runtime/registry.json +15 -0
  774. package/payload/skills/loom-review/.loom-runtime/route-matrix.md +63 -0
  775. package/payload/skills/loom-review/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
  776. package/payload/skills/loom-review/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
  777. package/payload/skills/loom-review/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
  778. package/payload/skills/loom-review/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
  779. package/payload/skills/loom-review/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
  780. package/payload/skills/loom-review/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
  781. package/payload/skills/loom-review/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
  782. package/payload/skills/loom-review/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
  783. package/payload/skills/loom-review/.loom-runtime/shared/references/governance/issue-model.md +127 -0
  784. package/payload/skills/loom-review/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
  785. package/payload/skills/loom-review/.loom-runtime/shared/references/governance/principles.md +180 -0
  786. package/payload/skills/loom-review/.loom-runtime/shared/references/governance/review-model.md +109 -0
  787. package/payload/skills/loom-review/.loom-runtime/shared/references/governance/state-machine.md +163 -0
  788. package/payload/skills/loom-review/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
  789. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
  790. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
  791. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
  792. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/execution-context.md +71 -0
  793. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
  794. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
  795. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
  796. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
  797. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
  798. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
  799. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
  800. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/review-execution.md +99 -0
  801. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
  802. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/status-surface.md +121 -0
  803. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
  804. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
  805. package/payload/skills/loom-review/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
  806. package/payload/skills/loom-review/.loom-runtime/shared/references/templates/pull-request.md +44 -0
  807. package/payload/skills/loom-review/.loom-runtime/shared/references/templates/review-record.md +35 -0
  808. package/payload/skills/loom-review/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
  809. package/payload/skills/loom-review/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
  810. package/payload/skills/loom-review/.loom-runtime/shared/scripts/governance_surface.py +879 -0
  811. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +4683 -0
  812. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
  813. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_init.py +1966 -0
  814. package/payload/skills/loom-review/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
  815. package/payload/skills/loom-review/.loom-runtime/shared/scripts/runtime_state.py +405 -0
  816. package/payload/skills/loom-review/.loom-runtime/upgrade-contract.json +29 -0
  817. package/payload/skills/loom-review/SKILL.md +95 -0
  818. package/payload/skills/loom-review/agents/openai.yaml +4 -0
  819. package/payload/skills/loom-review/contract.json +49 -0
  820. package/payload/skills/loom-review/references/input-signals.md +15 -0
  821. package/payload/skills/loom-review/references/loom-init/input-signals.md +16 -0
  822. package/payload/skills/loom-review/references/loom-init/intake-signals.md +155 -0
  823. package/payload/skills/loom-review/references/loom-init/output-contract.md +208 -0
  824. package/payload/skills/loom-review/references/output-contract.md +57 -0
  825. package/payload/skills/loom-review/references/route-matrix.md +63 -0
  826. package/payload/skills/loom-review/references/shared/adoption/deep-existing-repo-default.md +64 -0
  827. package/payload/skills/loom-review/references/shared/adoption/lightweight-retrofit-default.md +71 -0
  828. package/payload/skills/loom-review/references/shared/adoption/routing-and-checkpoints.md +88 -0
  829. package/payload/skills/loom-review/references/shared/governance/host-object-taxonomy.md +148 -0
  830. package/payload/skills/loom-review/references/shared/governance/issue-model.md +127 -0
  831. package/payload/skills/loom-review/references/shared/governance/maturity-and-closing.md +69 -0
  832. package/payload/skills/loom-review/references/shared/governance/principles.md +180 -0
  833. package/payload/skills/loom-review/references/shared/governance/review-model.md +109 -0
  834. package/payload/skills/loom-review/references/shared/governance/state-machine.md +163 -0
  835. package/payload/skills/loom-review/references/shared/governance/truth-and-sync-boundary.md +161 -0
  836. package/payload/skills/loom-review/references/shared/harness/automation-frontload.md +139 -0
  837. package/payload/skills/loom-review/references/shared/harness/closeout-gate.md +97 -0
  838. package/payload/skills/loom-review/references/shared/harness/execution-chain.md +56 -0
  839. package/payload/skills/loom-review/references/shared/harness/execution-context.md +71 -0
  840. package/payload/skills/loom-review/references/shared/harness/fact-chain-contract.md +160 -0
  841. package/payload/skills/loom-review/references/shared/harness/host-action-contract.md +128 -0
  842. package/payload/skills/loom-review/references/shared/harness/host-issue-binding.md +90 -0
  843. package/payload/skills/loom-review/references/shared/harness/host-lifecycle-boundary.md +56 -0
  844. package/payload/skills/loom-review/references/shared/harness/merge-checkpoint.md +95 -0
  845. package/payload/skills/loom-review/references/shared/harness/reconciliation-audit.md +64 -0
  846. package/payload/skills/loom-review/references/shared/harness/recovery-model.md +101 -0
  847. package/payload/skills/loom-review/references/shared/harness/review-execution.md +99 -0
  848. package/payload/skills/loom-review/references/shared/harness/runtime-state.md +103 -0
  849. package/payload/skills/loom-review/references/shared/harness/status-surface.md +121 -0
  850. package/payload/skills/loom-review/references/shared/harness/work-item-contract.md +104 -0
  851. package/payload/skills/loom-review/references/shared/harness/workspace-and-purity.md +73 -0
  852. package/payload/skills/loom-review/references/shared/harness/workspace-model.md +56 -0
  853. package/payload/skills/loom-review/references/shared/templates/pull-request.md +44 -0
  854. package/payload/skills/loom-review/references/shared/templates/review-record.md +35 -0
  855. package/payload/skills/loom-review/references/shared/templates/spec-suite.md +57 -0
  856. package/payload/skills/loom-review/scripts/loom-review.py +16 -0
@@ -0,0 +1,4683 @@
1
+ #!/usr/bin/env python3
2
+ """Minimal Loom repository mechanical self-check."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import unicodedata
12
+ import json
13
+ import os
14
+ from collections import Counter, defaultdict
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ from fact_chain_support import inspect_fact_chain
19
+ from governance_surface import build_governance_surface
20
+ from loom_flow import repo_specific_requirements_payload
21
+ from runtime_paths import repo_local_root
22
+
23
+ TOP_LEVEL_DIRS = (
24
+ "adoption",
25
+ "governance",
26
+ "harness",
27
+ "skills",
28
+ "templates",
29
+ )
30
+
31
+ TOP_LEVEL_FILES = (
32
+ "AGENTS.md",
33
+ "LICENSE",
34
+ "Makefile",
35
+ "README.md",
36
+ "VISION.md",
37
+ "governance-design.md",
38
+ "harness-design.md",
39
+ "system-design.md",
40
+ )
41
+
42
+ AREA_READMES = (
43
+ "adoption/README.md",
44
+ "governance/README.md",
45
+ "harness/README.md",
46
+ "skills/README.md",
47
+ "templates/README.md",
48
+ )
49
+
50
+ CORE_DOCS = (
51
+ ".github/PULL_REQUEST_TEMPLATE.md",
52
+ ".github/workflows/loom-check.yml",
53
+ "governance/principles.md",
54
+ "governance/review-model.md",
55
+ "governance/maturity-and-closing.md",
56
+ "governance/state-machine.md",
57
+ "governance/truth-and-sync-boundary.md",
58
+ "governance/host-object-taxonomy.md",
59
+ "harness/work-item-contract.md",
60
+ "harness/fact-chain-contract.md",
61
+ "harness/execution-context.md",
62
+ "harness/execution-chain.md",
63
+ "harness/checkpoint-model.md",
64
+ "harness/workspace-model.md",
65
+ "harness/workspace-lifecycle.md",
66
+ "harness/host-action-contract.md",
67
+ "harness/host-lifecycle-boundary.md",
68
+ "harness/reconciliation-audit.md",
69
+ "harness/recovery-model.md",
70
+ "harness/review-execution.md",
71
+ "harness/status-surface.md",
72
+ "harness/automation-frontload.md",
73
+ "harness/merge-checkpoint.md",
74
+ "harness/closeout-gate.md",
75
+ "harness/workspace-and-purity.md",
76
+ "templates/spec-suite.md",
77
+ "templates/pull-request.md",
78
+ "adoption/extraction-ledger.md",
79
+ "adoption/landing-map.md",
80
+ "adoption/rationale.md",
81
+ "adoption/routing-and-checkpoints.md",
82
+ "adoption/lightweight-retrofit-default.md",
83
+ "adoption/repo-companion-contract.md",
84
+ "adoption/repo-interop-contract.md",
85
+ "skills/distribution-and-adapter-contract.md",
86
+ "skills/registry.json",
87
+ "skills/install-layout.json",
88
+ "skills/upgrade-contract.json",
89
+ "skills/route-matrix.md",
90
+ "skills/loom-init/SKILL.md",
91
+ "skills/loom-init/contract.json",
92
+ "skills/loom-init/references/input-signals.md",
93
+ "skills/loom-init/references/intake-signals.md",
94
+ "skills/loom-init/references/output-contract.md",
95
+ "skills/loom-review/SKILL.md",
96
+ "skills/loom-review/contract.json",
97
+ "skills/loom-review/references/input-signals.md",
98
+ "skills/loom-review/references/output-contract.md",
99
+ "templates/review-record.md",
100
+ "templates/scaffold/spec.md",
101
+ "templates/scaffold/plan.md",
102
+ "tools/loom_init.py",
103
+ "tools/loom_flow.py",
104
+ )
105
+
106
+ AUTOMATION_FRONTLOAD_TEMPLATES = (
107
+ "templates/spec-suite.md",
108
+ "templates/pull-request.md",
109
+ )
110
+
111
+ AUTOMATION_FRONTLOAD_SKILLS = (
112
+ "skills/README.md",
113
+ "skills/distribution-and-adapter-contract.md",
114
+ "skills/install-layout.json",
115
+ "skills/route-matrix.md",
116
+ "skills/loom-init/SKILL.md",
117
+ "skills/loom-init/references/input-signals.md",
118
+ "skills/loom-init/references/intake-signals.md",
119
+ "skills/loom-init/references/output-contract.md",
120
+ )
121
+
122
+ AUTOMATION_FRONTLOAD_EXECUTION_SUPPORT = (
123
+ "harness/work-item-contract.md",
124
+ "harness/execution-context.md",
125
+ "harness/execution-chain.md",
126
+ "harness/checkpoint-model.md",
127
+ "harness/workspace-model.md",
128
+ "harness/workspace-lifecycle.md",
129
+ "harness/recovery-model.md",
130
+ "harness/status-surface.md",
131
+ "harness/automation-frontload.md",
132
+ "harness/merge-checkpoint.md",
133
+ "harness/workspace-and-purity.md",
134
+ )
135
+
136
+ DEMO_ASSETS = (
137
+ "examples/new-project/.gitkeep",
138
+ "examples/new-project/AGENTS.md",
139
+ "examples/new-project/.github/PULL_REQUEST_TEMPLATE.md",
140
+ "examples/new-project/.loom/bootstrap/init-result.json",
141
+ "examples/new-project/.loom/bootstrap/manifest.json",
142
+ "examples/new-project/.loom/work-items/INIT-0001.md",
143
+ "examples/new-project/.loom/progress/INIT-0001.md",
144
+ "examples/new-project/.loom/reviews/INIT-0001.json",
145
+ "examples/new-project/.loom/status/current.md",
146
+ "examples/new-project/.loom/bin/loom_init.py",
147
+ "examples/new-project/.loom/bin/fact_chain_support.py",
148
+ "examples/new-project/.loom/bin/runtime_paths.py",
149
+ "examples/new-project/.loom/bin/runtime_state.py",
150
+ "examples/new-project/.loom/bin/loom_flow.py",
151
+ "examples/new-project/.loom/bin/loom_check.py",
152
+ "examples/new-project/.loom/specs/INIT-0001/spec.md",
153
+ "examples/new-project/.loom/specs/INIT-0001/plan.md",
154
+ )
155
+
156
+ LINK_RE = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
157
+ HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)(?:\s+#+\s*)?$")
158
+ CODE_FENCE_RE = re.compile(r"^(```|~~~)")
159
+ EXTERNAL_SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*:")
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class Failure:
164
+ category: str
165
+ detail: str
166
+
167
+
168
+ GOVERNANCE_SURFACE_ROUTE_SKILLS = {
169
+ "loom-adopt",
170
+ "loom-resume",
171
+ }
172
+
173
+ GOVERNANCE_SURFACE_CONTRACT_SKILLS = {
174
+ "loom-adopt",
175
+ "loom-resume",
176
+ }
177
+
178
+ REVIEW_FINDING_SEVERITIES = {"warn", "block"}
179
+ REVIEW_FINDING_DISPOSITION_STATUSES = {"accepted", "rejected", "deferred"}
180
+ REPO_INTERFACE_AVAILABILITY = {"absent", "companion_docs_only", "incomplete", "present"}
181
+ REPO_INTERFACE_ENFORCEMENT = {"blocking", "advisory"}
182
+ REPO_INTEROP_AVAILABILITY = {"absent", "incomplete", "present"}
183
+
184
+
185
+ def repo_root_from_argv(argv: list[str]) -> Path:
186
+ if len(argv) > 2:
187
+ raise SystemExit("usage: loom_check.py [repo-root]")
188
+ if len(argv) == 2:
189
+ return Path(argv[1]).expanduser().resolve()
190
+ hinted_root = repo_local_root(__file__)
191
+ if hinted_root is not None:
192
+ return hinted_root
193
+ current = Path.cwd().resolve()
194
+ if (current / "skills").exists() and (current / "README.md").exists():
195
+ return current
196
+ return Path(__file__).resolve().parent.parent
197
+
198
+
199
+ def check_required_paths(root: Path, category: str, paths: tuple[str, ...]) -> list[Failure]:
200
+ failures: list[Failure] = []
201
+ for relative_path in paths:
202
+ if not (root / relative_path).exists():
203
+ failures.append(Failure(category, f"missing `{relative_path}`"))
204
+ return failures
205
+
206
+
207
+ def iter_markdown_files(root: Path) -> list[Path]:
208
+ return sorted(path for path in root.rglob("*.md") if path.is_file())
209
+
210
+
211
+ def split_link_target(raw_target: str) -> tuple[str, str]:
212
+ target = raw_target.strip()
213
+ if not target:
214
+ return "", ""
215
+ if target.startswith("<") and target.endswith(">"):
216
+ target = target[1:-1].strip()
217
+ if " " in target:
218
+ target = target.split(" ", 1)[0]
219
+ if "#" in target:
220
+ path_part, fragment = target.split("#", 1)
221
+ return path_part, fragment
222
+ return target, ""
223
+
224
+
225
+ def markdown_links(path: Path) -> list[tuple[int, str]]:
226
+ results: list[tuple[int, str]] = []
227
+ in_code_fence = False
228
+ for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
229
+ if CODE_FENCE_RE.match(line.strip()):
230
+ in_code_fence = not in_code_fence
231
+ continue
232
+ if in_code_fence:
233
+ continue
234
+ for match in LINK_RE.finditer(line):
235
+ results.append((line_no, match.group(1)))
236
+ return results
237
+
238
+
239
+ def strip_inline_markdown(text: str) -> str:
240
+ text = re.sub(r"`([^`]*)`", r"\1", text)
241
+ text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", text)
242
+ text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
243
+ text = re.sub(r"[*_~]", "", text)
244
+ return text
245
+
246
+
247
+ def github_anchor_map(path: Path, cache: dict[Path, set[str]]) -> set[str]:
248
+ cached = cache.get(path)
249
+ if cached is not None:
250
+ return cached
251
+
252
+ anchors: set[str] = set()
253
+ duplicates: Counter[str] = Counter()
254
+ in_code_fence = False
255
+ for line in path.read_text(encoding="utf-8").splitlines():
256
+ stripped = line.strip()
257
+ if CODE_FENCE_RE.match(stripped):
258
+ in_code_fence = not in_code_fence
259
+ continue
260
+ if in_code_fence:
261
+ continue
262
+ match = HEADING_RE.match(line)
263
+ if not match:
264
+ continue
265
+ base = github_slug(strip_inline_markdown(match.group(2)))
266
+ if not base:
267
+ continue
268
+ duplicates[base] += 1
269
+ anchor = base if duplicates[base] == 1 else f"{base}-{duplicates[base] - 1}"
270
+ anchors.add(anchor)
271
+
272
+ cache[path] = anchors
273
+ return anchors
274
+
275
+
276
+ def github_slug(text: str) -> str:
277
+ text = unicodedata.normalize("NFKD", text).lower().strip()
278
+ slug_chars: list[str] = []
279
+ last_was_dash = False
280
+ for char in text:
281
+ if char.isspace() or char == "-":
282
+ if slug_chars and not last_was_dash:
283
+ slug_chars.append("-")
284
+ last_was_dash = True
285
+ continue
286
+
287
+ category = unicodedata.category(char)
288
+ if category[0] in {"L", "N"} or category == "Mn":
289
+ slug_chars.append(char)
290
+ last_was_dash = False
291
+
292
+ return "".join(slug_chars).strip("-")
293
+
294
+
295
+ def resolve_link_target(root: Path, source_path: Path, raw_target: str) -> tuple[Path | None, str]:
296
+ target, fragment = split_link_target(raw_target)
297
+ if not target:
298
+ return source_path, fragment
299
+ if EXTERNAL_SCHEME_RE.match(target) or target.startswith("//"):
300
+ return None, ""
301
+ if target.startswith("/"):
302
+ return None, ""
303
+
304
+ resolved = (source_path.parent / target).resolve()
305
+ if resolved.exists():
306
+ return resolved, fragment
307
+ if resolved.is_dir():
308
+ readme = resolved / "README.md"
309
+ if readme.exists():
310
+ return readme, fragment
311
+ try:
312
+ resolved.relative_to(root)
313
+ except ValueError:
314
+ return resolved, fragment
315
+ return resolved, fragment
316
+
317
+
318
+ def check_markdown_links(root: Path) -> list[Failure]:
319
+ failures: list[Failure] = []
320
+ anchor_cache: dict[Path, set[str]] = {}
321
+ for markdown_path in iter_markdown_files(root):
322
+ for line_no, raw_target in markdown_links(markdown_path):
323
+ resolved, fragment = resolve_link_target(root, markdown_path, raw_target)
324
+ if resolved is None:
325
+ continue
326
+ if not resolved.exists():
327
+ detail = (
328
+ f"`{markdown_path.relative_to(root)}:{line_no}` -> `{raw_target}` "
329
+ f"(missing `{resolved.relative_to(root) if resolved.is_absolute() and is_within(resolved, root) else resolved}`)"
330
+ )
331
+ failures.append(Failure("markdown-links", detail))
332
+ continue
333
+ if fragment and resolved.suffix.lower() == ".md":
334
+ anchors = github_anchor_map(resolved, anchor_cache)
335
+ if fragment not in anchors:
336
+ detail = (
337
+ f"`{markdown_path.relative_to(root)}:{line_no}` -> `{raw_target}` "
338
+ f"(missing anchor `#{fragment}` in `{resolved.relative_to(root)}`)"
339
+ )
340
+ failures.append(Failure("markdown-links", detail))
341
+ return failures
342
+
343
+
344
+ def load_json_file(path: Path) -> object:
345
+ with path.open(encoding="utf-8") as handle:
346
+ return json.load(handle)
347
+
348
+
349
+ def run_command(
350
+ root: Path,
351
+ args: list[str],
352
+ cwd: Path | None = None,
353
+ env: dict[str, str] | None = None,
354
+ timeout_seconds: float | None = None,
355
+ ) -> subprocess.CompletedProcess[str]:
356
+ command_env = os.environ.copy()
357
+ for key in ("LOOM_SOURCE_REPO_ROOT", "LOOM_INSTALLED_SKILLS_ROOT", "LOOM_RUNTIME_SCENE"):
358
+ command_env.pop(key, None)
359
+ if env:
360
+ command_env.update(env)
361
+ return subprocess.run(
362
+ args,
363
+ cwd=cwd or root,
364
+ check=False,
365
+ capture_output=True,
366
+ text=True,
367
+ env=command_env,
368
+ timeout=timeout_seconds,
369
+ )
370
+
371
+
372
+ def load_command_json(
373
+ root: Path,
374
+ args: list[str],
375
+ *,
376
+ cwd: Path | None = None,
377
+ env: dict[str, str] | None = None,
378
+ timeout_seconds: float | None = None,
379
+ ) -> tuple[dict[str, object] | None, str | None]:
380
+ try:
381
+ result = run_command(root, args, cwd=cwd, env=env, timeout_seconds=timeout_seconds)
382
+ except subprocess.TimeoutExpired:
383
+ return None, f"command timed out after {int(timeout_seconds or 0)}s"
384
+ if not result.stdout.strip():
385
+ detail = "command produced no JSON output"
386
+ if result.stderr.strip():
387
+ detail += f": {result.stderr.strip()}"
388
+ return None, detail
389
+ try:
390
+ payload = json.loads(result.stdout)
391
+ except json.JSONDecodeError as exc:
392
+ return None, f"invalid JSON output: {exc.msg}"
393
+ if not isinstance(payload, dict):
394
+ return None, "command output must be a JSON object"
395
+ return payload, None
396
+
397
+
398
+ def load_command_json_with_retry(
399
+ root: Path,
400
+ args: list[str],
401
+ *,
402
+ cwd: Path | None = None,
403
+ env: dict[str, str] | None = None,
404
+ timeout_seconds: float | None = None,
405
+ retries: int = 2,
406
+ ) -> tuple[dict[str, object] | None, str | None]:
407
+ transient_needles = (
408
+ "EOF",
409
+ "unknown owner type",
410
+ "command timed out",
411
+ "connection reset",
412
+ "TLS handshake timeout",
413
+ )
414
+ last_payload: dict[str, object] | None = None
415
+ last_error: str | None = None
416
+ for _ in range(retries):
417
+ payload, error = load_command_json(
418
+ root,
419
+ args,
420
+ cwd=cwd,
421
+ env=env,
422
+ timeout_seconds=timeout_seconds,
423
+ )
424
+ if error is None:
425
+ return payload, None
426
+ last_payload = payload
427
+ last_error = error
428
+ if not any(needle in error for needle in transient_needles):
429
+ return payload, error
430
+ return last_payload, last_error
431
+
432
+
433
+ def payload_has_github_rate_limit(payload: object) -> bool:
434
+ if not isinstance(payload, dict):
435
+ return False
436
+ missing_inputs = payload.get("missing_inputs")
437
+ if not isinstance(missing_inputs, list):
438
+ return False
439
+ return any(isinstance(item, str) and "API rate limit exceeded" in item for item in missing_inputs)
440
+
441
+
442
+ def prepend_path_env(bin_dir: Path, extra: dict[str, str] | None = None) -> dict[str, str]:
443
+ env = dict(extra or {})
444
+ current_path = os.environ.get("PATH", "")
445
+ env["PATH"] = str(bin_dir) if not current_path else f"{bin_dir}:{current_path}"
446
+ return env
447
+
448
+
449
+ def write_fake_codex(
450
+ path: Path,
451
+ *,
452
+ mode: str,
453
+ tracked_edit_target: str | None = None,
454
+ ) -> None:
455
+ if mode == "success":
456
+ body = """#!/usr/bin/env python3
457
+ import json
458
+ import pathlib
459
+ import sys
460
+
461
+ args = sys.argv[1:]
462
+ output_path = pathlib.Path(args[args.index("-o") + 1])
463
+ payload = {
464
+ "decision": "allow",
465
+ "summary": "Default Codex reviewer found the item ready for merge checkpoint consumption.",
466
+ "findings": [
467
+ {
468
+ "id": "warn-1",
469
+ "summary": "Keep the follow-up validation note visible in the review record.",
470
+ "severity": "warn",
471
+ "rebuttal": None,
472
+ "disposition": {
473
+ "status": "accepted",
474
+ "summary": "The reviewer accepts the current validation coverage."
475
+ },
476
+ "details": "This finding is advisory and should not block merge-ready."
477
+ }
478
+ ]
479
+ }
480
+ output_path.parent.mkdir(parents=True, exist_ok=True)
481
+ output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\\n", encoding="utf-8")
482
+ sys.exit(0)
483
+ """
484
+ elif mode == "schema_drift":
485
+ body = """#!/usr/bin/env python3
486
+ import json
487
+ import pathlib
488
+ import sys
489
+
490
+ args = sys.argv[1:]
491
+ output_path = pathlib.Path(args[args.index("-o") + 1])
492
+ payload = {
493
+ "decision": "allow",
494
+ "findings": []
495
+ }
496
+ output_path.parent.mkdir(parents=True, exist_ok=True)
497
+ output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\\n", encoding="utf-8")
498
+ sys.exit(0)
499
+ """
500
+ elif mode == "tracked_edit":
501
+ target = tracked_edit_target or ""
502
+ body = f"""#!/usr/bin/env python3
503
+ import json
504
+ import pathlib
505
+ import sys
506
+
507
+ args = sys.argv[1:]
508
+ cwd = pathlib.Path(args[args.index("-C") + 1])
509
+ output_path = pathlib.Path(args[args.index("-o") + 1])
510
+ target = cwd / {target!r}
511
+ target.write_text(target.read_text(encoding="utf-8") + "\\ntracked edit from fake codex\\n", encoding="utf-8")
512
+ payload = {{
513
+ "decision": "block",
514
+ "summary": "Tracked repository content was modified during review.",
515
+ "findings": [
516
+ {{
517
+ "id": "block-1",
518
+ "summary": "Tracked repo content changed during review execution.",
519
+ "severity": "block",
520
+ "rebuttal": None,
521
+ "disposition": {{
522
+ "status": "rejected",
523
+ "summary": "The run must fail closed."
524
+ }}
525
+ }}
526
+ ]
527
+ }}
528
+ output_path.parent.mkdir(parents=True, exist_ok=True)
529
+ output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\\n", encoding="utf-8")
530
+ sys.exit(0)
531
+ """
532
+ else:
533
+ raise ValueError(f"unknown fake codex mode: {mode}")
534
+ path.write_text(body, encoding="utf-8")
535
+ path.chmod(0o755)
536
+
537
+
538
+ def require_governance_surface(
539
+ failures: list[Failure],
540
+ *,
541
+ category: str,
542
+ context: str,
543
+ payload: dict[str, object],
544
+ ) -> None:
545
+ governance_surface = payload.get("governance_surface")
546
+ if not isinstance(governance_surface, dict):
547
+ failures.append(Failure(category, f"{context} must include `governance_surface` as an object"))
548
+ return
549
+
550
+ required_keys = (
551
+ "repository_mode",
552
+ "loom_state",
553
+ "carrier_summary",
554
+ "execution_entry",
555
+ "validation_entry",
556
+ "review_merge_surface",
557
+ "github_control_plane",
558
+ "repo_interface",
559
+ "repo_interop",
560
+ "summary",
561
+ "missing_inputs",
562
+ )
563
+ for key in required_keys:
564
+ if key not in governance_surface:
565
+ failures.append(Failure(category, f"{context} governance_surface must include `{key}`"))
566
+
567
+ for key in ("repository_mode", "loom_state", "execution_entry", "validation_entry", "summary"):
568
+ if key in governance_surface and (not isinstance(governance_surface.get(key), str) or not governance_surface.get(key)):
569
+ failures.append(Failure(category, f"{context} governance_surface `{key}` must be a non-empty string"))
570
+ if governance_surface.get("repository_mode") not in {"new", "small-existing", "complex-existing"}:
571
+ failures.append(Failure(category, f"{context} governance_surface `repository_mode` must stay within the stable contract"))
572
+ if governance_surface.get("loom_state") not in {"active", "partial", "absent"}:
573
+ failures.append(Failure(category, f"{context} governance_surface `loom_state` must stay within the stable contract"))
574
+
575
+ missing_inputs = governance_surface.get("missing_inputs")
576
+ if missing_inputs is not None and not isinstance(missing_inputs, list):
577
+ failures.append(Failure(category, f"{context} governance_surface `missing_inputs` must be a list"))
578
+
579
+ carrier_summary = governance_surface.get("carrier_summary")
580
+ if not isinstance(carrier_summary, dict):
581
+ failures.append(Failure(category, f"{context} governance_surface must include `carrier_summary`"))
582
+ else:
583
+ required_carriers = ("work_item", "recovery", "review", "status_surface", "spec_path", "plan_path")
584
+ if set(carrier_summary.keys()) != set(required_carriers):
585
+ failures.append(Failure(category, f"{context} governance_surface carrier keys must stay within the stable contract"))
586
+ for carrier in required_carriers:
587
+ entry = carrier_summary.get(carrier)
588
+ if not isinstance(entry, dict):
589
+ failures.append(Failure(category, f"{context} governance_surface carrier `{carrier}` must be an object"))
590
+ continue
591
+ if entry.get("status") not in {"present", "missing", "planned"}:
592
+ failures.append(
593
+ Failure(category, f"{context} governance_surface carrier `{carrier}` status must stay within the stable contract")
594
+ )
595
+ for field in ("locator", "source"):
596
+ value = entry.get(field)
597
+ if not isinstance(value, str) or not value:
598
+ failures.append(
599
+ Failure(category, f"{context} governance_surface carrier `{carrier}` must include non-empty `{field}`")
600
+ )
601
+
602
+ review_merge_surface = governance_surface.get("review_merge_surface")
603
+ if review_merge_surface is not None and not isinstance(review_merge_surface, dict):
604
+ failures.append(Failure(category, f"{context} governance_surface `review_merge_surface` must be an object"))
605
+ elif isinstance(review_merge_surface, dict):
606
+ for key in ("pr_template", "validation_surface", "merge_surface"):
607
+ value = review_merge_surface.get(key)
608
+ if not isinstance(value, str) or not value:
609
+ failures.append(Failure(category, f"{context} governance_surface `review_merge_surface.{key}` must be a non-empty string"))
610
+
611
+ github_control_plane = governance_surface.get("github_control_plane")
612
+ if github_control_plane is not None and not isinstance(github_control_plane, dict):
613
+ failures.append(Failure(category, f"{context} governance_surface `github_control_plane` must be an object"))
614
+ elif isinstance(github_control_plane, dict):
615
+ for key in ("repository", "default_branch", "branch_protection", "required_checks", "pr_reviews"):
616
+ if key not in github_control_plane:
617
+ failures.append(Failure(category, f"{context} governance_surface `github_control_plane.{key}` must exist"))
618
+ for key in ("repository", "default_branch"):
619
+ value = github_control_plane.get(key)
620
+ if not isinstance(value, str) or not value:
621
+ failures.append(Failure(category, f"{context} governance_surface `github_control_plane.{key}` must be a non-empty string"))
622
+ if github_control_plane.get("branch_protection") not in {"enabled", "disabled", "unknown"}:
623
+ failures.append(Failure(category, f"{context} governance_surface `github_control_plane.branch_protection` must stay within the stable contract"))
624
+ if github_control_plane.get("pr_reviews") not in {"required", "not_required", "unknown"}:
625
+ failures.append(Failure(category, f"{context} governance_surface `github_control_plane.pr_reviews` must stay within the stable contract"))
626
+ required_checks = github_control_plane.get("required_checks")
627
+ if not (
628
+ required_checks == "unknown"
629
+ or (isinstance(required_checks, list) and all(isinstance(item, str) and item for item in required_checks))
630
+ ):
631
+ failures.append(Failure(category, f"{context} governance_surface `github_control_plane.required_checks` must be `unknown` or a string list"))
632
+
633
+ require_repo_interface_payload(
634
+ failures,
635
+ category=category,
636
+ context=f"{context} governance_surface.repo_interface",
637
+ payload=governance_surface.get("repo_interface"),
638
+ )
639
+ require_repo_interop_payload(
640
+ failures,
641
+ category=category,
642
+ context=f"{context} governance_surface.repo_interop",
643
+ payload=governance_surface.get("repo_interop"),
644
+ )
645
+
646
+
647
+ def require_locator_entry(
648
+ failures: list[Failure],
649
+ *,
650
+ category: str,
651
+ context: str,
652
+ payload: object,
653
+ allowed_statuses: set[str],
654
+ ) -> None:
655
+ if not isinstance(payload, dict):
656
+ failures.append(Failure(category, f"{context} must be an object"))
657
+ return
658
+ if payload.get("status") not in allowed_statuses:
659
+ failures.append(Failure(category, f"{context} status must stay within the stable contract"))
660
+ for field in ("locator", "source"):
661
+ value = payload.get(field)
662
+ if not isinstance(value, str) or not value:
663
+ failures.append(Failure(category, f"{context} must include non-empty `{field}`"))
664
+
665
+
666
+ def require_repo_interface_payload(
667
+ failures: list[Failure],
668
+ *,
669
+ category: str,
670
+ context: str,
671
+ payload: object,
672
+ ) -> None:
673
+ if not isinstance(payload, dict):
674
+ failures.append(Failure(category, f"{context} must be an object"))
675
+ return
676
+ if payload.get("availability") not in REPO_INTERFACE_AVAILABILITY:
677
+ failures.append(Failure(category, f"{context} availability must stay within the stable contract"))
678
+ require_locator_entry(
679
+ failures,
680
+ category=category,
681
+ context=f"{context}.manifest",
682
+ payload=payload.get("manifest"),
683
+ allowed_statuses={"present", "missing"},
684
+ )
685
+ for key in ("companion_entry", "repo_specific_requirements", "specialized_gates"):
686
+ require_locator_entry(
687
+ failures,
688
+ category=category,
689
+ context=f"{context}.{key}",
690
+ payload=payload.get(key),
691
+ allowed_statuses={"present", "missing"},
692
+ )
693
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
694
+ failures.append(Failure(category, f"{context} must include non-empty `summary`"))
695
+ if not isinstance(payload.get("missing_inputs"), list):
696
+ failures.append(Failure(category, f"{context} must include `missing_inputs` as a list"))
697
+
698
+
699
+ def require_repo_interop_payload(
700
+ failures: list[Failure],
701
+ *,
702
+ category: str,
703
+ context: str,
704
+ payload: object,
705
+ ) -> None:
706
+ if not isinstance(payload, dict):
707
+ failures.append(Failure(category, f"{context} must be an object"))
708
+ return
709
+ if payload.get("availability") not in REPO_INTEROP_AVAILABILITY:
710
+ failures.append(Failure(category, f"{context} availability must stay within the stable contract"))
711
+ require_locator_entry(
712
+ failures,
713
+ category=category,
714
+ context=f"{context}.contract",
715
+ payload=payload.get("contract"),
716
+ allowed_statuses={"present", "missing"},
717
+ )
718
+ for key in ("host_adapters", "repo_native_carriers", "shadow_surfaces"):
719
+ require_locator_entry(
720
+ failures,
721
+ category=category,
722
+ context=f"{context}.{key}",
723
+ payload=payload.get(key),
724
+ allowed_statuses={"present", "missing"},
725
+ )
726
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
727
+ failures.append(Failure(category, f"{context} must include non-empty `summary`"))
728
+ if not isinstance(payload.get("missing_inputs"), list):
729
+ failures.append(Failure(category, f"{context} must include `missing_inputs` as a list"))
730
+
731
+
732
+ def require_repo_specific_requirements_payload(
733
+ failures: list[Failure],
734
+ *,
735
+ category: str,
736
+ context: str,
737
+ payload: object,
738
+ expected_surface: str,
739
+ ) -> None:
740
+ if not isinstance(payload, dict):
741
+ failures.append(Failure(category, f"{context} must be an object"))
742
+ return
743
+ if payload.get("surface") != expected_surface:
744
+ failures.append(Failure(category, f"{context} must report `surface: {expected_surface}`"))
745
+ if payload.get("result") not in {"pass", "block"}:
746
+ failures.append(Failure(category, f"{context} result must be `pass` or `block`"))
747
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
748
+ failures.append(Failure(category, f"{context} must include non-empty `summary`"))
749
+ if not isinstance(payload.get("missing_inputs"), list):
750
+ failures.append(Failure(category, f"{context} must include `missing_inputs`"))
751
+ if payload.get("fallback_to") not in {None, "build", "merge"}:
752
+ failures.append(Failure(category, f"{context} fallback must stay within the stable contract"))
753
+ for key in ("declared_requirements", "blocking_requirements", "advisory_requirements"):
754
+ entries = payload.get(key)
755
+ if not isinstance(entries, list):
756
+ failures.append(Failure(category, f"{context} must include `{key}` as a list"))
757
+ continue
758
+ for index, entry in enumerate(entries):
759
+ if not isinstance(entry, dict):
760
+ failures.append(Failure(category, f"{context} {key}[{index}] must be an object"))
761
+ continue
762
+ for field in ("id", "summary", "locator", "enforcement"):
763
+ value = entry.get(field)
764
+ if not isinstance(value, str) or not value:
765
+ failures.append(Failure(category, f"{context} {key}[{index}] missing `{field}`"))
766
+ if entry.get("enforcement") not in REPO_INTERFACE_ENFORCEMENT:
767
+ failures.append(Failure(category, f"{context} {key}[{index}] enforcement must stay within the stable contract"))
768
+ declared = payload.get("declared_requirements")
769
+ blocking = payload.get("blocking_requirements")
770
+ advisory = payload.get("advisory_requirements")
771
+ if isinstance(declared, list) and isinstance(blocking, list) and isinstance(advisory, list):
772
+ if len(declared) != len(blocking) + len(advisory):
773
+ failures.append(Failure(category, f"{context} declared requirements must split cleanly into blocking and advisory"))
774
+
775
+
776
+ def require_shadow_parity_payload(
777
+ failures: list[Failure],
778
+ *,
779
+ category: str,
780
+ context: str,
781
+ payload: object,
782
+ expected_reports: int,
783
+ ) -> None:
784
+ if not isinstance(payload, dict):
785
+ failures.append(Failure(category, f"{context} must be an object"))
786
+ return
787
+ if payload.get("command") != "shadow-parity":
788
+ failures.append(Failure(category, f"{context} must report `command: shadow-parity`"))
789
+ if payload.get("result") not in {"pass", "warn"}:
790
+ failures.append(Failure(category, f"{context} result must be `pass` or `warn`"))
791
+ if payload.get("fallback_to") is not None:
792
+ failures.append(Failure(category, f"{context} fallback_to must remain `null`"))
793
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
794
+ failures.append(Failure(category, f"{context} must include non-empty `summary`"))
795
+ if not isinstance(payload.get("missing_inputs"), list):
796
+ failures.append(Failure(category, f"{context} must include `missing_inputs`"))
797
+ require_runtime_state_payload(
798
+ failures,
799
+ category=category,
800
+ context=context,
801
+ payload=payload.get("runtime_state"),
802
+ expected_scene="repo-local-demo",
803
+ expected_carrier="repo-local-wrapper",
804
+ allowed_results={"pass"},
805
+ )
806
+ governance_surface = {"governance_surface": payload.get("governance_surface")}
807
+ if isinstance(payload.get("governance_surface"), dict):
808
+ require_governance_surface(
809
+ failures,
810
+ category=category,
811
+ context=context,
812
+ payload=governance_surface,
813
+ )
814
+ reports = payload.get("reports")
815
+ if not isinstance(reports, list):
816
+ failures.append(Failure(category, f"{context} must include `reports` as a list"))
817
+ return
818
+ if len(reports) != expected_reports:
819
+ failures.append(Failure(category, f"{context} must include {expected_reports} parity reports"))
820
+ for index, report in enumerate(reports):
821
+ if not isinstance(report, dict):
822
+ failures.append(Failure(category, f"{context} reports[{index}] must be an object"))
823
+ continue
824
+ if report.get("surface") not in {"admission", "review", "merge_ready", "closeout"}:
825
+ failures.append(Failure(category, f"{context} reports[{index}] must declare a known surface"))
826
+ if report.get("result") not in {"match", "mismatch", "unreadable"}:
827
+ failures.append(Failure(category, f"{context} reports[{index}] result must stay within the stable contract"))
828
+ if not isinstance(report.get("summary"), str) or not report.get("summary"):
829
+ failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `summary`"))
830
+ if not isinstance(report.get("missing_inputs"), list):
831
+ failures.append(Failure(category, f"{context} reports[{index}] must include `missing_inputs`"))
832
+ for key in ("host_adapters", "repo_native_carriers"):
833
+ if not isinstance(report.get(key), list):
834
+ failures.append(Failure(category, f"{context} reports[{index}] must include `{key}` as a list"))
835
+ for surface_key in ("loom_surface", "repo_surface"):
836
+ surface_payload = report.get(surface_key)
837
+ if not isinstance(surface_payload, dict):
838
+ failures.append(Failure(category, f"{context} reports[{index}] must include `{surface_key}`"))
839
+ continue
840
+ if surface_payload.get("status") not in {"readable", "missing"}:
841
+ failures.append(Failure(category, f"{context} reports[{index}] `{surface_key}.status` must stay within the stable contract"))
842
+ locator = surface_payload.get("locator")
843
+ if not isinstance(locator, str) or not locator:
844
+ failures.append(Failure(category, f"{context} reports[{index}] `{surface_key}.locator` must be non-empty"))
845
+
846
+
847
+ def require_host_lifecycle_payload(
848
+ failures: list[Failure],
849
+ *,
850
+ category: str,
851
+ context: str,
852
+ payload: dict[str, object],
853
+ ) -> None:
854
+ if payload.get("result") not in {"pass", "block"}:
855
+ failures.append(Failure(category, f"{context} must return `pass` or `block`"))
856
+ if payload.get("fallback_to") not in {None, "admission"}:
857
+ failures.append(Failure(category, f"{context} fallback must be `null` or `admission`"))
858
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
859
+ failures.append(Failure(category, f"{context} must include a non-empty `summary`"))
860
+ if not isinstance(payload.get("missing_inputs"), list):
861
+ failures.append(Failure(category, f"{context} must include `missing_inputs`"))
862
+
863
+ objects = payload.get("objects")
864
+ if not isinstance(objects, dict):
865
+ failures.append(Failure(category, f"{context} must include `objects`"))
866
+ return
867
+
868
+ workspace = objects.get("workspace")
869
+ branch = objects.get("branch")
870
+ pr = objects.get("pr")
871
+ worktree = objects.get("worktree")
872
+ for key, value in (("workspace", workspace), ("branch", branch), ("pr", pr), ("worktree", worktree)):
873
+ if not isinstance(value, dict):
874
+ failures.append(Failure(category, f"{context} must include `{key}`"))
875
+ if not isinstance(workspace, dict) or not isinstance(branch, dict) or not isinstance(pr, dict) or not isinstance(worktree, dict):
876
+ return
877
+
878
+ if workspace.get("ownership") != "loom":
879
+ failures.append(Failure(category, f"{context} workspace ownership must stay `loom`"))
880
+ for field in ("entry", "path", "lifecycle_entry"):
881
+ value = workspace.get(field)
882
+ if not isinstance(value, str) or not value:
883
+ failures.append(Failure(category, f"{context} workspace must include non-empty `{field}`"))
884
+
885
+ if branch.get("ownership") != "host":
886
+ failures.append(Failure(category, f"{context} branch ownership must stay `host`"))
887
+ if branch.get("purity_status") not in {"report_only", "host_managed_without_local_branch"}:
888
+ failures.append(Failure(category, f"{context} branch purity_status must stay within the stable contract"))
889
+ if not isinstance(branch.get("next_action"), str) or not branch.get("next_action"):
890
+ failures.append(Failure(category, f"{context} branch must include non-empty `next_action`"))
891
+
892
+ if pr.get("ownership") != "host":
893
+ failures.append(Failure(category, f"{context} PR ownership must stay `host`"))
894
+ if pr.get("purity_status") != "report_only":
895
+ failures.append(Failure(category, f"{context} PR purity_status must stay `report_only`"))
896
+ if not isinstance(pr.get("next_action"), str) or not pr.get("next_action"):
897
+ failures.append(Failure(category, f"{context} PR must include non-empty `next_action`"))
898
+
899
+ if worktree.get("ownership") != "host":
900
+ failures.append(Failure(category, f"{context} worktree ownership must stay `host`"))
901
+ if worktree.get("status") != "host_managed":
902
+ failures.append(Failure(category, f"{context} worktree status must stay `host_managed`"))
903
+ for field in ("cwd_within_repo", "next_action"):
904
+ value = worktree.get(field)
905
+ if not isinstance(value, str) or not value:
906
+ failures.append(Failure(category, f"{context} worktree must include non-empty `{field}`"))
907
+
908
+
909
+ def require_reconciliation_payload(
910
+ failures: list[Failure],
911
+ *,
912
+ category: str,
913
+ context: str,
914
+ payload: object,
915
+ ) -> None:
916
+ if not isinstance(payload, dict):
917
+ failures.append(Failure(category, f"{context} must include `reconciliation` as an object"))
918
+ return
919
+ if payload.get("command") != "reconciliation":
920
+ failures.append(Failure(category, f"{context} must report `command: reconciliation`"))
921
+ if payload.get("operation") != "audit":
922
+ failures.append(Failure(category, f"{context} must report `operation: audit`"))
923
+ if payload.get("result") not in {"pass", "warn", "fix-needed", "block"}:
924
+ failures.append(Failure(category, f"{context} returned an unknown reconciliation result"))
925
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
926
+ failures.append(Failure(category, f"{context} must include a non-empty reconciliation `summary`"))
927
+ if not isinstance(payload.get("missing_inputs"), list):
928
+ failures.append(Failure(category, f"{context} must include reconciliation `missing_inputs`"))
929
+ if payload.get("fallback_to") not in {None, "manual-reconciliation"}:
930
+ failures.append(Failure(category, f"{context} reconciliation fallback must be `null` or `manual-reconciliation`"))
931
+ findings = payload.get("findings")
932
+ if not isinstance(findings, list):
933
+ failures.append(Failure(category, f"{context} must include reconciliation `findings` as a list"))
934
+ return
935
+ for finding in findings:
936
+ if not isinstance(finding, dict):
937
+ failures.append(Failure(category, f"{context} reconciliation findings must be JSON objects"))
938
+ continue
939
+ if finding.get("kind") not in {"absorbed_but_open", "parent_drift", "project_drift"}:
940
+ failures.append(Failure(category, f"{context} reconciliation finding kind must stay within the stable contract"))
941
+ if finding.get("severity") not in {"warn", "fix-needed", "block"}:
942
+ failures.append(Failure(category, f"{context} reconciliation finding severity must stay within the stable contract"))
943
+ if not isinstance(finding.get("subject"), str) or not finding.get("subject"):
944
+ failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `subject`"))
945
+ if not isinstance(finding.get("evidence"), dict):
946
+ failures.append(Failure(category, f"{context} reconciliation findings must include `evidence`"))
947
+ if not isinstance(finding.get("recommended_action"), str) or not finding.get("recommended_action"):
948
+ failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `recommended_action`"))
949
+
950
+
951
+ def require_closeout_reconciliation_contract(
952
+ failures: list[Failure],
953
+ *,
954
+ category: str,
955
+ context: str,
956
+ payload: dict[str, object],
957
+ ) -> None:
958
+ reconciliation = payload.get("reconciliation")
959
+ if reconciliation is None:
960
+ return
961
+ require_reconciliation_payload(
962
+ failures,
963
+ category=category,
964
+ context=f"{context} reconciliation",
965
+ payload=reconciliation,
966
+ )
967
+ if not isinstance(reconciliation, dict):
968
+ return
969
+ reconciliation_result = reconciliation.get("result")
970
+ closeout_result = payload.get("result")
971
+ fallback_to = payload.get("fallback_to")
972
+ if reconciliation_result == "fix-needed":
973
+ if closeout_result != "block":
974
+ failures.append(Failure(category, f"{context} must block when reconciliation returns `fix-needed`"))
975
+ if fallback_to != "reconciliation-sync":
976
+ failures.append(Failure(category, f"{context} must point `fix-needed` reconciliation drift to `reconciliation-sync`"))
977
+ if reconciliation_result == "block":
978
+ if closeout_result != "block":
979
+ failures.append(Failure(category, f"{context} must block when reconciliation returns `block`"))
980
+ if fallback_to != "manual-reconciliation":
981
+ failures.append(Failure(category, f"{context} must point blocked reconciliation drift to `manual-reconciliation`"))
982
+
983
+
984
+ def require_review_record_contract(
985
+ failures: list[Failure],
986
+ *,
987
+ category: str,
988
+ context: str,
989
+ payload: object,
990
+ ) -> None:
991
+ if not isinstance(payload, dict):
992
+ failures.append(Failure(category, f"{context} must include a review record object"))
993
+ return
994
+ findings = payload.get("findings")
995
+ if not isinstance(findings, list):
996
+ failures.append(Failure(category, f"{context} must include review `findings` as a list"))
997
+ return
998
+ for list_field in ("blocking_issues", "follow_ups"):
999
+ if not isinstance(payload.get(list_field), list):
1000
+ failures.append(Failure(category, f"{context} must include review `{list_field}` as a list"))
1001
+ consumed_inputs = payload.get("consumed_inputs")
1002
+ if consumed_inputs is not None:
1003
+ if not isinstance(consumed_inputs, dict):
1004
+ failures.append(Failure(category, f"{context} review `consumed_inputs` must be an object when present"))
1005
+ else:
1006
+ for key in ("engine_adapter", "engine_evidence", "normalized_findings"):
1007
+ value = consumed_inputs.get(key)
1008
+ if value is not None and (not isinstance(value, str) or not value):
1009
+ failures.append(Failure(category, f"{context} review consumed input `{key}` must be null or a non-empty string"))
1010
+ for finding in findings:
1011
+ if not isinstance(finding, dict):
1012
+ failures.append(Failure(category, f"{context} review findings must be JSON objects"))
1013
+ continue
1014
+ if not isinstance(finding.get("id"), str) or not finding.get("id"):
1015
+ failures.append(Failure(category, f"{context} review findings must include non-empty `id`"))
1016
+ if not isinstance(finding.get("summary"), str) or not finding.get("summary"):
1017
+ failures.append(Failure(category, f"{context} review findings must include non-empty `summary`"))
1018
+ if finding.get("severity") not in REVIEW_FINDING_SEVERITIES:
1019
+ failures.append(Failure(category, f"{context} review finding severity must stay within the stable contract"))
1020
+ rebuttal = finding.get("rebuttal")
1021
+ if rebuttal is not None and (not isinstance(rebuttal, str) or not rebuttal):
1022
+ failures.append(
1023
+ Failure(category, f"{context} review finding `rebuttal` must be `null` or a non-empty string")
1024
+ )
1025
+ disposition = finding.get("disposition")
1026
+ if disposition is not None:
1027
+ if not isinstance(disposition, dict):
1028
+ failures.append(Failure(category, f"{context} review finding disposition must be `null` or an object"))
1029
+ continue
1030
+ if disposition.get("status") not in REVIEW_FINDING_DISPOSITION_STATUSES:
1031
+ failures.append(Failure(category, f"{context} review finding disposition status must stay within the stable contract"))
1032
+ if not isinstance(disposition.get("summary"), str) or not disposition.get("summary"):
1033
+ failures.append(Failure(category, f"{context} review finding disposition must include non-empty `summary`"))
1034
+
1035
+
1036
+ def require_review_run_payload(
1037
+ failures: list[Failure],
1038
+ *,
1039
+ category: str,
1040
+ context: str,
1041
+ payload: object,
1042
+ expected_result: set[str],
1043
+ ) -> None:
1044
+ if not isinstance(payload, dict):
1045
+ failures.append(Failure(category, f"{context} must return a JSON object"))
1046
+ return
1047
+ if payload.get("command") != "review":
1048
+ failures.append(Failure(category, f"{context} must report `command: review`"))
1049
+ if payload.get("operation") != "run":
1050
+ failures.append(Failure(category, f"{context} must report `operation: run`"))
1051
+ if payload.get("result") not in expected_result:
1052
+ failures.append(Failure(category, f"{context} returned an unexpected result"))
1053
+ for key in ("item", "state_check", "runtime_evidence", "build_checkpoint", "review", "current_checkpoint", "engine", "manual_review"):
1054
+ if not isinstance(payload.get(key), dict):
1055
+ failures.append(Failure(category, f"{context} must include `{key}`"))
1056
+ require_runtime_state_payload(
1057
+ failures,
1058
+ category=category,
1059
+ context=context,
1060
+ payload=payload.get("runtime_state"),
1061
+ allowed_results={"pass", "block"},
1062
+ )
1063
+ engine = payload.get("engine")
1064
+ if not isinstance(engine, dict):
1065
+ return
1066
+ if engine.get("engine") != "codex":
1067
+ failures.append(Failure(category, f"{context} engine must stay `codex` for the default path"))
1068
+ if engine.get("adapter") != "loom/default-codex":
1069
+ failures.append(Failure(category, f"{context} adapter must stay `loom/default-codex`"))
1070
+ if engine.get("result") not in {"pass", "block", "not_run"}:
1071
+ failures.append(Failure(category, f"{context} engine result must stay within the stable contract"))
1072
+ if engine.get("failure_reason") not in {None, "engine_unavailable", "schema_drift", "runtime_conflict", "repo_diff_detected"}:
1073
+ failures.append(Failure(category, f"{context} engine failure reason must stay within the stable contract"))
1074
+ evidence = engine.get("evidence")
1075
+ if engine.get("result") == "not_run":
1076
+ if evidence is not None:
1077
+ failures.append(Failure(category, f"{context} engine evidence must be null when the engine is not run"))
1078
+ else:
1079
+ if not isinstance(evidence, dict):
1080
+ failures.append(Failure(category, f"{context} engine must include `evidence` when it runs"))
1081
+ else:
1082
+ for key in ("runtime_root", "prompt", "raw_result", "normalized_findings", "metadata"):
1083
+ value = evidence.get(key)
1084
+ if not isinstance(value, str) or not value:
1085
+ failures.append(Failure(category, f"{context} engine evidence must include non-empty `{key}`"))
1086
+ manual_review = payload.get("manual_review")
1087
+ if isinstance(manual_review, dict):
1088
+ if not isinstance(manual_review.get("summary"), str) or not manual_review.get("summary"):
1089
+ failures.append(Failure(category, f"{context} manual_review must include non-empty `summary`"))
1090
+ if not isinstance(manual_review.get("review_record_path"), str) or not manual_review.get("review_record_path"):
1091
+ failures.append(Failure(category, f"{context} manual_review must include `review_record_path`"))
1092
+ if manual_review.get("recommended_kind") not in {"general_review", "code_review", "spec_review"}:
1093
+ failures.append(Failure(category, f"{context} manual_review recommended kind must stay within the stable contract"))
1094
+ if not isinstance(manual_review.get("command"), list):
1095
+ failures.append(Failure(category, f"{context} manual_review must include `command` as a list"))
1096
+ review_record_input = payload.get("review_record_input")
1097
+ if payload.get("result") == "pass":
1098
+ if not isinstance(review_record_input, dict):
1099
+ failures.append(Failure(category, f"{context} must include `review_record_input` when engine review passes"))
1100
+ else:
1101
+ for key in ("decision", "summary", "reviewer", "kind", "findings_file", "engine_adapter", "engine_evidence", "normalized_findings"):
1102
+ value = review_record_input.get(key)
1103
+ if not isinstance(value, str) or not value:
1104
+ failures.append(Failure(category, f"{context} review_record_input must include non-empty `{key}`"))
1105
+ if review_record_input.get("decision") not in {"allow", "block", "fallback"}:
1106
+ failures.append(Failure(category, f"{context} review_record_input decision must stay within the stable contract"))
1107
+ if review_record_input.get("reviewer") != "loom/default-codex":
1108
+ failures.append(Failure(category, f"{context} review_record_input reviewer must stay `loom/default-codex`"))
1109
+
1110
+
1111
+ def require_runtime_state_payload(
1112
+ failures: list[Failure],
1113
+ *,
1114
+ category: str,
1115
+ context: str,
1116
+ payload: object,
1117
+ expected_scene: str | None = None,
1118
+ expected_carrier: str | None = None,
1119
+ allowed_results: set[str] | None = None,
1120
+ ) -> None:
1121
+ if not isinstance(payload, dict):
1122
+ failures.append(Failure(category, f"{context} must include `runtime_state` as an object"))
1123
+ return
1124
+ if payload.get("result") not in (allowed_results or {"pass", "block"}):
1125
+ failures.append(Failure(category, f"{context} runtime_state.result must stay within the stable contract"))
1126
+ if expected_scene is not None and payload.get("scene") != expected_scene:
1127
+ failures.append(Failure(category, f"{context} runtime_state.scene must be `{expected_scene}`"))
1128
+ if expected_carrier is not None and payload.get("carrier") != expected_carrier:
1129
+ failures.append(Failure(category, f"{context} runtime_state.carrier must be `{expected_carrier}`"))
1130
+ if payload.get("entry_family") not in {"loom-init", "loom-flow"}:
1131
+ failures.append(Failure(category, f"{context} runtime_state.entry_family must stay within the stable contract"))
1132
+ if not isinstance(payload.get("runtime_root"), str) or not payload.get("runtime_root"):
1133
+ failures.append(Failure(category, f"{context} runtime_state must include non-empty `runtime_root`"))
1134
+ checks = payload.get("checks")
1135
+ if not isinstance(checks, dict):
1136
+ failures.append(Failure(category, f"{context} runtime_state must include `checks`"))
1137
+ return
1138
+ for key in ("scene_marker", "carrier_layout", "registry_contract", "shared_runtime", "referenced_resources"):
1139
+ check = checks.get(key)
1140
+ if not isinstance(check, dict):
1141
+ failures.append(Failure(category, f"{context} runtime_state must include check `{key}`"))
1142
+ continue
1143
+ if check.get("status") not in {"pass", "block", "not_applicable"}:
1144
+ failures.append(Failure(category, f"{context} runtime_state check `{key}` returned an unknown status"))
1145
+ if not isinstance(check.get("summary"), str) or not check.get("summary"):
1146
+ failures.append(Failure(category, f"{context} runtime_state check `{key}` must include non-empty `summary`"))
1147
+
1148
+
1149
+ def require_route_payload(
1150
+ failures: list[Failure],
1151
+ *,
1152
+ category: str,
1153
+ context: str,
1154
+ payload: object,
1155
+ expected_skill: str,
1156
+ expected_mode: str,
1157
+ expected_runtime_scene: str | None = None,
1158
+ expected_runtime_carrier: str | None = None,
1159
+ allowed_results: set[str] | None = None,
1160
+ ) -> None:
1161
+ if not isinstance(payload, dict):
1162
+ failures.append(Failure(category, f"{context} must return a JSON object"))
1163
+ return
1164
+ if payload.get("command") != "route":
1165
+ failures.append(Failure(category, f"{context} must report `command: route`"))
1166
+ if payload.get("result") not in (allowed_results or {"pass"}):
1167
+ failures.append(Failure(category, f"{context} result must stay within the stable contract"))
1168
+ if payload.get("selected_skill") != expected_skill:
1169
+ failures.append(Failure(category, f"{context} must select `{expected_skill}`"))
1170
+ if payload.get("mode") != expected_mode:
1171
+ failures.append(Failure(category, f"{context} must report `mode: {expected_mode}`"))
1172
+ if not isinstance(payload.get("matched_signals"), list):
1173
+ failures.append(Failure(category, f"{context} must include `matched_signals`"))
1174
+ if not isinstance(payload.get("missing_inputs"), list):
1175
+ failures.append(Failure(category, f"{context} must include `missing_inputs`"))
1176
+ if payload.get("fallback_to") not in {"loom-init", "refresh-install", "rebootstrap-runtime", "manual-runtime-reconciliation", None}:
1177
+ failures.append(Failure(category, f"{context} fallback must stay within the stable contract"))
1178
+ if expected_runtime_scene is not None or expected_runtime_carrier is not None:
1179
+ require_runtime_state_payload(
1180
+ failures,
1181
+ category=category,
1182
+ context=context,
1183
+ payload=payload.get("runtime_state"),
1184
+ expected_scene=expected_runtime_scene,
1185
+ expected_carrier=expected_runtime_carrier,
1186
+ allowed_results={"pass", "block"},
1187
+ )
1188
+
1189
+
1190
+ def check_skill_manifests(root: Path) -> list[Failure]:
1191
+ failures: list[Failure] = []
1192
+ expected_entries = {
1193
+ "loom-init": "bootstrap/root",
1194
+ "loom-adopt": "scenario/adopt",
1195
+ "loom-resume": "scenario/resume",
1196
+ "loom-pre-review": "scenario/pre-review",
1197
+ "loom-review": "scenario/review",
1198
+ "loom-handoff": "scenario/handoff",
1199
+ "loom-retire": "scenario/retire",
1200
+ "loom-merge-ready": "scenario/merge-ready",
1201
+ }
1202
+ registry_path = root / "skills/registry.json"
1203
+ upgrade_contract_path = root / "skills/upgrade-contract.json"
1204
+
1205
+ for candidate in (registry_path, upgrade_contract_path):
1206
+ if not candidate.exists():
1207
+ return failures
1208
+
1209
+ try:
1210
+ registry = load_json_file(registry_path)
1211
+ except json.JSONDecodeError as exc:
1212
+ return [Failure("skill-manifests", f"`skills/registry.json` is invalid JSON: {exc.msg}")]
1213
+
1214
+ try:
1215
+ upgrade_contract = load_json_file(upgrade_contract_path)
1216
+ except json.JSONDecodeError as exc:
1217
+ return [Failure("skill-manifests", f"`skills/upgrade-contract.json` is invalid JSON: {exc.msg}")]
1218
+
1219
+ if not isinstance(registry, dict):
1220
+ failures.append(Failure("skill-manifests", "`skills/registry.json` must be a JSON object"))
1221
+ return failures
1222
+ if not isinstance(upgrade_contract, dict):
1223
+ failures.append(Failure("skill-manifests", "`skills/upgrade-contract.json` must be a JSON object"))
1224
+ return failures
1225
+
1226
+ registry_version = registry.get("registry_version")
1227
+ root_entry = registry.get("root_entry")
1228
+ entries = registry.get("entries")
1229
+ upgrade_reference = registry.get("upgrade_contract")
1230
+ install_layout_reference = registry.get("install_layout")
1231
+ layout_manifest: dict[str, object] | None = None
1232
+ if registry_version != upgrade_contract.get("registry_version"):
1233
+ failures.append(Failure("skill-manifests", "`skills/upgrade-contract.json` registry version must match `skills/registry.json`"))
1234
+ if install_layout_reference != "install-layout.json":
1235
+ failures.append(Failure("skill-manifests", "`skills/registry.json` must point `install_layout` to `install-layout.json`"))
1236
+ else:
1237
+ install_layout_path = registry_path.parent / install_layout_reference
1238
+ if not install_layout_path.exists():
1239
+ failures.append(Failure("skill-manifests", "`skills/install-layout.json` must exist"))
1240
+ else:
1241
+ try:
1242
+ candidate_layout = load_json_file(install_layout_path)
1243
+ except json.JSONDecodeError as exc:
1244
+ failures.append(Failure("skill-manifests", f"`skills/install-layout.json` is invalid JSON: {exc.msg}"))
1245
+ else:
1246
+ layout_manifest = candidate_layout
1247
+ required_paths = candidate_layout.get("required_paths")
1248
+ if not isinstance(required_paths, list) or not required_paths:
1249
+ failures.append(Failure("skill-manifests", "`skills/install-layout.json` must declare a non-empty `required_paths`"))
1250
+ else:
1251
+ for relative in required_paths:
1252
+ if not isinstance(relative, str) or not relative:
1253
+ failures.append(Failure("skill-manifests", "`skills/install-layout.json` required paths must be non-empty strings"))
1254
+ continue
1255
+ if not (registry_path.parent / relative).exists():
1256
+ failures.append(Failure("skill-manifests", f"`skills/install-layout.json` points to missing path `{relative}`"))
1257
+ runtime_state = candidate_layout.get("runtime_state")
1258
+ if not isinstance(runtime_state, dict):
1259
+ failures.append(Failure("skill-manifests", "`skills/install-layout.json` must declare `runtime_state`"))
1260
+ else:
1261
+ recognized_states = runtime_state.get("recognized_states")
1262
+ if recognized_states != ["installed-runtime", "repo-local-demo", "upgrade-rehearsal"]:
1263
+ failures.append(
1264
+ Failure(
1265
+ "skill-manifests",
1266
+ "`skills/install-layout.json` runtime_state recognized_states must stay in the stable order",
1267
+ )
1268
+ )
1269
+ if not isinstance(root_entry, str) or not root_entry:
1270
+ failures.append(Failure("skill-manifests", "`skills/registry.json` must declare a non-empty `root_entry`"))
1271
+ return failures
1272
+ if not isinstance(entries, list) or not entries:
1273
+ failures.append(Failure("skill-manifests", "`skills/registry.json` must declare at least one entry"))
1274
+ return failures
1275
+ if root_entry != "loom-init":
1276
+ failures.append(Failure("skill-manifests", "`skills/registry.json` root entry must remain `loom-init`"))
1277
+
1278
+ root_registry_entry: dict[str, object] | None = None
1279
+ seen_ids: set[str] = set()
1280
+ for entry in entries:
1281
+ if not isinstance(entry, dict):
1282
+ failures.append(Failure("skill-manifests", "every registry entry must be an object"))
1283
+ continue
1284
+ entry_id = entry.get("id")
1285
+ if not isinstance(entry_id, str) or not entry_id:
1286
+ failures.append(Failure("skill-manifests", "every registry entry must declare a non-empty `id`"))
1287
+ continue
1288
+ if entry_id in seen_ids:
1289
+ failures.append(Failure("skill-manifests", f"registry declares duplicate entry `{entry_id}`"))
1290
+ continue
1291
+ seen_ids.add(entry_id)
1292
+ if entry_id == root_entry:
1293
+ root_registry_entry = entry
1294
+ expected_role = expected_entries.get(entry_id)
1295
+ if expected_role is None:
1296
+ failures.append(Failure("skill-manifests", f"registry declares unexpected entry `{entry_id}`"))
1297
+ elif entry.get("role") != expected_role:
1298
+ failures.append(Failure("skill-manifests", f"registry entry `{entry_id}` must declare role `{expected_role}`"))
1299
+
1300
+ for field in ("role", "contract_version", "manifest", "executable"):
1301
+ value = entry.get(field)
1302
+ if not isinstance(value, str) or not value:
1303
+ failures.append(Failure("skill-manifests", f"registry entry `{entry_id}` must declare `{field}`"))
1304
+
1305
+ manifest_path = entry.get("manifest")
1306
+ if not isinstance(manifest_path, str) or not manifest_path:
1307
+ continue
1308
+ manifest_file = registry_path.parent / manifest_path
1309
+ if not manifest_file.exists():
1310
+ failures.append(Failure("skill-manifests", f"registry entry `{entry_id}` points to missing manifest `{manifest_path}`"))
1311
+ continue
1312
+ executable_path = entry.get("executable")
1313
+ if isinstance(executable_path, str) and executable_path:
1314
+ if not (registry_path.parent / executable_path).resolve().exists():
1315
+ failures.append(
1316
+ Failure("skill-manifests", f"registry entry `{entry_id}` points to missing executable `{executable_path}`")
1317
+ )
1318
+
1319
+ try:
1320
+ contract = load_json_file(manifest_file)
1321
+ except json.JSONDecodeError as exc:
1322
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` is invalid JSON: {exc.msg}"))
1323
+ continue
1324
+ if not isinstance(contract, dict):
1325
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must be a JSON object"))
1326
+ continue
1327
+
1328
+ if contract.get("id") != entry_id:
1329
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` id must match registry entry `{entry_id}`"))
1330
+ if contract.get("role") != entry.get("role"):
1331
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` role must match registry entry `{entry_id}`"))
1332
+ if contract.get("contract_version") != entry.get("contract_version"):
1333
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` contract version must match registry entry `{entry_id}`"))
1334
+
1335
+ contract_root = contract.get("root_entry")
1336
+ if entry_id == root_entry:
1337
+ if contract_root is not True:
1338
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `root_entry: true`"))
1339
+ elif contract_root is not False:
1340
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `root_entry: false`"))
1341
+
1342
+ entrypoint = contract.get("entrypoint")
1343
+ if not isinstance(entrypoint, dict):
1344
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `entrypoint`"))
1345
+ else:
1346
+ required_entrypoint_keys = {"skill_markdown", "adapter_metadata"}
1347
+ if entry_id == "loom-init":
1348
+ required_entrypoint_keys.add("bootstrap_cli")
1349
+ required_entrypoint_keys.add("route_cli")
1350
+ else:
1351
+ required_entrypoint_keys.add("orchestration_cli")
1352
+ for key in required_entrypoint_keys:
1353
+ value = entrypoint.get(key)
1354
+ if not isinstance(value, str) or not value:
1355
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` missing `entrypoint.{key}`"))
1356
+ continue
1357
+ if not (manifest_file.parent / value).exists():
1358
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` points `entrypoint.{key}` to missing `{value}`"))
1359
+
1360
+ for section in ("input_contract", "output_contract", "routing"):
1361
+ value = contract.get(section)
1362
+ if not isinstance(value, dict):
1363
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `{section}`"))
1364
+ continue
1365
+ reference = value.get("reference")
1366
+ if not isinstance(reference, str) or not reference:
1367
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `{section}.reference`"))
1368
+ continue
1369
+ if not (manifest_file.parent / reference).exists():
1370
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` points `{section}.reference` to missing `{reference}`"))
1371
+
1372
+ output_contract = contract.get("output_contract")
1373
+ if isinstance(output_contract, dict) and entry_id in GOVERNANCE_SURFACE_CONTRACT_SKILLS:
1374
+ required_sections = output_contract.get("required_sections")
1375
+ if not isinstance(required_sections, list):
1376
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `output_contract.required_sections`"))
1377
+ elif "governance_surface" not in required_sections:
1378
+ failures.append(
1379
+ Failure(
1380
+ "skill-manifests",
1381
+ f"`{manifest_path}` must require `governance_surface` in `output_contract.required_sections`",
1382
+ )
1383
+ )
1384
+
1385
+ installation = contract.get("installation")
1386
+ if not isinstance(installation, dict):
1387
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `installation`"))
1388
+ else:
1389
+ for field in ("registry", "upgrade_contract", "layout_manifest"):
1390
+ value = installation.get(field)
1391
+ if not isinstance(value, str) or not value:
1392
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `installation.{field}`"))
1393
+ continue
1394
+ if not (manifest_file.parent / value).exists():
1395
+ failures.append(Failure("skill-manifests", f"`{manifest_path}` points `installation.{field}` to missing `{value}`"))
1396
+
1397
+ if root_registry_entry is None:
1398
+ failures.append(Failure("skill-manifests", f"`skills/registry.json` root entry `{root_entry}` does not match any declared entry"))
1399
+ return failures
1400
+ if seen_ids != set(expected_entries):
1401
+ missing = sorted(set(expected_entries) - seen_ids)
1402
+ extra = sorted(seen_ids - set(expected_entries))
1403
+ if missing:
1404
+ failures.append(Failure("skill-manifests", f"registry is missing first-wave entries: {', '.join(missing)}"))
1405
+ if extra:
1406
+ failures.append(Failure("skill-manifests", f"registry contains unexpected first-wave entries: {', '.join(extra)}"))
1407
+ if upgrade_reference != "upgrade-contract.json":
1408
+ failures.append(Failure("skill-manifests", "`skills/registry.json` must point to `upgrade-contract.json`"))
1409
+
1410
+ upgrade_root = upgrade_contract.get("root_entry")
1411
+ current_contract_version = upgrade_contract.get("current_contract_version")
1412
+ upgrade_policy = upgrade_contract.get("upgrade_policy")
1413
+ if upgrade_root != root_entry:
1414
+ failures.append(
1415
+ Failure(
1416
+ "skill-manifests",
1417
+ f"`skills/upgrade-contract.json` root entry `{upgrade_root}` does not match registry root `{root_entry}`",
1418
+ )
1419
+ )
1420
+ if current_contract_version != root_registry_entry.get("contract_version"):
1421
+ failures.append(
1422
+ Failure(
1423
+ "skill-manifests",
1424
+ "`skills/upgrade-contract.json` current contract version must match the registry entry version",
1425
+ )
1426
+ )
1427
+ if not isinstance(upgrade_policy, dict):
1428
+ failures.append(Failure("skill-manifests", "`skills/upgrade-contract.json` must declare `upgrade_policy`"))
1429
+ else:
1430
+ if upgrade_policy.get("mode") != "explicit":
1431
+ failures.append(Failure("skill-manifests", "`upgrade_policy.mode` must be `explicit`"))
1432
+ refresh_required = upgrade_policy.get("refresh_required")
1433
+ if not isinstance(refresh_required, list) or not refresh_required:
1434
+ failures.append(Failure("skill-manifests", "`upgrade_policy.refresh_required` must be a non-empty list"))
1435
+ else:
1436
+ required = {"registry", "manifest", "executable", "referenced_resources", "layout_manifest"}
1437
+ if not required.issubset(set(refresh_required)):
1438
+ failures.append(
1439
+ Failure(
1440
+ "skill-manifests",
1441
+ "`upgrade_policy.refresh_required` must cover registry, manifest, executable, referenced_resources, and layout_manifest",
1442
+ )
1443
+ )
1444
+
1445
+ return failures
1446
+
1447
+
1448
+ def check_skill_routing(root: Path) -> list[Failure]:
1449
+ failures: list[Failure] = []
1450
+ target = root / "examples/new-project"
1451
+ tool_path = root / "tools/loom_init.py"
1452
+ if not tool_path.exists() or not target.exists():
1453
+ return failures
1454
+
1455
+ registry = load_json_file(root / "skills/registry.json")
1456
+ if not isinstance(registry, dict):
1457
+ return failures
1458
+ entries = registry.get("entries")
1459
+ if not isinstance(entries, list):
1460
+ return failures
1461
+ explicit_skills = [
1462
+ entry.get("id")
1463
+ for entry in entries
1464
+ if isinstance(entry, dict) and isinstance(entry.get("id"), str) and entry.get("id")
1465
+ ]
1466
+ for skill_id in explicit_skills:
1467
+ payload, error = load_command_json(
1468
+ root,
1469
+ ["python3", "tools/loom_init.py", "route", "--target", "examples/new-project", "--skill", skill_id],
1470
+ )
1471
+ if error:
1472
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` failed: {error}"))
1473
+ continue
1474
+ if payload.get("command") != "route":
1475
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must report `command: route`"))
1476
+ if payload.get("result") != "pass":
1477
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must pass"))
1478
+ if payload.get("selected_skill") != skill_id:
1479
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` selected `{payload.get('selected_skill')}`"))
1480
+ if payload.get("mode") != "explicit":
1481
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must report `mode: explicit`"))
1482
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
1483
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must include `summary`"))
1484
+ if not isinstance(payload.get("matched_signals"), list):
1485
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must include `matched_signals`"))
1486
+ if not isinstance(payload.get("missing_inputs"), list):
1487
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must include `missing_inputs`"))
1488
+ if payload.get("fallback_to") != "loom-init":
1489
+ failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must keep `fallback_to: loom-init`"))
1490
+ if skill_id in GOVERNANCE_SURFACE_ROUTE_SKILLS and payload.get("result") == "pass":
1491
+ require_governance_surface(
1492
+ failures,
1493
+ category="skill-routing",
1494
+ context=f"explicit route for `{skill_id}`",
1495
+ payload=payload,
1496
+ )
1497
+
1498
+ implicit_cases = (
1499
+ ("请初始化这个新项目并接入 Loom", "loom-adopt"),
1500
+ ("请接手当前事项并恢复上下文后继续推进", "loom-resume"),
1501
+ ("请在进入 review 前做统一检查", "loom-pre-review"),
1502
+ ("请对当前事项做正式 review 并给出审查结论", "loom-review"),
1503
+ ("请准备交接并回写停点", "loom-handoff"),
1504
+ ("请清理并 retire 当前事项现场", "loom-retire"),
1505
+ ("请确认这个事项是否 merge-ready", "loom-merge-ready"),
1506
+ )
1507
+ for task, skill_id in implicit_cases:
1508
+ payload, error = load_command_json(
1509
+ root,
1510
+ ["python3", "tools/loom_init.py", "route", "--target", "examples/new-project", "--task", task],
1511
+ )
1512
+ if error:
1513
+ failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` failed: {error}"))
1514
+ continue
1515
+ if payload.get("command") != "route":
1516
+ failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must report `command: route`"))
1517
+ if payload.get("result") != "pass":
1518
+ failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must pass"))
1519
+ if payload.get("selected_skill") != skill_id:
1520
+ failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` selected `{payload.get('selected_skill')}`"))
1521
+ if payload.get("mode") != "implicit":
1522
+ failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must report `mode: implicit`"))
1523
+ if not isinstance(payload.get("matched_signals"), list) or not payload.get("matched_signals"):
1524
+ failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must include matched signals"))
1525
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
1526
+ failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must include `summary`"))
1527
+ if payload.get("fallback_to") != "loom-init":
1528
+ failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must keep `fallback_to: loom-init`"))
1529
+ if skill_id in GOVERNANCE_SURFACE_ROUTE_SKILLS and payload.get("result") == "pass":
1530
+ require_governance_surface(
1531
+ failures,
1532
+ category="skill-routing",
1533
+ context=f"implicit route for `{skill_id}`",
1534
+ payload=payload,
1535
+ )
1536
+
1537
+ fallback_payload, error = load_command_json(
1538
+ root,
1539
+ ["python3", "tools/loom_init.py", "route", "--target", "examples/new-project", "--task", "请帮我看看这个仓库"],
1540
+ )
1541
+ if error:
1542
+ failures.append(Failure("skill-routing", f"fallback route failed: {error}"))
1543
+ else:
1544
+ if fallback_payload.get("result") != "fallback":
1545
+ failures.append(Failure("skill-routing", "ambiguous task must return `fallback`"))
1546
+ if fallback_payload.get("selected_skill") != "loom-init":
1547
+ failures.append(Failure("skill-routing", "fallback route must select `loom-init`"))
1548
+ if not isinstance(fallback_payload.get("missing_inputs"), list) or not fallback_payload.get("missing_inputs"):
1549
+ failures.append(Failure("skill-routing", "fallback route must include `missing_inputs`"))
1550
+
1551
+ ambiguous_payload, error = load_command_json(
1552
+ root,
1553
+ [
1554
+ "python3",
1555
+ "tools/loom_init.py",
1556
+ "route",
1557
+ "--target",
1558
+ "examples/new-project",
1559
+ "--task",
1560
+ "请接手当前事项并在 review 前检查",
1561
+ ],
1562
+ )
1563
+ if error:
1564
+ failures.append(Failure("skill-routing", f"ambiguous route failed: {error}"))
1565
+ else:
1566
+ if ambiguous_payload.get("result") != "fallback":
1567
+ failures.append(Failure("skill-routing", "multi-match task must return `fallback`"))
1568
+ if ambiguous_payload.get("selected_skill") != "loom-init":
1569
+ failures.append(Failure("skill-routing", "multi-match route must select `loom-init`"))
1570
+ if not isinstance(ambiguous_payload.get("matched_signals"), list) or len(ambiguous_payload.get("matched_signals", [])) < 2:
1571
+ failures.append(Failure("skill-routing", "multi-match route must expose matched signals"))
1572
+
1573
+ unknown_payload, error = load_command_json(
1574
+ root,
1575
+ ["python3", "tools/loom_init.py", "route", "--target", "examples/new-project", "--skill", "not-a-skill"],
1576
+ )
1577
+ if error:
1578
+ failures.append(Failure("skill-routing", f"unknown explicit route failed: {error}"))
1579
+ else:
1580
+ if unknown_payload.get("result") != "block":
1581
+ failures.append(Failure("skill-routing", "unknown explicit skill must block"))
1582
+ if unknown_payload.get("selected_skill") != "loom-init":
1583
+ failures.append(Failure("skill-routing", "unknown explicit skill must fall back to `loom-init`"))
1584
+
1585
+ return failures
1586
+
1587
+
1588
+ def check_demo_assets(root: Path) -> list[Failure]:
1589
+ failures = check_required_paths(root, "demo-assets", DEMO_ASSETS)
1590
+
1591
+ init_result_path = root / "examples/new-project/.loom/bootstrap/init-result.json"
1592
+ if init_result_path.exists():
1593
+ try:
1594
+ init_result = load_json_file(init_result_path)
1595
+ except json.JSONDecodeError as exc:
1596
+ failures.append(Failure("demo-assets", f"demo init-result is invalid JSON: {exc.msg}"))
1597
+ return failures
1598
+ if not isinstance(init_result, dict):
1599
+ failures.append(Failure("demo-assets", "demo init-result must be a JSON object"))
1600
+ return failures
1601
+ run = init_result.get("run")
1602
+ if not isinstance(run, dict) or run.get("scenario_key") != "new":
1603
+ failures.append(Failure("demo-assets", "demo init-result must keep `scenario_key` as `new`"))
1604
+ return failures
1605
+
1606
+
1607
+ def check_demo_fact_chain(root: Path) -> list[Failure]:
1608
+ target = root / "examples/new-project"
1609
+ if not target.exists():
1610
+ return []
1611
+
1612
+ report, errors = inspect_fact_chain(target)
1613
+ failures: list[Failure] = []
1614
+ for detail in errors:
1615
+ failures.append(Failure("demo-fact-chain", detail))
1616
+ if report and report.get("fact_chain", {}).get("entry_points", {}).get("status_surface") != ".loom/status/current.md":
1617
+ failures.append(Failure("demo-fact-chain", "demo fact chain must point status_surface to `.loom/status/current.md`"))
1618
+ return failures
1619
+
1620
+
1621
+ def check_demo_repo_local_cli(root: Path) -> list[Failure]:
1622
+ failures: list[Failure] = []
1623
+ target = root / "examples/new-project"
1624
+ if not target.exists():
1625
+ return failures
1626
+
1627
+ repo_local_commands = [
1628
+ (
1629
+ "repo-local-verify",
1630
+ ["python3", ".loom/bin/loom_init.py", "verify", "--target", "."],
1631
+ "ok",
1632
+ ),
1633
+ (
1634
+ "repo-local-fact-chain",
1635
+ ["python3", ".loom/bin/loom_init.py", "fact-chain", "--target", "."],
1636
+ "ok",
1637
+ ),
1638
+ ]
1639
+ for label, args, expected_key in repo_local_commands:
1640
+ payload, error = load_command_json(root, args, cwd=target)
1641
+ if error:
1642
+ failures.append(Failure("demo-repo-local-cli", f"`{label}` failed: {error}"))
1643
+ continue
1644
+ if payload.get(expected_key) is not True:
1645
+ failures.append(Failure("demo-repo-local-cli", f"`{label}` must report `{expected_key}: true`"))
1646
+ return failures
1647
+
1648
+
1649
+ def check_deep_existing_repo_bootstrap(root: Path) -> list[Failure]:
1650
+ failures: list[Failure] = []
1651
+ with tempfile.TemporaryDirectory(prefix="loom-check-deep-existing-") as tmp:
1652
+ tmp_root = Path(tmp)
1653
+
1654
+ def write_repo(target: Path, *, validation_entry: bool, pr_template: bool, workflow_doc: bool) -> None:
1655
+ (target / ".github" / "workflows").mkdir(parents=True, exist_ok=True)
1656
+ (target / "scripts").mkdir(parents=True, exist_ok=True)
1657
+ (target / "src").mkdir(parents=True, exist_ok=True)
1658
+ (target / "README.md").write_text("# Sample Repo\n", encoding="utf-8")
1659
+ (target / "AGENTS.md").write_text("# Root Rules\n", encoding="utf-8")
1660
+ (target / "src" / "main.py").write_text("print('ok')\n", encoding="utf-8")
1661
+ (target / "scripts" / "governance_status.py").write_text("print('ok')\n", encoding="utf-8")
1662
+ (target / ".github" / "workflows" / "ci.yml").write_text("name: ci\n", encoding="utf-8")
1663
+ if workflow_doc:
1664
+ (target / "WORKFLOW.md").write_text("# Workflow\n", encoding="utf-8")
1665
+ if validation_entry:
1666
+ (target / "Makefile").write_text("check:\n\t@echo ok\n", encoding="utf-8")
1667
+ if pr_template:
1668
+ (target / ".github" / "PULL_REQUEST_TEMPLATE.md").write_text("## Summary\n", encoding="utf-8")
1669
+
1670
+ deep_target = tmp_root / "deep-existing"
1671
+ write_repo(deep_target, validation_entry=True, pr_template=True, workflow_doc=True)
1672
+ deep_payload, deep_error = load_command_json(
1673
+ root,
1674
+ [
1675
+ "python3",
1676
+ "tools/loom_init.py",
1677
+ "bootstrap",
1678
+ "--target",
1679
+ str(deep_target),
1680
+ "--write",
1681
+ "--force",
1682
+ "--verify",
1683
+ "--install-pr-template",
1684
+ ],
1685
+ )
1686
+ if deep_error:
1687
+ failures.append(Failure("deep-existing-bootstrap", f"`deep-existing bootstrap` failed: {deep_error}"))
1688
+ else:
1689
+ recommended = deep_payload.get("recommended_adoption")
1690
+ verification = deep_payload.get("verification")
1691
+ governance_surface = deep_payload.get("governance_surface")
1692
+ if not isinstance(recommended, dict) or recommended.get("path") != "deep-existing-repo":
1693
+ failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must select `recommended_adoption.path = deep-existing-repo`"))
1694
+ run = deep_payload.get("run")
1695
+ if not isinstance(run, dict) or run.get("scenario_key") != "complex-existing":
1696
+ failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must keep `scenario_key = complex-existing`"))
1697
+ if not isinstance(verification, dict) or verification.get("ok") is not True:
1698
+ failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must verify successfully"))
1699
+ if not isinstance(governance_surface, dict) or governance_surface.get("repository_mode") != "complex-existing":
1700
+ failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must keep `governance_surface.repository_mode = complex-existing`"))
1701
+ for required in (
1702
+ ".loom/companion/README.md",
1703
+ ".loom/companion/checkpoints.md",
1704
+ ".loom/companion/review.md",
1705
+ ".loom/companion/merge-ready.md",
1706
+ ".loom/companion/closeout.md",
1707
+ ):
1708
+ if not (deep_target / required).exists():
1709
+ failures.append(Failure("deep-existing-bootstrap", f"`deep-existing bootstrap` is missing `{required}`"))
1710
+ for forbidden in (
1711
+ ".loom/work-items/INIT-0001.md",
1712
+ ".loom/progress/INIT-0001.md",
1713
+ ".loom/status/current.md",
1714
+ ):
1715
+ if (deep_target / forbidden).exists():
1716
+ failures.append(Failure("deep-existing-bootstrap", f"`deep-existing bootstrap` must not generate `{forbidden}`"))
1717
+ fact_chain = deep_payload.get("fact_chain")
1718
+ if not isinstance(fact_chain, dict) or fact_chain.get("mode") != "repo-native attach-only":
1719
+ failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must keep `fact_chain.mode = repo-native attach-only`"))
1720
+
1721
+ full_target = tmp_root / "full-bootstrap"
1722
+ write_repo(full_target, validation_entry=False, pr_template=False, workflow_doc=False)
1723
+ full_payload, full_error = load_command_json(
1724
+ root,
1725
+ [
1726
+ "python3",
1727
+ "tools/loom_init.py",
1728
+ "bootstrap",
1729
+ "--target",
1730
+ str(full_target),
1731
+ "--write",
1732
+ "--force",
1733
+ "--verify",
1734
+ "--install-pr-template",
1735
+ ],
1736
+ )
1737
+ if full_error:
1738
+ failures.append(Failure("deep-existing-bootstrap", f"`full-bootstrap fallback sample` failed: {full_error}"))
1739
+ else:
1740
+ recommended = full_payload.get("recommended_adoption")
1741
+ if not isinstance(recommended, dict) or recommended.get("path") != "full-bootstrap":
1742
+ failures.append(Failure("deep-existing-bootstrap", "complex existing sample without overload must keep `recommended_adoption.path = full-bootstrap`"))
1743
+ for required in (
1744
+ ".loom/work-items/INIT-0001.md",
1745
+ ".loom/progress/INIT-0001.md",
1746
+ ".loom/status/current.md",
1747
+ ):
1748
+ if not (full_target / required).exists():
1749
+ failures.append(Failure("deep-existing-bootstrap", f"`full-bootstrap fallback sample` must generate `{required}`"))
1750
+ return failures
1751
+
1752
+
1753
+ def check_daily_execution_cli(root: Path) -> list[Failure]:
1754
+ failures: list[Failure] = []
1755
+ example_target = root / "examples/new-project"
1756
+ tool_path = root / "tools/loom_flow.py"
1757
+ if not tool_path.exists() or not example_target.exists():
1758
+ return failures
1759
+
1760
+ demo_commands = [
1761
+ (
1762
+ "runtime-state-init",
1763
+ ["python3", "tools/loom_init.py", "runtime-state", "--target", "."],
1764
+ {"pass"},
1765
+ ),
1766
+ (
1767
+ "runtime-state-flow",
1768
+ ["python3", "tools/loom_flow.py", "runtime-state", "--target", "examples/new-project", "--item", "INIT-0001"],
1769
+ {"pass"},
1770
+ ),
1771
+ (
1772
+ "fact-chain",
1773
+ ["python3", "tools/loom_flow.py", "fact-chain", "--target", "examples/new-project", "--item", "INIT-0001"],
1774
+ {"pass"},
1775
+ ),
1776
+ (
1777
+ "runtime-evidence",
1778
+ [
1779
+ "python3",
1780
+ "tools/loom_flow.py",
1781
+ "runtime-evidence",
1782
+ "--target",
1783
+ "examples/new-project",
1784
+ "--item",
1785
+ "INIT-0001",
1786
+ ],
1787
+ {"pass"},
1788
+ ),
1789
+ (
1790
+ "state-check",
1791
+ ["python3", "tools/loom_flow.py", "state-check", "--target", "examples/new-project", "--item", "INIT-0001"],
1792
+ {"pass"},
1793
+ ),
1794
+ (
1795
+ "flow-pre-review",
1796
+ [
1797
+ "python3",
1798
+ "tools/loom_flow.py",
1799
+ "flow",
1800
+ "pre-review",
1801
+ "--target",
1802
+ "examples/new-project",
1803
+ "--item",
1804
+ "INIT-0001",
1805
+ ],
1806
+ {"pass", "block", "fallback"},
1807
+ ),
1808
+ (
1809
+ "flow-review",
1810
+ [
1811
+ "python3",
1812
+ "tools/loom_flow.py",
1813
+ "flow",
1814
+ "review",
1815
+ "--target",
1816
+ "examples/new-project",
1817
+ "--item",
1818
+ "INIT-0001",
1819
+ ],
1820
+ {"pass", "block", "fallback"},
1821
+ ),
1822
+ (
1823
+ "flow-resume",
1824
+ [
1825
+ "python3",
1826
+ "tools/loom_flow.py",
1827
+ "flow",
1828
+ "resume",
1829
+ "--target",
1830
+ "examples/new-project",
1831
+ "--item",
1832
+ "INIT-0001",
1833
+ ],
1834
+ {"pass"},
1835
+ ),
1836
+ (
1837
+ "flow-handoff",
1838
+ [
1839
+ "python3",
1840
+ "tools/loom_flow.py",
1841
+ "flow",
1842
+ "handoff",
1843
+ "--target",
1844
+ "examples/new-project",
1845
+ "--item",
1846
+ "INIT-0001",
1847
+ ],
1848
+ {"pass", "block"},
1849
+ ),
1850
+ (
1851
+ "flow-merge-ready",
1852
+ [
1853
+ "python3",
1854
+ "tools/loom_flow.py",
1855
+ "flow",
1856
+ "merge-ready",
1857
+ "--target",
1858
+ "examples/new-project",
1859
+ "--item",
1860
+ "INIT-0001",
1861
+ ],
1862
+ {"pass", "block", "fallback"},
1863
+ ),
1864
+ (
1865
+ "admission",
1866
+ ["python3", "tools/loom_flow.py", "checkpoint", "admission", "--target", "examples/new-project", "--item", "INIT-0001"],
1867
+ {"pass"},
1868
+ ),
1869
+ (
1870
+ "build",
1871
+ ["python3", "tools/loom_flow.py", "checkpoint", "build", "--target", "examples/new-project", "--item", "INIT-0001"],
1872
+ {"pass", "block", "fallback"},
1873
+ ),
1874
+ (
1875
+ "merge",
1876
+ ["python3", "tools/loom_flow.py", "checkpoint", "merge", "--target", "examples/new-project", "--item", "INIT-0001"],
1877
+ {"pass", "block", "fallback"},
1878
+ ),
1879
+ (
1880
+ "locate",
1881
+ ["python3", "tools/loom_flow.py", "workspace", "locate", "--target", "examples/new-project", "--item", "INIT-0001"],
1882
+ {"pass"},
1883
+ ),
1884
+ (
1885
+ "review-read",
1886
+ ["python3", "tools/loom_flow.py", "review", "read", "--target", "examples/new-project", "--item", "INIT-0001"],
1887
+ {"pass"},
1888
+ ),
1889
+ (
1890
+ "host-lifecycle",
1891
+ ["python3", "tools/loom_flow.py", "host-lifecycle", "--target", "examples/new-project", "--item", "INIT-0001"],
1892
+ {"pass"},
1893
+ ),
1894
+ (
1895
+ "closeout-check",
1896
+ ["python3", "tools/loom_flow.py", "closeout", "check", "--target", ".", "--skip-gate"],
1897
+ {"pass"},
1898
+ ),
1899
+ (
1900
+ "closeout-sync",
1901
+ ["python3", "tools/loom_flow.py", "closeout", "sync", "--target", ".", "--skip-gate"],
1902
+ {"pass"},
1903
+ ),
1904
+ (
1905
+ "reconciliation-audit",
1906
+ ["python3", "tools/loom_flow.py", "reconciliation", "audit", "--target", "."],
1907
+ {"block"},
1908
+ ),
1909
+ (
1910
+ "purity",
1911
+ ["python3", "tools/loom_flow.py", "purity-check", "--target", "examples/new-project", "--item", "INIT-0001"],
1912
+ {"pass"},
1913
+ ),
1914
+ ]
1915
+ for label, args, allowed_results in demo_commands:
1916
+ payload, error = load_command_json(root, args)
1917
+ if error:
1918
+ failures.append(Failure("daily-execution-cli", f"`{label}` command failed: {error}"))
1919
+ continue
1920
+ result = payload.get("result")
1921
+ if result not in allowed_results:
1922
+ failures.append(
1923
+ Failure(
1924
+ "daily-execution-cli",
1925
+ f"`{label}` returned unexpected result `{result}`",
1926
+ )
1927
+ )
1928
+ if label == "runtime-state-init":
1929
+ if payload.get("command") != "runtime-state":
1930
+ failures.append(Failure("daily-execution-cli", "`loom-init runtime-state` must report `command: runtime-state`"))
1931
+ require_runtime_state_payload(
1932
+ failures,
1933
+ category="daily-execution-cli",
1934
+ context="`loom-init runtime-state`",
1935
+ payload=payload.get("runtime_state"),
1936
+ expected_scene="repo-local-demo",
1937
+ expected_carrier="repo-local-wrapper",
1938
+ allowed_results={"pass"},
1939
+ )
1940
+ if label == "runtime-state-flow":
1941
+ if payload.get("command") != "runtime-state":
1942
+ failures.append(Failure("daily-execution-cli", "`loom-flow runtime-state` must report `command: runtime-state`"))
1943
+ require_runtime_state_payload(
1944
+ failures,
1945
+ category="daily-execution-cli",
1946
+ context="`loom-flow runtime-state`",
1947
+ payload=payload.get("runtime_state"),
1948
+ expected_scene="repo-local-demo",
1949
+ expected_carrier="repo-local-wrapper",
1950
+ allowed_results={"pass"},
1951
+ )
1952
+ if label == "runtime-evidence":
1953
+ require_runtime_state_payload(
1954
+ failures,
1955
+ category="daily-execution-cli",
1956
+ context="`runtime-evidence`",
1957
+ payload=payload.get("runtime_state"),
1958
+ expected_scene="repo-local-demo",
1959
+ expected_carrier="repo-local-wrapper",
1960
+ allowed_results={"pass"},
1961
+ )
1962
+ if label == "state-check":
1963
+ require_runtime_state_payload(
1964
+ failures,
1965
+ category="daily-execution-cli",
1966
+ context="`state-check`",
1967
+ payload=payload.get("runtime_state"),
1968
+ expected_scene="repo-local-demo",
1969
+ expected_carrier="repo-local-wrapper",
1970
+ allowed_results={"pass"},
1971
+ )
1972
+ if label == "flow-pre-review":
1973
+ require_runtime_state_payload(
1974
+ failures,
1975
+ category="daily-execution-cli",
1976
+ context="`flow pre-review`",
1977
+ payload=payload.get("runtime_state"),
1978
+ expected_scene="repo-local-demo",
1979
+ expected_carrier="repo-local-wrapper",
1980
+ allowed_results={"pass"},
1981
+ )
1982
+ steps = payload.get("steps")
1983
+ if isinstance(steps, list):
1984
+ step_names = [step.get("name") for step in steps if isinstance(step, dict)]
1985
+ if step_names != [
1986
+ "runtime-state",
1987
+ "fact-chain",
1988
+ "state-check",
1989
+ "runtime-evidence",
1990
+ "checkpoint-admission",
1991
+ "workspace-locate",
1992
+ ]:
1993
+ failures.append(
1994
+ Failure(
1995
+ "daily-execution-cli",
1996
+ "`flow pre-review` must run runtime-state, fact-chain, state-check, runtime-evidence, checkpoint-admission, and workspace-locate in order",
1997
+ )
1998
+ )
1999
+ if label == "purity":
2000
+ purity = payload.get("purity")
2001
+ if not isinstance(purity, dict):
2002
+ failures.append(Failure("daily-execution-cli", "`purity` output must include a `purity` object"))
2003
+ continue
2004
+ require_runtime_state_payload(
2005
+ failures,
2006
+ category="daily-execution-cli",
2007
+ context="`purity`",
2008
+ payload=payload.get("runtime_state"),
2009
+ expected_scene="repo-local-demo",
2010
+ expected_carrier="repo-local-wrapper",
2011
+ allowed_results={"pass"},
2012
+ )
2013
+ scope_assessment = purity.get("scope_assessment")
2014
+ if not isinstance(scope_assessment, dict):
2015
+ failures.append(Failure("daily-execution-cli", "`purity` output must include `scope_assessment`"))
2016
+ continue
2017
+ mode = scope_assessment.get("mode")
2018
+ if mode not in {"constrained", "unconstrained"}:
2019
+ failures.append(
2020
+ Failure("daily-execution-cli", "`scope_assessment.mode` must be `constrained` or `unconstrained`")
2021
+ )
2022
+ if label == "flow-resume":
2023
+ if payload.get("command") != "flow":
2024
+ failures.append(Failure("daily-execution-cli", "`flow resume` must report `command: flow`"))
2025
+ if payload.get("operation") != "resume":
2026
+ failures.append(Failure("daily-execution-cli", "`flow resume` must report `operation: resume`"))
2027
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
2028
+ failures.append(Failure("daily-execution-cli", "`flow resume` must include a non-empty `summary`"))
2029
+ if not isinstance(payload.get("missing_inputs"), list):
2030
+ failures.append(Failure("daily-execution-cli", "`flow resume` must include `missing_inputs`"))
2031
+ if payload.get("fallback_to") not in {None, "admission"}:
2032
+ failures.append(Failure("daily-execution-cli", "`flow resume` fallback must be `null` or `admission`"))
2033
+ for key in ("item", "workspace", "recovery", "checkpoint", "state_check"):
2034
+ if not isinstance(payload.get(key), dict):
2035
+ failures.append(Failure("daily-execution-cli", f"`flow resume` must include `{key}`"))
2036
+ require_runtime_state_payload(
2037
+ failures,
2038
+ category="daily-execution-cli",
2039
+ context="`flow resume`",
2040
+ payload=payload.get("runtime_state"),
2041
+ expected_scene="repo-local-demo",
2042
+ expected_carrier="repo-local-wrapper",
2043
+ allowed_results={"pass"},
2044
+ )
2045
+ require_governance_surface(
2046
+ failures,
2047
+ category="daily-execution-cli",
2048
+ context="`flow resume`",
2049
+ payload=payload,
2050
+ )
2051
+ steps = payload.get("steps")
2052
+ if not isinstance(steps, list):
2053
+ failures.append(Failure("daily-execution-cli", "`flow resume` must include `steps`"))
2054
+ continue
2055
+ step_names = [step.get("name") for step in steps if isinstance(step, dict)]
2056
+ if step_names != ["runtime-state", "fact-chain", "state-check", "workspace-locate"]:
2057
+ failures.append(
2058
+ Failure(
2059
+ "daily-execution-cli",
2060
+ "`flow resume` must run runtime-state, fact-chain, state-check, and workspace-locate in order",
2061
+ )
2062
+ )
2063
+ recovery = payload.get("recovery")
2064
+ if isinstance(recovery, dict):
2065
+ for field in ("current_stop", "next_step", "blockers", "latest_validation_summary"):
2066
+ value = recovery.get(field)
2067
+ if not isinstance(value, str) or not value:
2068
+ failures.append(
2069
+ Failure("daily-execution-cli", f"`flow resume` recovery must include non-empty `{field}`")
2070
+ )
2071
+ checkpoint = payload.get("checkpoint")
2072
+ if isinstance(checkpoint, dict):
2073
+ if checkpoint.get("normalized") not in {"admission", "build", "merge", "retired"}:
2074
+ failures.append(
2075
+ Failure("daily-execution-cli", "`flow resume` checkpoint must include a known normalized value")
2076
+ )
2077
+ state_check = payload.get("state_check")
2078
+ if isinstance(state_check, dict):
2079
+ if state_check.get("result") not in {"pass", "block"}:
2080
+ failures.append(
2081
+ Failure("daily-execution-cli", "`flow resume` state_check.result must be `pass` or `block`")
2082
+ )
2083
+ if not isinstance(state_check.get("checks"), dict):
2084
+ failures.append(Failure("daily-execution-cli", "`flow resume` must include `state_check.checks`"))
2085
+ if label == "flow-handoff":
2086
+ if payload.get("command") != "flow":
2087
+ failures.append(Failure("daily-execution-cli", "`flow handoff` must report `command: flow`"))
2088
+ if payload.get("operation") != "handoff":
2089
+ failures.append(Failure("daily-execution-cli", "`flow handoff` must report `operation: handoff`"))
2090
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
2091
+ failures.append(Failure("daily-execution-cli", "`flow handoff` must include a non-empty `summary`"))
2092
+ if not isinstance(payload.get("missing_inputs"), list):
2093
+ failures.append(Failure("daily-execution-cli", "`flow handoff` must include `missing_inputs`"))
2094
+ if payload.get("fallback_to") not in {None, "admission"}:
2095
+ failures.append(Failure("daily-execution-cli", "`flow handoff` fallback must be `null` or `admission`"))
2096
+ for key in ("item", "workspace", "checkpoint", "state_check"):
2097
+ if not isinstance(payload.get(key), dict):
2098
+ failures.append(Failure("daily-execution-cli", f"`flow handoff` must include `{key}`"))
2099
+ require_runtime_state_payload(
2100
+ failures,
2101
+ category="daily-execution-cli",
2102
+ context="`flow handoff`",
2103
+ payload=payload.get("runtime_state"),
2104
+ expected_scene="repo-local-demo",
2105
+ expected_carrier="repo-local-wrapper",
2106
+ allowed_results={"pass"},
2107
+ )
2108
+ for key in (
2109
+ "recovery_entry",
2110
+ "status_surface",
2111
+ "current_stop",
2112
+ "next_step",
2113
+ "blockers",
2114
+ "latest_validation_summary",
2115
+ ):
2116
+ value = payload.get(key)
2117
+ if not isinstance(value, str) or not value:
2118
+ failures.append(Failure("daily-execution-cli", f"`flow handoff` must include non-empty `{key}`"))
2119
+ if payload.get("fallback_target") not in {None, "admission"}:
2120
+ failures.append(Failure("daily-execution-cli", "`flow handoff` fallback_target must be `null` or `admission`"))
2121
+ writeback_fields = payload.get("writeback_fields")
2122
+ if writeback_fields != [
2123
+ "current_stop",
2124
+ "next_step",
2125
+ "blockers",
2126
+ "latest_validation_summary",
2127
+ ]:
2128
+ failures.append(
2129
+ Failure(
2130
+ "daily-execution-cli",
2131
+ "`flow handoff` must report the stable writeback field list in order",
2132
+ )
2133
+ )
2134
+ steps = payload.get("steps")
2135
+ if not isinstance(steps, list):
2136
+ failures.append(Failure("daily-execution-cli", "`flow handoff` must include `steps`"))
2137
+ continue
2138
+ step_names = [step.get("name") for step in steps if isinstance(step, dict)]
2139
+ if step_names != ["runtime-state", "fact-chain", "state-check", "workspace-locate"]:
2140
+ failures.append(
2141
+ Failure(
2142
+ "daily-execution-cli",
2143
+ "`flow handoff` must run runtime-state, fact-chain, state-check, and workspace-locate in order",
2144
+ )
2145
+ )
2146
+ state_check = payload.get("state_check")
2147
+ if isinstance(state_check, dict):
2148
+ if state_check.get("result") not in {"pass", "block"}:
2149
+ failures.append(
2150
+ Failure("daily-execution-cli", "`flow handoff` state_check.result must be `pass` or `block`")
2151
+ )
2152
+ if not isinstance(state_check.get("checks"), dict):
2153
+ failures.append(Failure("daily-execution-cli", "`flow handoff` must include `state_check.checks`"))
2154
+ if label == "flow-review":
2155
+ if payload.get("command") != "flow":
2156
+ failures.append(Failure("daily-execution-cli", "`flow review` must report `command: flow`"))
2157
+ if payload.get("operation") != "review":
2158
+ failures.append(Failure("daily-execution-cli", "`flow review` must report `operation: review`"))
2159
+ for key in ("item", "state_check", "runtime_evidence", "build_checkpoint", "review", "current_checkpoint", "repo_specific_requirements"):
2160
+ if not isinstance(payload.get(key), dict):
2161
+ failures.append(Failure("daily-execution-cli", f"`flow review` must include `{key}`"))
2162
+ require_runtime_state_payload(
2163
+ failures,
2164
+ category="daily-execution-cli",
2165
+ context="`flow review`",
2166
+ payload=payload.get("runtime_state"),
2167
+ expected_scene="repo-local-demo",
2168
+ expected_carrier="repo-local-wrapper",
2169
+ allowed_results={"pass"},
2170
+ )
2171
+ steps = payload.get("steps")
2172
+ if not isinstance(steps, list):
2173
+ failures.append(Failure("daily-execution-cli", "`flow review` must include `steps`"))
2174
+ continue
2175
+ step_names = [step.get("name") for step in steps if isinstance(step, dict)]
2176
+ if step_names != [
2177
+ "runtime-state",
2178
+ "fact-chain",
2179
+ "state-check",
2180
+ "runtime-evidence",
2181
+ "checkpoint-build",
2182
+ "review-entry",
2183
+ ]:
2184
+ failures.append(
2185
+ Failure(
2186
+ "daily-execution-cli",
2187
+ "`flow review` must run runtime-state, fact-chain, state-check, runtime-evidence, checkpoint-build, and review-entry in order",
2188
+ )
2189
+ )
2190
+ review = payload.get("review")
2191
+ if isinstance(review, dict):
2192
+ require_review_record_contract(
2193
+ failures,
2194
+ category="daily-execution-cli",
2195
+ context="`flow review` review.record",
2196
+ payload=review.get("record"),
2197
+ )
2198
+ require_repo_specific_requirements_payload(
2199
+ failures,
2200
+ category="daily-execution-cli",
2201
+ context="`flow review` repo_specific_requirements",
2202
+ payload=payload.get("repo_specific_requirements"),
2203
+ expected_surface="review",
2204
+ )
2205
+ if label == "review-read":
2206
+ if payload.get("command") != "review":
2207
+ failures.append(Failure("daily-execution-cli", "`review read` must report `command: review`"))
2208
+ if payload.get("operation") != "read":
2209
+ failures.append(Failure("daily-execution-cli", "`review read` must report `operation: read`"))
2210
+ review = payload.get("review")
2211
+ if not isinstance(review, dict):
2212
+ failures.append(Failure("daily-execution-cli", "`review read` must include a `review` object"))
2213
+ elif not isinstance(review.get("record"), dict):
2214
+ failures.append(Failure("daily-execution-cli", "`review read` must include `review.record`"))
2215
+ else:
2216
+ require_review_record_contract(
2217
+ failures,
2218
+ category="daily-execution-cli",
2219
+ context="`review read` review.record",
2220
+ payload=review.get("record"),
2221
+ )
2222
+ if label == "host-lifecycle":
2223
+ if payload.get("command") != "host-lifecycle":
2224
+ failures.append(Failure("daily-execution-cli", "`host-lifecycle` must report `command: host-lifecycle`"))
2225
+ require_host_lifecycle_payload(
2226
+ failures,
2227
+ category="daily-execution-cli",
2228
+ context="`host-lifecycle`",
2229
+ payload=payload,
2230
+ )
2231
+ if label in {"closeout-check", "closeout-sync"}:
2232
+ if payload.get("command") != "closeout":
2233
+ failures.append(Failure("daily-execution-cli", f"`{label}` must report `command: closeout`"))
2234
+ expected_operation = "check" if label == "closeout-check" else "sync"
2235
+ if payload.get("operation") != expected_operation:
2236
+ failures.append(
2237
+ Failure("daily-execution-cli", f"`{label}` must report `operation: {expected_operation}`")
2238
+ )
2239
+ repo = payload.get("repo")
2240
+ if not isinstance(repo, dict):
2241
+ failures.append(Failure("daily-execution-cli", f"`{label}` must include `repo`"))
2242
+ else:
2243
+ if not isinstance(repo.get("owner"), str) or not repo.get("owner"):
2244
+ failures.append(Failure("daily-execution-cli", f"`{label}` must include `repo.owner`"))
2245
+ if not isinstance(repo.get("name"), str) or not repo.get("name"):
2246
+ failures.append(Failure("daily-execution-cli", f"`{label}` must include `repo.name`"))
2247
+ require_runtime_state_payload(
2248
+ failures,
2249
+ category="daily-execution-cli",
2250
+ context=f"`{label}`",
2251
+ payload=payload.get("runtime_state"),
2252
+ expected_scene="repo-local-demo",
2253
+ expected_carrier="repo-local-wrapper",
2254
+ allowed_results={"pass"},
2255
+ )
2256
+ require_closeout_reconciliation_contract(
2257
+ failures,
2258
+ category="daily-execution-cli",
2259
+ context=f"`{label}`",
2260
+ payload=payload,
2261
+ )
2262
+ require_repo_specific_requirements_payload(
2263
+ failures,
2264
+ category="daily-execution-cli",
2265
+ context=f"`{label}` repo_specific_requirements",
2266
+ payload=payload.get("repo_specific_requirements"),
2267
+ expected_surface="closeout",
2268
+ )
2269
+ if label == "reconciliation-audit":
2270
+ if payload.get("command") != "reconciliation":
2271
+ failures.append(Failure("daily-execution-cli", "`reconciliation audit` must report `command: reconciliation`"))
2272
+ if payload.get("operation") != "audit":
2273
+ failures.append(Failure("daily-execution-cli", "`reconciliation audit` must report `operation: audit`"))
2274
+ require_runtime_state_payload(
2275
+ failures,
2276
+ category="daily-execution-cli",
2277
+ context="`reconciliation audit`",
2278
+ payload=payload.get("runtime_state"),
2279
+ expected_scene="repo-local-demo",
2280
+ expected_carrier="repo-local-wrapper",
2281
+ allowed_results={"pass"},
2282
+ )
2283
+ require_reconciliation_payload(
2284
+ failures,
2285
+ category="daily-execution-cli",
2286
+ context="`reconciliation audit`",
2287
+ payload=payload,
2288
+ )
2289
+ if label == "flow-merge-ready":
2290
+ if payload.get("command") != "flow":
2291
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must report `command: flow`"))
2292
+ if payload.get("operation") != "merge-ready":
2293
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must report `operation: merge-ready`"))
2294
+ if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
2295
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include a non-empty `summary`"))
2296
+ if not isinstance(payload.get("missing_inputs"), list):
2297
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `missing_inputs`"))
2298
+ if payload.get("fallback_to") not in {None, "admission", "build", "merge", "retired"}:
2299
+ failures.append(
2300
+ Failure(
2301
+ "daily-execution-cli",
2302
+ "`flow merge-ready` fallback must be `null` or a known checkpoint",
2303
+ )
2304
+ )
2305
+ for key in ("item", "runtime_state", "state_check", "runtime_evidence", "build_checkpoint", "merge_checkpoint", "current_checkpoint", "repo_specific_requirements"):
2306
+ if not isinstance(payload.get(key), dict):
2307
+ failures.append(Failure("daily-execution-cli", f"`flow merge-ready` must include `{key}`"))
2308
+ require_runtime_state_payload(
2309
+ failures,
2310
+ category="daily-execution-cli",
2311
+ context="`flow merge-ready`",
2312
+ payload=payload.get("runtime_state"),
2313
+ expected_scene="repo-local-demo",
2314
+ expected_carrier="repo-local-wrapper",
2315
+ allowed_results={"pass"},
2316
+ )
2317
+ if not isinstance(payload.get("current_lane"), str) or not payload.get("current_lane"):
2318
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `current_lane`"))
2319
+ if not isinstance(payload.get("latest_validation_summary"), str) or not payload.get("latest_validation_summary"):
2320
+ failures.append(
2321
+ Failure("daily-execution-cli", "`flow merge-ready` must include `latest_validation_summary`")
2322
+ )
2323
+ steps = payload.get("steps")
2324
+ if not isinstance(steps, list):
2325
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `steps`"))
2326
+ continue
2327
+ step_names = [step.get("name") for step in steps if isinstance(step, dict)]
2328
+ if step_names != [
2329
+ "runtime-state",
2330
+ "fact-chain",
2331
+ "state-check",
2332
+ "runtime-evidence",
2333
+ "checkpoint-build",
2334
+ "checkpoint-merge",
2335
+ ]:
2336
+ failures.append(
2337
+ Failure(
2338
+ "daily-execution-cli",
2339
+ "`flow merge-ready` must run runtime-state, fact-chain, state-check, runtime-evidence, checkpoint-build, and checkpoint-merge in order",
2340
+ )
2341
+ )
2342
+ state_check = payload.get("state_check")
2343
+ if isinstance(state_check, dict):
2344
+ if state_check.get("result") not in {"pass", "block"}:
2345
+ failures.append(
2346
+ Failure("daily-execution-cli", "`flow merge-ready` state_check.result must be `pass` or `block`")
2347
+ )
2348
+ if not isinstance(state_check.get("checks"), dict):
2349
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `state_check.checks`"))
2350
+ runtime_evidence = payload.get("runtime_evidence")
2351
+ if isinstance(runtime_evidence, dict):
2352
+ for field in ("run_entry", "logs_entry", "diagnostics_entry", "verification_entry", "lane_entry"):
2353
+ if not isinstance(runtime_evidence.get(field), dict):
2354
+ failures.append(
2355
+ Failure("daily-execution-cli", f"`flow merge-ready` must include runtime evidence field `{field}`")
2356
+ )
2357
+ for key in ("build_checkpoint", "merge_checkpoint"):
2358
+ checkpoint = payload.get(key)
2359
+ if isinstance(checkpoint, dict):
2360
+ if checkpoint.get("result") not in {"pass", "block", "fallback"}:
2361
+ failures.append(
2362
+ Failure(
2363
+ "daily-execution-cli",
2364
+ f"`flow merge-ready` {key}.result must be `pass`, `block`, or `fallback`",
2365
+ )
2366
+ )
2367
+ if not isinstance(checkpoint.get("missing_inputs"), list):
2368
+ failures.append(
2369
+ Failure("daily-execution-cli", f"`flow merge-ready` {key} must include `missing_inputs`")
2370
+ )
2371
+ merge_checkpoint = payload.get("merge_checkpoint")
2372
+ if isinstance(merge_checkpoint, dict) and not isinstance(merge_checkpoint.get("pr_template"), dict):
2373
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `merge_checkpoint.pr_template`"))
2374
+ require_repo_specific_requirements_payload(
2375
+ failures,
2376
+ category="daily-execution-cli",
2377
+ context="`flow merge-ready` repo_specific_requirements",
2378
+ payload=payload.get("repo_specific_requirements"),
2379
+ expected_surface="merge_ready",
2380
+ )
2381
+ if payload.get("result") != "fallback":
2382
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must return `fallback` for the bootstrap demo"))
2383
+ if payload.get("fallback_to") != "admission":
2384
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` must fall back to `admission` for the bootstrap demo"))
2385
+ if isinstance(payload.get("build_checkpoint"), dict) and payload["build_checkpoint"].get("fallback_to") != "admission":
2386
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` build checkpoint must fall back to `admission` for the bootstrap demo"))
2387
+ if isinstance(payload.get("merge_checkpoint"), dict) and payload["merge_checkpoint"].get("fallback_to") != "admission":
2388
+ failures.append(Failure("daily-execution-cli", "`flow merge-ready` merge checkpoint must fall back to `admission` for the bootstrap demo"))
2389
+
2390
+ with tempfile.TemporaryDirectory(prefix="loom-check-review-run-") as tmp:
2391
+ source_snapshot = Path(tmp) / "source-snapshot"
2392
+ review_target = Path(tmp) / "new-project"
2393
+ fake_bin = Path(tmp) / "bin"
2394
+ fake_bin.mkdir(parents=True, exist_ok=True)
2395
+ shutil.copytree(root, source_snapshot, ignore=shutil.ignore_patterns(".git", ".DS_Store", "__pycache__"))
2396
+
2397
+ def prepare_review_target(target: Path, label: str) -> bool:
2398
+ shutil.copytree(source_snapshot, target)
2399
+ for args in (
2400
+ ["git", "init"],
2401
+ ["git", "config", "user.email", "loom-check@example.com"],
2402
+ ["git", "config", "user.name", "loom-check"],
2403
+ ):
2404
+ result = run_command(root, args, cwd=target)
2405
+ if result.returncode != 0:
2406
+ detail = result.stderr.strip() or result.stdout.strip() or "git setup failed"
2407
+ failures.append(Failure("daily-execution-cli", f"`{label}` setup failed: {detail}"))
2408
+ return False
2409
+ payload, error = load_command_json(
2410
+ root,
2411
+ [
2412
+ "python3",
2413
+ "tools/loom_init.py",
2414
+ "bootstrap",
2415
+ "--target",
2416
+ ".",
2417
+ "--write",
2418
+ "--force",
2419
+ "--verify",
2420
+ "--install-pr-template",
2421
+ ],
2422
+ cwd=target,
2423
+ )
2424
+ if error:
2425
+ failures.append(Failure("daily-execution-cli", f"`{label}` bootstrap failed: {error}"))
2426
+ return False
2427
+ verification = payload.get("verification")
2428
+ if not isinstance(verification, dict) or verification.get("ok") is not True:
2429
+ failures.append(Failure("daily-execution-cli", f"`{label}` bootstrap must verify successfully"))
2430
+ return False
2431
+ for args in (
2432
+ ["git", "add", "."],
2433
+ ["git", "add", "-f", ".loom"],
2434
+ ["git", "commit", "-m", "review-run baseline"],
2435
+ ):
2436
+ result = run_command(root, args, cwd=target)
2437
+ if result.returncode != 0:
2438
+ detail = result.stderr.strip() or result.stdout.strip() or "git baseline commit failed"
2439
+ failures.append(Failure("daily-execution-cli", f"`{label}` setup failed: {detail}"))
2440
+ return False
2441
+ return True
2442
+
2443
+ prepare_review_target(review_target, "review run positive chain")
2444
+ write_fake_codex(fake_bin / "codex", mode="success")
2445
+ success_env = prepend_path_env(fake_bin)
2446
+
2447
+ payload, error = load_command_json(
2448
+ root,
2449
+ [
2450
+ "python3",
2451
+ "tools/loom_flow.py",
2452
+ "review",
2453
+ "run",
2454
+ "--target",
2455
+ str(review_target),
2456
+ "--item",
2457
+ "INIT-0001",
2458
+ ],
2459
+ env=success_env,
2460
+ )
2461
+ if error:
2462
+ failures.append(Failure("daily-execution-cli", f"`review run` positive chain failed: {error}"))
2463
+ else:
2464
+ require_review_run_payload(
2465
+ failures,
2466
+ category="daily-execution-cli",
2467
+ context="`review run` positive chain",
2468
+ payload=payload,
2469
+ expected_result={"pass"},
2470
+ )
2471
+
2472
+ engine_missing_target = Path(tmp) / "engine-missing"
2473
+ prepare_review_target(engine_missing_target, "review run engine unavailable")
2474
+ payload, error = load_command_json(
2475
+ root,
2476
+ [
2477
+ "python3",
2478
+ "tools/loom_flow.py",
2479
+ "review",
2480
+ "run",
2481
+ "--target",
2482
+ str(engine_missing_target),
2483
+ "--item",
2484
+ "INIT-0001",
2485
+ ],
2486
+ env={"PATH": "/usr/bin:/bin"},
2487
+ )
2488
+ if error:
2489
+ failures.append(Failure("daily-execution-cli", f"`review run` engine unavailable failed: {error}"))
2490
+ elif payload.get("result") != "block":
2491
+ failures.append(Failure("daily-execution-cli", "`review run` must block when the default engine is unavailable"))
2492
+ else:
2493
+ require_review_run_payload(
2494
+ failures,
2495
+ category="daily-execution-cli",
2496
+ context="`review run` engine unavailable",
2497
+ payload=payload,
2498
+ expected_result={"block"},
2499
+ )
2500
+ engine = payload.get("engine")
2501
+ if isinstance(engine, dict) and engine.get("failure_reason") != "engine_unavailable":
2502
+ failures.append(Failure("daily-execution-cli", "`review run` must report `engine_unavailable` when Codex is missing"))
2503
+ if payload.get("fallback_to") is not None:
2504
+ failures.append(Failure("daily-execution-cli", "`review run` must not convert engine failure into checkpoint fallback"))
2505
+
2506
+ schema_target = Path(tmp) / "schema-drift"
2507
+ prepare_review_target(schema_target, "review run schema drift")
2508
+ write_fake_codex(fake_bin / "codex", mode="schema_drift")
2509
+ payload, error = load_command_json(
2510
+ root,
2511
+ [
2512
+ "python3",
2513
+ "tools/loom_flow.py",
2514
+ "review",
2515
+ "run",
2516
+ "--target",
2517
+ str(schema_target),
2518
+ "--item",
2519
+ "INIT-0001",
2520
+ ],
2521
+ env=success_env,
2522
+ )
2523
+ if error:
2524
+ failures.append(Failure("daily-execution-cli", f"`review run` schema drift failed: {error}"))
2525
+ elif payload.get("result") != "block":
2526
+ failures.append(Failure("daily-execution-cli", "`review run` must block on schema drift"))
2527
+ else:
2528
+ require_review_run_payload(
2529
+ failures,
2530
+ category="daily-execution-cli",
2531
+ context="`review run` schema drift",
2532
+ payload=payload,
2533
+ expected_result={"block"},
2534
+ )
2535
+ engine = payload.get("engine")
2536
+ if isinstance(engine, dict) and engine.get("failure_reason") != "schema_drift":
2537
+ failures.append(Failure("daily-execution-cli", "`review run` must report `schema_drift` for invalid engine output"))
2538
+
2539
+ dirty_target = Path(tmp) / "tracked-edit"
2540
+ prepare_review_target(dirty_target, "review run tracked edit")
2541
+ write_fake_codex(fake_bin / "codex", mode="tracked_edit", tracked_edit_target=".loom/status/current.md")
2542
+ payload, error = load_command_json(
2543
+ root,
2544
+ [
2545
+ "python3",
2546
+ "tools/loom_flow.py",
2547
+ "review",
2548
+ "run",
2549
+ "--target",
2550
+ str(dirty_target),
2551
+ "--item",
2552
+ "INIT-0001",
2553
+ ],
2554
+ env=success_env,
2555
+ )
2556
+ if error:
2557
+ failures.append(Failure("daily-execution-cli", f"`review run` tracked edit failed: {error}"))
2558
+ elif payload.get("result") != "block":
2559
+ failures.append(Failure("daily-execution-cli", "`review run` must block when engine modifies tracked repo content"))
2560
+ else:
2561
+ require_review_run_payload(
2562
+ failures,
2563
+ category="daily-execution-cli",
2564
+ context="`review run` tracked edit",
2565
+ payload=payload,
2566
+ expected_result={"block"},
2567
+ )
2568
+ engine = payload.get("engine")
2569
+ if isinstance(engine, dict) and engine.get("failure_reason") != "repo_diff_detected":
2570
+ failures.append(Failure("daily-execution-cli", "`review run` must report `repo_diff_detected` when tracked files change"))
2571
+
2572
+ with tempfile.TemporaryDirectory(prefix="loom-check-flow-") as tmp:
2573
+ lifecycle_target = Path(tmp) / "new-project"
2574
+ shutil.copytree(example_target, lifecycle_target)
2575
+ temp_root = lifecycle_target / ".loom/flow/tmp"
2576
+ temp_root.mkdir(parents=True, exist_ok=True)
2577
+ (temp_root / "sentinel.txt").write_text("temp\n", encoding="utf-8")
2578
+
2579
+ for operation in ("create", "cleanup", "retire"):
2580
+ payload, error = load_command_json(
2581
+ root,
2582
+ [
2583
+ "python3",
2584
+ "tools/loom_flow.py",
2585
+ "workspace",
2586
+ operation,
2587
+ "--target",
2588
+ str(lifecycle_target),
2589
+ "--item",
2590
+ "INIT-0001",
2591
+ ],
2592
+ )
2593
+ if error:
2594
+ failures.append(Failure("daily-execution-cli", f"`workspace {operation}` failed: {error}"))
2595
+ continue
2596
+ if payload.get("result") != "pass":
2597
+ failures.append(
2598
+ Failure(
2599
+ "daily-execution-cli",
2600
+ f"`workspace {operation}` must pass on a clean temp copy, got `{payload.get('result')}`",
2601
+ )
2602
+ )
2603
+
2604
+ locate_payload, error = load_command_json(
2605
+ root,
2606
+ [
2607
+ "python3",
2608
+ "tools/loom_flow.py",
2609
+ "workspace",
2610
+ "locate",
2611
+ "--target",
2612
+ str(lifecycle_target),
2613
+ "--item",
2614
+ "INIT-0001",
2615
+ ],
2616
+ )
2617
+ if error:
2618
+ failures.append(Failure("daily-execution-cli", f"`workspace locate` after retire failed: {error}"))
2619
+ elif (
2620
+ not isinstance(locate_payload.get("checkpoint"), dict)
2621
+ or locate_payload["checkpoint"].get("normalized") != "retired"
2622
+ ):
2623
+ failures.append(Failure("daily-execution-cli", "`workspace retire` must leave the copied sample in `retired` state"))
2624
+
2625
+ with tempfile.TemporaryDirectory(prefix="loom-check-authoring-") as tmp:
2626
+ authoring_target = Path(tmp) / "new-project"
2627
+ shutil.copytree(example_target, authoring_target)
2628
+
2629
+ payload, error = load_command_json(
2630
+ root,
2631
+ [
2632
+ "python3",
2633
+ "tools/loom_flow.py",
2634
+ "recovery",
2635
+ "writeback",
2636
+ "--target",
2637
+ str(authoring_target),
2638
+ "--item",
2639
+ "INIT-0001",
2640
+ "--current-stop",
2641
+ "Bootstrap review has started.",
2642
+ "--next-step",
2643
+ "Record the first formal review conclusion.",
2644
+ "--latest-validation-summary",
2645
+ "Bootstrap artifacts verified and ready for semantic review.",
2646
+ ],
2647
+ )
2648
+ if error:
2649
+ failures.append(Failure("daily-execution-cli", f"`recovery writeback` failed: {error}"))
2650
+ elif payload.get("result") != "pass":
2651
+ failures.append(Failure("daily-execution-cli", "`recovery writeback` must pass on a clean temp copy"))
2652
+
2653
+ payload, error = load_command_json(
2654
+ root,
2655
+ [
2656
+ "python3",
2657
+ "tools/loom_flow.py",
2658
+ "work-item",
2659
+ "create",
2660
+ "--target",
2661
+ str(authoring_target),
2662
+ "--item",
2663
+ "NEXT-0001",
2664
+ "--goal",
2665
+ "Validate work item authoring",
2666
+ "--scope",
2667
+ "Limit changes to `.loom/` artifacts for this temp check",
2668
+ "--execution-path",
2669
+ "execution/support",
2670
+ "--workspace-entry",
2671
+ ".",
2672
+ "--validation-entry",
2673
+ "python3 .loom/bin/loom_init.py verify --target .",
2674
+ "--closing-condition",
2675
+ "The authored work item can be activated and read mechanically.",
2676
+ "--init-recovery",
2677
+ "--activate",
2678
+ ],
2679
+ )
2680
+ if error:
2681
+ failures.append(Failure("daily-execution-cli", f"`work-item create` failed: {error}"))
2682
+ elif payload.get("result") != "pass":
2683
+ failures.append(Failure("daily-execution-cli", "`work-item create --activate` must pass on a clean temp copy"))
2684
+
2685
+ payload, error = load_command_json(
2686
+ root,
2687
+ [
2688
+ "python3",
2689
+ "tools/loom_flow.py",
2690
+ "work-item",
2691
+ "update",
2692
+ "--target",
2693
+ str(authoring_target),
2694
+ "--item",
2695
+ "NEXT-0001",
2696
+ "--scope",
2697
+ "Keep the temp authoring check constrained to `.loom/` files",
2698
+ "--add-artifact",
2699
+ ".loom/reviews/NEXT-0001.json",
2700
+ ],
2701
+ )
2702
+ if error:
2703
+ failures.append(Failure("daily-execution-cli", f"`work-item update` failed: {error}"))
2704
+ elif payload.get("result") != "pass":
2705
+ failures.append(Failure("daily-execution-cli", "`work-item update` must pass on a clean temp copy"))
2706
+
2707
+ findings_path = authoring_target / ".loom" / "review-findings.json"
2708
+ findings_path.parent.mkdir(parents=True, exist_ok=True)
2709
+ findings_path.write_text(
2710
+ json.dumps(
2711
+ [
2712
+ {
2713
+ "id": "compat-block-1",
2714
+ "summary": "Formal review has not approved the item yet.",
2715
+ "severity": "block",
2716
+ "rebuttal": None,
2717
+ "disposition": {
2718
+ "status": "rejected",
2719
+ "summary": "The finding remains open until the missing approval signal is resolved."
2720
+ },
2721
+ },
2722
+ {
2723
+ "id": "compat-warn-1",
2724
+ "summary": "Re-run formal review after the missing approval signal is resolved.",
2725
+ "severity": "warn",
2726
+ "rebuttal": "A follow-up review will be recorded after the blocking issue is resolved.",
2727
+ "disposition": {
2728
+ "status": "deferred",
2729
+ "summary": "This follow-up stays open until the next formal review."
2730
+ },
2731
+ },
2732
+ ],
2733
+ ensure_ascii=False,
2734
+ indent=2,
2735
+ )
2736
+ + "\n",
2737
+ encoding="utf-8",
2738
+ )
2739
+
2740
+ payload, error = load_command_json(
2741
+ root,
2742
+ [
2743
+ "python3",
2744
+ "tools/loom_flow.py",
2745
+ "review",
2746
+ "record",
2747
+ "--target",
2748
+ str(authoring_target),
2749
+ "--item",
2750
+ "NEXT-0001",
2751
+ "--decision",
2752
+ "fallback",
2753
+ "--kind",
2754
+ "code_review",
2755
+ "--summary",
2756
+ "Formal review has not approved the item yet.",
2757
+ "--reviewer",
2758
+ "loom-check",
2759
+ "--fallback-to",
2760
+ "admission",
2761
+ "--findings-file",
2762
+ ".loom/review-findings.json",
2763
+ ],
2764
+ )
2765
+ if error:
2766
+ failures.append(Failure("daily-execution-cli", f"`review record` failed: {error}"))
2767
+ elif payload.get("result") != "pass":
2768
+ failures.append(Failure("daily-execution-cli", "`review record` must pass for an authored fallback decision"))
2769
+ else:
2770
+ review = payload.get("review")
2771
+ if isinstance(review, dict):
2772
+ require_review_record_contract(
2773
+ failures,
2774
+ category="daily-execution-cli",
2775
+ context="`review record` review.record",
2776
+ payload=review.get("record"),
2777
+ )
2778
+
2779
+ if shutil.which("git") is not None:
2780
+ with tempfile.TemporaryDirectory(prefix="loom-check-purity-") as tmp:
2781
+ dirty_target = Path(tmp) / "new-project"
2782
+ shutil.copytree(example_target, dirty_target)
2783
+ run_command(root, ["git", "init"], cwd=dirty_target)
2784
+ run_command(root, ["git", "config", "user.email", "loom-check@example.com"], cwd=dirty_target)
2785
+ run_command(root, ["git", "config", "user.name", "loom-check"], cwd=dirty_target)
2786
+ run_command(root, ["git", "add", "."], cwd=dirty_target)
2787
+ run_command(root, ["git", "commit", "-m", "baseline"], cwd=dirty_target)
2788
+ (dirty_target / "untriaged.txt").write_text("pending\n", encoding="utf-8")
2789
+ payload, error = load_command_json(
2790
+ root,
2791
+ [
2792
+ "python3",
2793
+ "tools/loom_flow.py",
2794
+ "purity-check",
2795
+ "--target",
2796
+ str(dirty_target),
2797
+ "--item",
2798
+ "INIT-0001",
2799
+ ],
2800
+ )
2801
+ if error:
2802
+ failures.append(Failure("daily-execution-cli", f"`purity-check` negative sample failed: {error}"))
2803
+ elif payload.get("result") != "block":
2804
+ failures.append(
2805
+ Failure(
2806
+ "daily-execution-cli",
2807
+ f"`purity-check` negative sample must block, got `{payload.get('result')}`",
2808
+ )
2809
+ )
2810
+ state_payload, error = load_command_json(
2811
+ root,
2812
+ [
2813
+ "python3",
2814
+ "tools/loom_flow.py",
2815
+ "state-check",
2816
+ "--target",
2817
+ str(dirty_target),
2818
+ "--item",
2819
+ "INIT-0001",
2820
+ ],
2821
+ )
2822
+ if error:
2823
+ failures.append(Failure("daily-execution-cli", f"`state-check` negative sample failed: {error}"))
2824
+ elif state_payload.get("result") != "block":
2825
+ failures.append(
2826
+ Failure(
2827
+ "daily-execution-cli",
2828
+ f"`state-check` negative sample must block, got `{state_payload.get('result')}`",
2829
+ )
2830
+ )
2831
+
2832
+ with tempfile.TemporaryDirectory(prefix="loom-check-runtime-state-") as tmp:
2833
+ tmp_root = Path(tmp)
2834
+ install_root = tmp_root / "installed" / "skills"
2835
+ target_root = tmp_root / "target"
2836
+ bootstrap_target = tmp_root / "bootstrapped-target"
2837
+ shutil.copytree(root / "skills", install_root)
2838
+ target_root.mkdir(parents=True, exist_ok=True)
2839
+ shutil.copytree(example_target, bootstrap_target)
2840
+
2841
+ payload, error = load_command_json(
2842
+ root,
2843
+ ["python3", str(install_root / "loom-init" / "scripts" / "loom-init.py"), "runtime-state", "--target", str(target_root)],
2844
+ )
2845
+ if error:
2846
+ failures.append(Failure("daily-execution-cli", f"`installed loom-init runtime-state` failed: {error}"))
2847
+ else:
2848
+ require_runtime_state_payload(
2849
+ failures,
2850
+ category="daily-execution-cli",
2851
+ context="`installed loom-init runtime-state`",
2852
+ payload=payload.get("runtime_state"),
2853
+ expected_scene="installed-runtime",
2854
+ expected_carrier="installed-skills-root",
2855
+ allowed_results={"pass"},
2856
+ )
2857
+
2858
+ payload, error = load_command_json(
2859
+ root,
2860
+ ["python3", str(install_root / "shared" / "scripts" / "loom_flow.py"), "runtime-state", "--target", str(target_root)],
2861
+ env={"LOOM_RUNTIME_SCENE": "upgrade-rehearsal"},
2862
+ )
2863
+ if error:
2864
+ failures.append(Failure("daily-execution-cli", f"`installed loom-flow runtime-state -- rehearsal` failed: {error}"))
2865
+ else:
2866
+ require_runtime_state_payload(
2867
+ failures,
2868
+ category="daily-execution-cli",
2869
+ context="`installed loom-flow runtime-state -- rehearsal`",
2870
+ payload=payload.get("runtime_state"),
2871
+ expected_scene="upgrade-rehearsal",
2872
+ expected_carrier="installed-skills-root",
2873
+ allowed_results={"pass"},
2874
+ )
2875
+
2876
+ broken_install = tmp_root / "broken-install" / "skills"
2877
+ shutil.copytree(root / "skills", broken_install)
2878
+ (broken_install / "shared" / "scripts" / "loom_flow.py").unlink()
2879
+ payload, error = load_command_json(
2880
+ root,
2881
+ ["python3", str(broken_install / "loom-init" / "scripts" / "loom-init.py"), "runtime-state", "--target", str(target_root)],
2882
+ )
2883
+ if error:
2884
+ failures.append(Failure("daily-execution-cli", f"`installed runtime-state` missing shared runtime failed unexpectedly: {error}"))
2885
+ elif payload.get("result") != "block":
2886
+ failures.append(Failure("daily-execution-cli", "`installed runtime-state` must block when shared runtime is missing"))
2887
+
2888
+ drift_install = tmp_root / "drift-install" / "skills"
2889
+ shutil.copytree(root / "skills", drift_install)
2890
+ (drift_install / "install-layout.json").unlink()
2891
+ payload, error = load_command_json(
2892
+ root,
2893
+ ["python3", str(drift_install / "loom-init" / "scripts" / "loom-init.py"), "runtime-state", "--target", str(target_root)],
2894
+ )
2895
+ if error:
2896
+ failures.append(Failure("daily-execution-cli", f"`installed runtime-state` missing install-layout failed unexpectedly: {error}"))
2897
+ elif payload.get("result") != "block":
2898
+ failures.append(Failure("daily-execution-cli", "`installed runtime-state` must block when install-layout is missing"))
2899
+
2900
+ payload, error = load_command_json(
2901
+ root,
2902
+ ["python3", str(install_root / "shared" / "scripts" / "loom_flow.py"), "runtime-state", "--target", str(target_root)],
2903
+ env={"LOOM_RUNTIME_SCENE": "repo-local-demo"},
2904
+ )
2905
+ if error:
2906
+ failures.append(Failure("daily-execution-cli", f"`installed runtime-state` scene conflict failed unexpectedly: {error}"))
2907
+ elif payload.get("result") != "block":
2908
+ failures.append(Failure("daily-execution-cli", "`installed runtime-state` must block on scene/carrier conflict"))
2909
+
2910
+ payload, error = load_command_json(
2911
+ root,
2912
+ ["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
2913
+ cwd=bootstrap_target,
2914
+ )
2915
+ if error:
2916
+ failures.append(Failure("daily-execution-cli", f"`bootstrapped loom-init runtime-state` failed: {error}"))
2917
+ else:
2918
+ require_runtime_state_payload(
2919
+ failures,
2920
+ category="daily-execution-cli",
2921
+ context="`bootstrapped loom-init runtime-state`",
2922
+ payload=payload.get("runtime_state"),
2923
+ expected_scene="installed-runtime",
2924
+ expected_carrier="bootstrapped-target-runtime",
2925
+ allowed_results={"pass"},
2926
+ )
2927
+
2928
+ broken_bootstrap = tmp_root / "broken-bootstrapped-target"
2929
+ shutil.copytree(example_target, broken_bootstrap)
2930
+ manifest_path = broken_bootstrap / ".loom" / "bootstrap" / "manifest.json"
2931
+ manifest = load_json_file(manifest_path)
2932
+ if isinstance(manifest, dict):
2933
+ artifacts = manifest.get("artifacts")
2934
+ if isinstance(artifacts, list):
2935
+ for artifact in artifacts:
2936
+ if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/runtime_state.py":
2937
+ artifact["source"] = "broken/source.py"
2938
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
2939
+ payload, error = load_command_json(
2940
+ root,
2941
+ ["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
2942
+ cwd=broken_bootstrap,
2943
+ )
2944
+ if error:
2945
+ failures.append(Failure("daily-execution-cli", f"`bootstrapped runtime-state` manifest drift failed unexpectedly: {error}"))
2946
+ elif payload.get("result") != "block":
2947
+ failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when the bootstrap manifest drifts"))
2948
+
2949
+ if shutil.which("git") is not None:
2950
+ with tempfile.TemporaryDirectory(prefix="loom-check-installed-pre-merge-") as tmp:
2951
+ tmp_root = Path(tmp)
2952
+ install_root = tmp_root / "installed" / "skills"
2953
+ source_snapshot = tmp_root / "source-snapshot"
2954
+ positive_target = tmp_root / "positive-target"
2955
+ review_fallback_target = tmp_root / "review-fallback-target"
2956
+ fake_bin = tmp_root / "bin"
2957
+ fake_bin.mkdir(parents=True, exist_ok=True)
2958
+ write_fake_codex(fake_bin / "codex", mode="success")
2959
+ installed_review_env = prepend_path_env(fake_bin)
2960
+ shutil.copytree(root / "skills", install_root)
2961
+ shutil.copytree(root, source_snapshot, ignore=shutil.ignore_patterns(".git", ".DS_Store", "__pycache__"))
2962
+
2963
+ def prepare_target(target: Path) -> tuple[str | None, list[str]]:
2964
+ errors: list[str] = []
2965
+ shutil.copytree(source_snapshot, target)
2966
+ for args in (
2967
+ ["git", "init"],
2968
+ ["git", "config", "user.email", "loom-check@example.com"],
2969
+ ["git", "config", "user.name", "loom-check"],
2970
+ ):
2971
+ result = run_command(root, args, cwd=target)
2972
+ if result.returncode != 0:
2973
+ detail = result.stderr.strip() or result.stdout.strip() or "git setup failed"
2974
+ errors.append(detail)
2975
+ return None, errors
2976
+
2977
+ payload, error = load_command_json(
2978
+ root,
2979
+ [
2980
+ "python3",
2981
+ str(install_root / "loom-init" / "scripts" / "loom-init.py"),
2982
+ "bootstrap",
2983
+ "--target",
2984
+ str(target),
2985
+ "--write",
2986
+ "--force",
2987
+ "--verify",
2988
+ "--install-pr-template",
2989
+ ],
2990
+ )
2991
+ if error:
2992
+ errors.append(error)
2993
+ return None, errors
2994
+ verification = payload.get("verification")
2995
+ if not isinstance(verification, dict) or verification.get("ok") is not True:
2996
+ errors.append("installed bootstrap must verify successfully before the pre-merge chain starts")
2997
+ return None, errors
2998
+
2999
+ git_add = run_command(root, ["git", "add", "."], cwd=target)
3000
+ if git_add.returncode != 0:
3001
+ detail = git_add.stderr.strip() or git_add.stdout.strip() or "git add failed"
3002
+ errors.append(detail)
3003
+ return None, errors
3004
+ git_commit = run_command(root, ["git", "commit", "-m", "bootstrap baseline for #209"], cwd=target)
3005
+ if git_commit.returncode != 0:
3006
+ detail = git_commit.stderr.strip() or git_commit.stdout.strip() or "git commit failed"
3007
+ errors.append(detail)
3008
+ return None, errors
3009
+
3010
+ resume_payload, resume_error = load_command_json(
3011
+ root,
3012
+ [
3013
+ "python3",
3014
+ str(install_root / "loom-resume" / "scripts" / "loom-resume.py"),
3015
+ "flow",
3016
+ "resume",
3017
+ "--target",
3018
+ str(target),
3019
+ "--item",
3020
+ "INIT-0001",
3021
+ ],
3022
+ )
3023
+ if resume_error:
3024
+ errors.append(resume_error)
3025
+ return None, errors
3026
+ recovery = resume_payload.get("recovery")
3027
+ if not isinstance(recovery, dict):
3028
+ errors.append("resume payload must include `recovery`")
3029
+ return None, errors
3030
+ summary = recovery.get("latest_validation_summary")
3031
+ if not isinstance(summary, str) or not summary:
3032
+ errors.append("resume payload must expose a non-empty `latest_validation_summary`")
3033
+ return None, errors
3034
+ return summary, errors
3035
+
3036
+ positive_summary, positive_setup_errors = prepare_target(positive_target)
3037
+ if positive_setup_errors:
3038
+ failures.append(
3039
+ Failure(
3040
+ "daily-execution-cli",
3041
+ f"`installed pre-merge chain` setup failed: {'; '.join(positive_setup_errors)}",
3042
+ )
3043
+ )
3044
+ else:
3045
+ task_signals = {
3046
+ "resume": "请接手当前事项并恢复上下文后继续推进",
3047
+ "pre-review": "请在进入 review 前做统一检查",
3048
+ "review": "请对当前事项做正式 review 并给出审查结论",
3049
+ "merge-ready": "请做 merge-ready 最终放行前预检并确认是否可以合并",
3050
+ }
3051
+
3052
+ payload, error = load_command_json(
3053
+ root,
3054
+ [
3055
+ "python3",
3056
+ str(install_root / "loom-init" / "scripts" / "loom-init.py"),
3057
+ "route",
3058
+ "--target",
3059
+ str(positive_target),
3060
+ "--task",
3061
+ task_signals["resume"],
3062
+ ],
3063
+ )
3064
+ if error:
3065
+ failures.append(Failure("daily-execution-cli", f"`installed route resume` failed: {error}"))
3066
+ else:
3067
+ require_route_payload(
3068
+ failures,
3069
+ category="daily-execution-cli",
3070
+ context="`installed route resume`",
3071
+ payload=payload,
3072
+ expected_skill="loom-resume",
3073
+ expected_mode="implicit",
3074
+ expected_runtime_scene="installed-runtime",
3075
+ expected_runtime_carrier="installed-skills-root",
3076
+ )
3077
+
3078
+ resume_payload, error = load_command_json(
3079
+ root,
3080
+ [
3081
+ "python3",
3082
+ str(install_root / "loom-resume" / "scripts" / "loom-resume.py"),
3083
+ "flow",
3084
+ "resume",
3085
+ "--target",
3086
+ str(positive_target),
3087
+ "--item",
3088
+ "INIT-0001",
3089
+ ],
3090
+ )
3091
+ if error:
3092
+ failures.append(Failure("daily-execution-cli", f"`installed flow resume` failed: {error}"))
3093
+ elif resume_payload.get("result") != "pass":
3094
+ failures.append(Failure("daily-execution-cli", "`installed flow resume` must pass for the positive chain"))
3095
+ else:
3096
+ require_runtime_state_payload(
3097
+ failures,
3098
+ category="daily-execution-cli",
3099
+ context="`installed flow resume`",
3100
+ payload=resume_payload.get("runtime_state"),
3101
+ expected_scene="installed-runtime",
3102
+ expected_carrier="installed-skills-root",
3103
+ allowed_results={"pass"},
3104
+ )
3105
+
3106
+ payload, error = load_command_json(
3107
+ root,
3108
+ [
3109
+ "python3",
3110
+ str(install_root / "loom-init" / "scripts" / "loom-init.py"),
3111
+ "route",
3112
+ "--target",
3113
+ str(positive_target),
3114
+ "--task",
3115
+ task_signals["pre-review"],
3116
+ ],
3117
+ )
3118
+ if error:
3119
+ failures.append(Failure("daily-execution-cli", f"`installed route pre-review` failed: {error}"))
3120
+ else:
3121
+ require_route_payload(
3122
+ failures,
3123
+ category="daily-execution-cli",
3124
+ context="`installed route pre-review`",
3125
+ payload=payload,
3126
+ expected_skill="loom-pre-review",
3127
+ expected_mode="implicit",
3128
+ expected_runtime_scene="installed-runtime",
3129
+ expected_runtime_carrier="installed-skills-root",
3130
+ )
3131
+
3132
+ payload, error = load_command_json(
3133
+ root,
3134
+ [
3135
+ "python3",
3136
+ str(install_root / "loom-pre-review" / "scripts" / "loom-pre-review.py"),
3137
+ "flow",
3138
+ "pre-review",
3139
+ "--target",
3140
+ str(positive_target),
3141
+ "--item",
3142
+ "INIT-0001",
3143
+ ],
3144
+ )
3145
+ if error:
3146
+ failures.append(Failure("daily-execution-cli", f"`installed flow pre-review` failed: {error}"))
3147
+ elif payload.get("result") != "pass":
3148
+ failures.append(Failure("daily-execution-cli", "`installed flow pre-review` must pass for the positive chain"))
3149
+
3150
+ payload, error = load_command_json(
3151
+ root,
3152
+ [
3153
+ "python3",
3154
+ str(install_root / "loom-init" / "scripts" / "loom-init.py"),
3155
+ "route",
3156
+ "--target",
3157
+ str(positive_target),
3158
+ "--task",
3159
+ task_signals["review"],
3160
+ ],
3161
+ )
3162
+ if error:
3163
+ failures.append(Failure("daily-execution-cli", f"`installed route review` failed: {error}"))
3164
+ else:
3165
+ require_route_payload(
3166
+ failures,
3167
+ category="daily-execution-cli",
3168
+ context="`installed route review`",
3169
+ payload=payload,
3170
+ expected_skill="loom-review",
3171
+ expected_mode="implicit",
3172
+ expected_runtime_scene="installed-runtime",
3173
+ expected_runtime_carrier="installed-skills-root",
3174
+ )
3175
+
3176
+ review_flow_payload, error = load_command_json(
3177
+ root,
3178
+ [
3179
+ "python3",
3180
+ str(install_root / "loom-review" / "scripts" / "loom-review.py"),
3181
+ "flow",
3182
+ "review",
3183
+ "--target",
3184
+ str(positive_target),
3185
+ "--item",
3186
+ "INIT-0001",
3187
+ ],
3188
+ )
3189
+ if error:
3190
+ failures.append(Failure("daily-execution-cli", f"`installed flow review` failed: {error}"))
3191
+ elif review_flow_payload.get("result") != "pass":
3192
+ failures.append(Failure("daily-execution-cli", "`installed flow review` must pass for the positive chain"))
3193
+ else:
3194
+ review = review_flow_payload.get("review")
3195
+ if isinstance(review, dict):
3196
+ require_review_record_contract(
3197
+ failures,
3198
+ category="daily-execution-cli",
3199
+ context="`installed flow review` review.record",
3200
+ payload=review.get("record"),
3201
+ )
3202
+
3203
+ review_run_payload: dict[str, object] | None = None
3204
+ review_record_input: dict[str, object] | None = None
3205
+ review_run_payload, error = load_command_json(
3206
+ root,
3207
+ [
3208
+ "python3",
3209
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3210
+ "review",
3211
+ "run",
3212
+ "--target",
3213
+ str(positive_target),
3214
+ "--item",
3215
+ "INIT-0001",
3216
+ ],
3217
+ env=installed_review_env,
3218
+ )
3219
+ if error:
3220
+ failures.append(Failure("daily-execution-cli", f"`installed review run` failed: {error}"))
3221
+ elif review_run_payload.get("result") != "pass":
3222
+ failures.append(Failure("daily-execution-cli", "`installed review run` must pass for the positive chain"))
3223
+ else:
3224
+ require_review_run_payload(
3225
+ failures,
3226
+ category="daily-execution-cli",
3227
+ context="`installed review run`",
3228
+ payload=review_run_payload,
3229
+ expected_result={"pass"},
3230
+ )
3231
+ review_record_input = review_run_payload.get("review_record_input") if isinstance(review_run_payload, dict) else None
3232
+ review_record_payload, error = load_command_json(
3233
+ root,
3234
+ [
3235
+ "python3",
3236
+ str(install_root / "loom-review" / "scripts" / "loom-review.py"),
3237
+ "review",
3238
+ "record",
3239
+ "--target",
3240
+ str(positive_target),
3241
+ "--item",
3242
+ "INIT-0001",
3243
+ "--decision",
3244
+ str(review_record_input.get("decision", "allow")) if isinstance(review_record_input, dict) else "allow",
3245
+ "--kind",
3246
+ str(review_record_input.get("kind", "code_review")) if isinstance(review_record_input, dict) else "code_review",
3247
+ "--summary",
3248
+ str(review_record_input.get("summary", "Installed pre-merge chain is ready for merge checkpoint consumption."))
3249
+ if isinstance(review_record_input, dict)
3250
+ else "Installed pre-merge chain is ready for merge checkpoint consumption.",
3251
+ "--reviewer",
3252
+ str(review_record_input.get("reviewer", "loom-check")) if isinstance(review_record_input, dict) else "loom-check",
3253
+ "--findings-file",
3254
+ str(review_record_input.get("findings_file", ".loom/review-findings.json")) if isinstance(review_record_input, dict) else ".loom/review-findings.json",
3255
+ "--engine-adapter",
3256
+ str(review_record_input.get("engine_adapter", "loom/default-codex")) if isinstance(review_record_input, dict) else "loom/default-codex",
3257
+ "--engine-evidence",
3258
+ str(review_record_input.get("engine_evidence", ".loom/runtime/review/INIT-0001/unknown-head/engine-result.json"))
3259
+ if isinstance(review_record_input, dict)
3260
+ else ".loom/runtime/review/INIT-0001/unknown-head/engine-result.json",
3261
+ "--normalized-findings",
3262
+ str(review_record_input.get("normalized_findings", ".loom/review-findings.json"))
3263
+ if isinstance(review_record_input, dict)
3264
+ else ".loom/review-findings.json",
3265
+ ],
3266
+ )
3267
+ if error:
3268
+ failures.append(Failure("daily-execution-cli", f"`installed review record allow` failed: {error}"))
3269
+ elif review_record_payload.get("result") != "pass":
3270
+ failures.append(Failure("daily-execution-cli", "`installed review record allow` must pass"))
3271
+ else:
3272
+ review = review_record_payload.get("review")
3273
+ if isinstance(review, dict):
3274
+ require_review_record_contract(
3275
+ failures,
3276
+ category="daily-execution-cli",
3277
+ context="`installed review record allow` review.record",
3278
+ payload=review.get("record"),
3279
+ )
3280
+
3281
+ payload, error = load_command_json(
3282
+ root,
3283
+ [
3284
+ "python3",
3285
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3286
+ "recovery",
3287
+ "writeback",
3288
+ "--target",
3289
+ str(positive_target),
3290
+ "--item",
3291
+ "INIT-0001",
3292
+ "--current-checkpoint",
3293
+ "merge checkpoint",
3294
+ "--current-stop",
3295
+ "Installed review completed and merge-ready validation is next.",
3296
+ "--next-step",
3297
+ "Run merge-ready and checkpoint merge from installed skills.",
3298
+ "--latest-validation-summary",
3299
+ positive_summary,
3300
+ ],
3301
+ )
3302
+ if error:
3303
+ failures.append(Failure("daily-execution-cli", f"`installed recovery writeback for merge` failed: {error}"))
3304
+ elif payload.get("result") != "pass":
3305
+ failures.append(Failure("daily-execution-cli", "`installed recovery writeback for merge` must pass"))
3306
+
3307
+ git_add = run_command(
3308
+ root,
3309
+ [
3310
+ "git",
3311
+ "add",
3312
+ "-f",
3313
+ ".loom/progress/INIT-0001.md",
3314
+ ".loom/status/current.md",
3315
+ ".loom/reviews/INIT-0001.json",
3316
+ ],
3317
+ cwd=positive_target,
3318
+ )
3319
+ if git_add.returncode != 0:
3320
+ detail = git_add.stderr.strip() or git_add.stdout.strip() or "git add failed"
3321
+ failures.append(Failure("daily-execution-cli", f"`installed pre-merge carrier commit` add failed: {detail}"))
3322
+ else:
3323
+ git_commit = run_command(
3324
+ root,
3325
+ ["git", "commit", "-m", "author installed pre-merge carriers for #209"],
3326
+ cwd=positive_target,
3327
+ )
3328
+ if git_commit.returncode != 0:
3329
+ detail = git_commit.stderr.strip() or git_commit.stdout.strip() or "git commit failed"
3330
+ failures.append(Failure("daily-execution-cli", f"`installed pre-merge carrier commit` failed: {detail}"))
3331
+
3332
+ payload, error = load_command_json(
3333
+ root,
3334
+ [
3335
+ "python3",
3336
+ str(install_root / "loom-init" / "scripts" / "loom-init.py"),
3337
+ "route",
3338
+ "--target",
3339
+ str(positive_target),
3340
+ "--task",
3341
+ task_signals["merge-ready"],
3342
+ ],
3343
+ )
3344
+ if error:
3345
+ failures.append(Failure("daily-execution-cli", f"`installed route merge-ready` failed: {error}"))
3346
+ else:
3347
+ require_route_payload(
3348
+ failures,
3349
+ category="daily-execution-cli",
3350
+ context="`installed route merge-ready`",
3351
+ payload=payload,
3352
+ expected_skill="loom-merge-ready",
3353
+ expected_mode="implicit",
3354
+ expected_runtime_scene="installed-runtime",
3355
+ expected_runtime_carrier="installed-skills-root",
3356
+ )
3357
+
3358
+ merge_ready_payload, error = load_command_json(
3359
+ root,
3360
+ [
3361
+ "python3",
3362
+ str(install_root / "loom-merge-ready" / "scripts" / "loom-merge-ready.py"),
3363
+ "flow",
3364
+ "merge-ready",
3365
+ "--target",
3366
+ str(positive_target),
3367
+ "--item",
3368
+ "INIT-0001",
3369
+ ],
3370
+ )
3371
+ if error:
3372
+ failures.append(Failure("daily-execution-cli", f"`installed flow merge-ready` failed: {error}"))
3373
+ elif merge_ready_payload.get("result") != "pass":
3374
+ failures.append(Failure("daily-execution-cli", "`installed flow merge-ready` must pass for the positive chain"))
3375
+ else:
3376
+ merge_checkpoint = merge_ready_payload.get("merge_checkpoint")
3377
+ if not isinstance(merge_checkpoint, dict) or merge_checkpoint.get("result") != "pass":
3378
+ failures.append(Failure("daily-execution-cli", "`installed flow merge-ready` must expose `merge_checkpoint.result = pass`"))
3379
+
3380
+ checkpoint_merge_payload, error = load_command_json(
3381
+ root,
3382
+ [
3383
+ "python3",
3384
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3385
+ "checkpoint",
3386
+ "merge",
3387
+ "--target",
3388
+ str(positive_target),
3389
+ "--item",
3390
+ "INIT-0001",
3391
+ ],
3392
+ )
3393
+ if error:
3394
+ failures.append(Failure("daily-execution-cli", f"`installed checkpoint merge` failed: {error}"))
3395
+ elif checkpoint_merge_payload.get("result") != "pass":
3396
+ failures.append(Failure("daily-execution-cli", "`installed checkpoint merge` must pass for the positive chain"))
3397
+
3398
+ broken_install = tmp_root / "broken-install" / "skills"
3399
+ shutil.copytree(root / "skills", broken_install)
3400
+ (broken_install / "install-layout.json").unlink()
3401
+ payload, error = load_command_json(
3402
+ root,
3403
+ [
3404
+ "python3",
3405
+ str(broken_install / "loom-init" / "scripts" / "loom-init.py"),
3406
+ "route",
3407
+ "--target",
3408
+ str(positive_target),
3409
+ "--task",
3410
+ task_signals["resume"],
3411
+ ],
3412
+ )
3413
+ if error:
3414
+ failures.append(Failure("daily-execution-cli", f"`installed route` missing install-layout failed unexpectedly: {error}"))
3415
+ else:
3416
+ require_route_payload(
3417
+ failures,
3418
+ category="daily-execution-cli",
3419
+ context="`installed route` missing install-layout",
3420
+ payload=payload,
3421
+ expected_skill="loom-init",
3422
+ expected_mode="fallback",
3423
+ expected_runtime_scene="installed-runtime",
3424
+ expected_runtime_carrier="installed-skills-root",
3425
+ allowed_results={"block"},
3426
+ )
3427
+
3428
+ payload, error = load_command_json(
3429
+ root,
3430
+ [
3431
+ "python3",
3432
+ str(broken_install / "loom-pre-review" / "scripts" / "loom-pre-review.py"),
3433
+ "flow",
3434
+ "pre-review",
3435
+ "--target",
3436
+ str(positive_target),
3437
+ "--item",
3438
+ "INIT-0001",
3439
+ ],
3440
+ )
3441
+ if error:
3442
+ failures.append(Failure("daily-execution-cli", f"`installed flow pre-review` missing install-layout failed unexpectedly: {error}"))
3443
+ elif payload.get("result") != "block":
3444
+ failures.append(Failure("daily-execution-cli", "`installed flow pre-review` must block when install-layout is missing"))
3445
+ else:
3446
+ require_runtime_state_payload(
3447
+ failures,
3448
+ category="daily-execution-cli",
3449
+ context="`installed flow pre-review` missing install-layout",
3450
+ payload=payload.get("runtime_state"),
3451
+ expected_scene="installed-runtime",
3452
+ expected_carrier="installed-skills-root",
3453
+ allowed_results={"block"},
3454
+ )
3455
+
3456
+ review_fallback_summary, review_fallback_errors = prepare_target(review_fallback_target)
3457
+ if review_fallback_errors:
3458
+ failures.append(
3459
+ Failure(
3460
+ "daily-execution-cli",
3461
+ f"`installed review baseline fallback` setup failed: {'; '.join(review_fallback_errors)}",
3462
+ )
3463
+ )
3464
+ else:
3465
+ payload, error = load_command_json(
3466
+ root,
3467
+ [
3468
+ "python3",
3469
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3470
+ "recovery",
3471
+ "writeback",
3472
+ "--target",
3473
+ str(review_fallback_target),
3474
+ "--item",
3475
+ "INIT-0001",
3476
+ "--current-checkpoint",
3477
+ "admission checkpoint",
3478
+ "--current-stop",
3479
+ "Installed review baseline is still at admission.",
3480
+ "--next-step",
3481
+ "Promote the target repo to build checkpoint before review.",
3482
+ "--latest-validation-summary",
3483
+ review_fallback_summary,
3484
+ ],
3485
+ )
3486
+ if error:
3487
+ failures.append(Failure("daily-execution-cli", f"`installed recovery writeback for admission fallback` failed: {error}"))
3488
+ elif payload.get("result") != "pass":
3489
+ failures.append(Failure("daily-execution-cli", "`installed recovery writeback for admission fallback` must pass"))
3490
+
3491
+ git_add = run_command(
3492
+ root,
3493
+ ["git", "add", "-f", ".loom/progress/INIT-0001.md", ".loom/status/current.md"],
3494
+ cwd=review_fallback_target,
3495
+ )
3496
+ if git_add.returncode != 0:
3497
+ detail = git_add.stderr.strip() or git_add.stdout.strip() or "git add failed"
3498
+ failures.append(Failure("daily-execution-cli", f"`installed review baseline fallback` add failed: {detail}"))
3499
+ else:
3500
+ git_commit = run_command(
3501
+ root,
3502
+ ["git", "commit", "-m", "lower checkpoint to admission for #209 fallback"],
3503
+ cwd=review_fallback_target,
3504
+ )
3505
+ if git_commit.returncode != 0:
3506
+ detail = git_commit.stderr.strip() or git_commit.stdout.strip() or "git commit failed"
3507
+ failures.append(Failure("daily-execution-cli", f"`installed review baseline fallback` commit failed: {detail}"))
3508
+
3509
+ payload, error = load_command_json(
3510
+ root,
3511
+ [
3512
+ "python3",
3513
+ str(install_root / "loom-review" / "scripts" / "loom-review.py"),
3514
+ "flow",
3515
+ "review",
3516
+ "--target",
3517
+ str(review_fallback_target),
3518
+ "--item",
3519
+ "INIT-0001",
3520
+ ],
3521
+ )
3522
+ if error:
3523
+ failures.append(Failure("daily-execution-cli", f"`installed flow review` admission fallback failed: {error}"))
3524
+ elif payload.get("result") != "fallback" or payload.get("fallback_to") != "admission":
3525
+ failures.append(Failure("daily-execution-cli", "`installed flow review` must fall back to `admission` when build checkpoint is missing"))
3526
+
3527
+ payload, error = load_command_json(
3528
+ root,
3529
+ [
3530
+ "python3",
3531
+ str(install_root / "loom-merge-ready" / "scripts" / "loom-merge-ready.py"),
3532
+ "flow",
3533
+ "merge-ready",
3534
+ "--target",
3535
+ str(review_fallback_target),
3536
+ "--item",
3537
+ "INIT-0001",
3538
+ ],
3539
+ )
3540
+ if error:
3541
+ failures.append(Failure("daily-execution-cli", f"`installed flow merge-ready` review-baseline fallback failed: {error}"))
3542
+ elif payload.get("result") not in {"fallback", "block"}:
3543
+ failures.append(Failure("daily-execution-cli", "`installed flow merge-ready` must fail closed when review baseline is missing"))
3544
+
3545
+ readme_path = positive_target / "README.md"
3546
+ readme_path.write_text(readme_path.read_text(encoding="utf-8") + "\n# review-head-drift\n", encoding="utf-8")
3547
+ git_add = run_command(root, ["git", "add", "README.md"], cwd=positive_target)
3548
+ if git_add.returncode != 0:
3549
+ detail = git_add.stderr.strip() or git_add.stdout.strip() or "git add failed"
3550
+ failures.append(Failure("daily-execution-cli", f"`installed merge-ready drift` add failed: {detail}"))
3551
+ else:
3552
+ git_commit = run_command(
3553
+ root,
3554
+ ["git", "commit", "-m", "introduce non-carrier drift after review for #209"],
3555
+ cwd=positive_target,
3556
+ )
3557
+ if git_commit.returncode != 0:
3558
+ detail = git_commit.stderr.strip() or git_commit.stdout.strip() or "git commit failed"
3559
+ failures.append(Failure("daily-execution-cli", f"`installed merge-ready drift` commit failed: {detail}"))
3560
+
3561
+ payload, error = load_command_json(
3562
+ root,
3563
+ [
3564
+ "python3",
3565
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3566
+ "checkpoint",
3567
+ "merge",
3568
+ "--target",
3569
+ str(positive_target),
3570
+ "--item",
3571
+ "INIT-0001",
3572
+ ],
3573
+ )
3574
+ if error:
3575
+ failures.append(Failure("daily-execution-cli", f"`installed checkpoint merge` drift negative failed: {error}"))
3576
+ elif payload.get("result") != "block":
3577
+ failures.append(Failure("daily-execution-cli", "`installed checkpoint merge` must block when HEAD drifts beyond Loom carriers"))
3578
+
3579
+ gh_auth_ready = shutil.which("gh") is not None and run_command(root, ["gh", "auth", "status"]).returncode == 0
3580
+ if gh_auth_ready:
3581
+ with tempfile.TemporaryDirectory(prefix="loom-check-installed-post-merge-") as tmp:
3582
+ tmp_root = Path(tmp)
3583
+ install_root = tmp_root / "installed" / "skills"
3584
+ retire_target = tmp_root / "retire-target"
3585
+ dirty_target = tmp_root / "dirty-target"
3586
+ broken_install = tmp_root / "broken-install" / "skills"
3587
+ shutil.copytree(root / "skills", install_root)
3588
+
3589
+ for label, args in (
3590
+ (
3591
+ "installed reconciliation audit",
3592
+ [
3593
+ "python3",
3594
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3595
+ "reconciliation",
3596
+ "audit",
3597
+ "--target",
3598
+ str(root),
3599
+ "--issue",
3600
+ "131",
3601
+ "--pr",
3602
+ "138",
3603
+ "--project",
3604
+ "5",
3605
+ ],
3606
+ ),
3607
+ (
3608
+ "installed reconciliation sync dry-run",
3609
+ [
3610
+ "python3",
3611
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3612
+ "reconciliation",
3613
+ "sync",
3614
+ "--target",
3615
+ str(root),
3616
+ "--issue",
3617
+ "131",
3618
+ "--pr",
3619
+ "138",
3620
+ "--project",
3621
+ "5",
3622
+ "--dry-run",
3623
+ ],
3624
+ ),
3625
+ (
3626
+ "installed closeout check",
3627
+ [
3628
+ "python3",
3629
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3630
+ "closeout",
3631
+ "check",
3632
+ "--target",
3633
+ str(root),
3634
+ "--issue",
3635
+ "131",
3636
+ "--pr",
3637
+ "138",
3638
+ "--project",
3639
+ "5",
3640
+ "--skip-gate",
3641
+ ],
3642
+ ),
3643
+ (
3644
+ "installed closeout sync",
3645
+ [
3646
+ "python3",
3647
+ str(install_root / "shared" / "scripts" / "loom_flow.py"),
3648
+ "closeout",
3649
+ "sync",
3650
+ "--target",
3651
+ str(root),
3652
+ "--issue",
3653
+ "131",
3654
+ "--pr",
3655
+ "138",
3656
+ "--project",
3657
+ "5",
3658
+ "--skip-gate",
3659
+ ],
3660
+ ),
3661
+ ):
3662
+ payload, error = load_command_json_with_retry(
3663
+ root,
3664
+ args,
3665
+ timeout_seconds=30,
3666
+ retries=3,
3667
+ )
3668
+ if error:
3669
+ if label in {"installed closeout check", "installed closeout sync"} and "command timed out" in error:
3670
+ continue
3671
+ failures.append(Failure("daily-execution-cli", f"`{label}` failed: {error}"))
3672
+ continue
3673
+ rate_limited = payload_has_github_rate_limit(payload)
3674
+ if label == "installed reconciliation audit":
3675
+ if payload.get("result") != "pass" and not rate_limited:
3676
+ failures.append(Failure("daily-execution-cli", "`installed reconciliation audit` must pass on the historical closeout sample"))
3677
+ require_runtime_state_payload(
3678
+ failures,
3679
+ category="daily-execution-cli",
3680
+ context="`installed reconciliation audit`",
3681
+ payload=payload.get("runtime_state"),
3682
+ expected_scene="installed-runtime",
3683
+ expected_carrier="installed-skills-root",
3684
+ allowed_results={"pass"},
3685
+ )
3686
+ require_reconciliation_payload(
3687
+ failures,
3688
+ category="daily-execution-cli",
3689
+ context="`installed reconciliation audit`",
3690
+ payload=payload,
3691
+ )
3692
+ elif label == "installed reconciliation sync dry-run":
3693
+ if payload.get("result") != "pass" and not rate_limited:
3694
+ failures.append(Failure("daily-execution-cli", "`installed reconciliation sync --dry-run` must pass on an already aligned sample"))
3695
+ require_runtime_state_payload(
3696
+ failures,
3697
+ category="daily-execution-cli",
3698
+ context="`installed reconciliation sync --dry-run`",
3699
+ payload=payload.get("runtime_state"),
3700
+ expected_scene="installed-runtime",
3701
+ expected_carrier="installed-skills-root",
3702
+ allowed_results={"pass"},
3703
+ )
3704
+ else:
3705
+ if payload.get("result") != "pass" and not rate_limited:
3706
+ failures.append(Failure("daily-execution-cli", f"`{label}` must pass on the historical closeout sample"))
3707
+ require_runtime_state_payload(
3708
+ failures,
3709
+ category="daily-execution-cli",
3710
+ context=f"`{label}`",
3711
+ payload=payload.get("runtime_state"),
3712
+ expected_scene="installed-runtime",
3713
+ expected_carrier="installed-skills-root",
3714
+ allowed_results={"pass"},
3715
+ )
3716
+ require_closeout_reconciliation_contract(
3717
+ failures,
3718
+ category="daily-execution-cli",
3719
+ context=f"`{label}`",
3720
+ payload=payload,
3721
+ )
3722
+
3723
+ for target in (retire_target, dirty_target):
3724
+ shutil.copytree(example_target, target)
3725
+ for args in (
3726
+ ["git", "init"],
3727
+ ["git", "config", "user.email", "loom-check@example.com"],
3728
+ ["git", "config", "user.name", "loom-check"],
3729
+ ["git", "add", "."],
3730
+ ["git", "commit", "-m", "baseline"],
3731
+ ):
3732
+ result = run_command(root, args, cwd=target)
3733
+ if result.returncode != 0:
3734
+ detail = result.stderr.strip() or result.stdout.strip() or "git setup failed"
3735
+ failures.append(Failure("daily-execution-cli", f"`installed retire` setup failed: {detail}"))
3736
+ break
3737
+
3738
+ purity_payload, error = load_command_json(
3739
+ root,
3740
+ [
3741
+ "python3",
3742
+ str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
3743
+ "purity-check",
3744
+ "--target",
3745
+ str(retire_target),
3746
+ "--item",
3747
+ "INIT-0001",
3748
+ ],
3749
+ )
3750
+ if error:
3751
+ failures.append(Failure("daily-execution-cli", f"`installed purity-check` failed: {error}"))
3752
+ elif purity_payload.get("result") != "pass":
3753
+ failures.append(Failure("daily-execution-cli", "`installed purity-check` must pass on a clean retire target"))
3754
+ else:
3755
+ require_runtime_state_payload(
3756
+ failures,
3757
+ category="daily-execution-cli",
3758
+ context="`installed purity-check`",
3759
+ payload=purity_payload.get("runtime_state"),
3760
+ expected_scene="installed-runtime",
3761
+ expected_carrier="installed-skills-root",
3762
+ allowed_results={"pass"},
3763
+ )
3764
+
3765
+ temp_root = retire_target / ".loom" / ".tmp"
3766
+ temp_root.mkdir(parents=True, exist_ok=True)
3767
+ (temp_root / "sentinel.txt").write_text("temp\n", encoding="utf-8")
3768
+
3769
+ cleanup_payload, error = load_command_json(
3770
+ root,
3771
+ [
3772
+ "python3",
3773
+ str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
3774
+ "workspace",
3775
+ "cleanup",
3776
+ "--target",
3777
+ str(retire_target),
3778
+ "--item",
3779
+ "INIT-0001",
3780
+ ],
3781
+ )
3782
+ if error:
3783
+ failures.append(Failure("daily-execution-cli", f"`installed workspace cleanup` failed: {error}"))
3784
+ elif cleanup_payload.get("result") != "pass":
3785
+ failures.append(Failure("daily-execution-cli", "`installed workspace cleanup` must pass for Loom-owned residue"))
3786
+ else:
3787
+ require_runtime_state_payload(
3788
+ failures,
3789
+ category="daily-execution-cli",
3790
+ context="`installed workspace cleanup`",
3791
+ payload=cleanup_payload.get("runtime_state"),
3792
+ expected_scene="installed-runtime",
3793
+ expected_carrier="installed-skills-root",
3794
+ allowed_results={"pass"},
3795
+ )
3796
+
3797
+ retire_payload, error = load_command_json(
3798
+ root,
3799
+ [
3800
+ "python3",
3801
+ str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
3802
+ "workspace",
3803
+ "retire",
3804
+ "--target",
3805
+ str(retire_target),
3806
+ "--item",
3807
+ "INIT-0001",
3808
+ ],
3809
+ )
3810
+ if error:
3811
+ failures.append(Failure("daily-execution-cli", f"`installed workspace retire` failed: {error}"))
3812
+ elif retire_payload.get("result") != "pass":
3813
+ failures.append(Failure("daily-execution-cli", "`installed workspace retire` must pass after cleanup"))
3814
+ else:
3815
+ require_runtime_state_payload(
3816
+ failures,
3817
+ category="daily-execution-cli",
3818
+ context="`installed workspace retire`",
3819
+ payload=retire_payload.get("runtime_state"),
3820
+ expected_scene="installed-runtime",
3821
+ expected_carrier="installed-skills-root",
3822
+ allowed_results={"pass"},
3823
+ )
3824
+ checkpoint = retire_payload.get("checkpoint")
3825
+ if not isinstance(checkpoint, dict) or checkpoint.get("normalized") != "retired":
3826
+ failures.append(Failure("daily-execution-cli", "`installed workspace retire` must leave the target in `retired` state"))
3827
+
3828
+ (dirty_target / "foreign-residue.txt").write_text("pending\n", encoding="utf-8")
3829
+ dirty_add = run_command(root, ["git", "add", "foreign-residue.txt"], cwd=dirty_target)
3830
+ if dirty_add.returncode != 0:
3831
+ detail = dirty_add.stderr.strip() or dirty_add.stdout.strip() or "git add failed"
3832
+ failures.append(Failure("daily-execution-cli", f"`installed retire` dirty sample setup failed: {detail}"))
3833
+
3834
+ for label, args in (
3835
+ (
3836
+ "installed purity-check dirty sample",
3837
+ [
3838
+ "python3",
3839
+ str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
3840
+ "purity-check",
3841
+ "--target",
3842
+ str(dirty_target),
3843
+ "--item",
3844
+ "INIT-0001",
3845
+ ],
3846
+ ),
3847
+ (
3848
+ "installed workspace cleanup dirty sample",
3849
+ [
3850
+ "python3",
3851
+ str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
3852
+ "workspace",
3853
+ "cleanup",
3854
+ "--target",
3855
+ str(dirty_target),
3856
+ "--item",
3857
+ "INIT-0001",
3858
+ ],
3859
+ ),
3860
+ (
3861
+ "installed workspace retire dirty sample",
3862
+ [
3863
+ "python3",
3864
+ str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
3865
+ "workspace",
3866
+ "retire",
3867
+ "--target",
3868
+ str(dirty_target),
3869
+ "--item",
3870
+ "INIT-0001",
3871
+ ],
3872
+ ),
3873
+ ):
3874
+ payload, error = load_command_json(root, args)
3875
+ if error:
3876
+ failures.append(Failure("daily-execution-cli", f"`{label}` failed: {error}"))
3877
+ continue
3878
+ if payload.get("result") != "block":
3879
+ failures.append(Failure("daily-execution-cli", f"`{label}` must block when non-Loom residue is present"))
3880
+ require_runtime_state_payload(
3881
+ failures,
3882
+ category="daily-execution-cli",
3883
+ context=f"`{label}`",
3884
+ payload=payload.get("runtime_state"),
3885
+ expected_scene="installed-runtime",
3886
+ expected_carrier="installed-skills-root",
3887
+ allowed_results={"pass"},
3888
+ )
3889
+
3890
+ shutil.copytree(root / "skills", broken_install)
3891
+ (broken_install / "install-layout.json").unlink()
3892
+ for label, args in (
3893
+ (
3894
+ "installed closeout check missing install-layout",
3895
+ [
3896
+ "python3",
3897
+ str(broken_install / "shared" / "scripts" / "loom_flow.py"),
3898
+ "closeout",
3899
+ "check",
3900
+ "--target",
3901
+ str(root),
3902
+ "--issue",
3903
+ "131",
3904
+ "--pr",
3905
+ "138",
3906
+ "--skip-gate",
3907
+ ],
3908
+ ),
3909
+ (
3910
+ "installed purity-check missing install-layout",
3911
+ [
3912
+ "python3",
3913
+ str(broken_install / "loom-retire" / "scripts" / "loom-retire.py"),
3914
+ "purity-check",
3915
+ "--target",
3916
+ str(retire_target),
3917
+ "--item",
3918
+ "INIT-0001",
3919
+ ],
3920
+ ),
3921
+ ):
3922
+ payload, error = load_command_json(root, args)
3923
+ if error:
3924
+ failures.append(Failure("daily-execution-cli", f"`{label}` failed unexpectedly: {error}"))
3925
+ continue
3926
+ if payload.get("result") != "block":
3927
+ failures.append(Failure("daily-execution-cli", f"`{label}` must block when install-layout is missing"))
3928
+ require_runtime_state_payload(
3929
+ failures,
3930
+ category="daily-execution-cli",
3931
+ context=f"`{label}`",
3932
+ payload=payload.get("runtime_state"),
3933
+ expected_scene="installed-runtime",
3934
+ expected_carrier="installed-skills-root",
3935
+ allowed_results={"block"},
3936
+ )
3937
+
3938
+ fail_closed_payloads = [
3939
+ (
3940
+ "closeout-fix-needed-fail-open",
3941
+ {
3942
+ "result": "pass",
3943
+ "fallback_to": None,
3944
+ "reconciliation": {
3945
+ "command": "reconciliation",
3946
+ "operation": "audit",
3947
+ "result": "fix-needed",
3948
+ "summary": "fix-needed",
3949
+ "missing_inputs": [],
3950
+ "fallback_to": "manual-reconciliation",
3951
+ "findings": [
3952
+ {
3953
+ "kind": "absorbed_but_open",
3954
+ "severity": "fix-needed",
3955
+ "subject": "issue #177",
3956
+ "evidence": {},
3957
+ "recommended_action": "run reconciliation sync",
3958
+ }
3959
+ ],
3960
+ },
3961
+ },
3962
+ ),
3963
+ (
3964
+ "closeout-block-fallback-drift",
3965
+ {
3966
+ "result": "block",
3967
+ "fallback_to": "merge",
3968
+ "reconciliation": {
3969
+ "command": "reconciliation",
3970
+ "operation": "audit",
3971
+ "result": "block",
3972
+ "summary": "block",
3973
+ "missing_inputs": ["issue/pr/project"],
3974
+ "fallback_to": "manual-reconciliation",
3975
+ "findings": [
3976
+ {
3977
+ "kind": "parent_drift",
3978
+ "severity": "block",
3979
+ "subject": "parent issue #148",
3980
+ "evidence": {},
3981
+ "recommended_action": "manual reconciliation",
3982
+ }
3983
+ ],
3984
+ },
3985
+ },
3986
+ ),
3987
+ (
3988
+ "closeout-malformed-reconciliation",
3989
+ {
3990
+ "result": "pass",
3991
+ "fallback_to": None,
3992
+ "reconciliation": {
3993
+ "command": "reconciliation",
3994
+ "operation": "audit",
3995
+ "summary": "broken",
3996
+ "missing_inputs": "bad",
3997
+ "findings": "bad",
3998
+ },
3999
+ },
4000
+ ),
4001
+ ]
4002
+ for label, payload in fail_closed_payloads:
4003
+ sample_failures: list[Failure] = []
4004
+ require_closeout_reconciliation_contract(
4005
+ sample_failures,
4006
+ category="daily-execution-cli",
4007
+ context=f"`{label}`",
4008
+ payload=payload,
4009
+ )
4010
+ if not sample_failures:
4011
+ failures.append(
4012
+ Failure(
4013
+ "daily-execution-cli",
4014
+ f"`{label}` synthetic payload must fail closeout reconciliation validation",
4015
+ )
4016
+ )
4017
+
4018
+ warn_payload_failures: list[Failure] = []
4019
+ require_closeout_reconciliation_contract(
4020
+ warn_payload_failures,
4021
+ category="daily-execution-cli",
4022
+ context="`closeout-warn-does-not-block`",
4023
+ payload={
4024
+ "result": "pass",
4025
+ "fallback_to": None,
4026
+ "reconciliation": {
4027
+ "command": "reconciliation",
4028
+ "operation": "audit",
4029
+ "result": "warn",
4030
+ "summary": "warn",
4031
+ "missing_inputs": [],
4032
+ "fallback_to": "manual-reconciliation",
4033
+ "findings": [
4034
+ {
4035
+ "kind": "project_drift",
4036
+ "severity": "warn",
4037
+ "subject": "project 5",
4038
+ "evidence": {},
4039
+ "recommended_action": "review warning",
4040
+ }
4041
+ ],
4042
+ },
4043
+ },
4044
+ )
4045
+ if warn_payload_failures:
4046
+ failures.append(
4047
+ Failure(
4048
+ "daily-execution-cli",
4049
+ "`closeout-warn-does-not-block` synthetic payload must allow non-blocking reconciliation warnings",
4050
+ )
4051
+ )
4052
+
4053
+ return failures
4054
+
4055
+
4056
+ def check_repo_companion_interface_contracts(root: Path) -> list[Failure]:
4057
+ failures: list[Failure] = []
4058
+ example_target = root / "examples/new-project"
4059
+ if not example_target.exists():
4060
+ return failures
4061
+
4062
+ def write_json(path: Path, payload: object) -> None:
4063
+ path.parent.mkdir(parents=True, exist_ok=True)
4064
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
4065
+
4066
+ def install_companion(
4067
+ target: Path,
4068
+ *,
4069
+ manifest: dict[str, object] | None = None,
4070
+ repo_interface: dict[str, object] | None = None,
4071
+ legacy_docs_only: bool = False,
4072
+ ) -> None:
4073
+ companion_dir = target / ".loom" / "companion"
4074
+ companion_dir.mkdir(parents=True, exist_ok=True)
4075
+ if legacy_docs_only:
4076
+ (companion_dir / "README.md").write_text("# Legacy Companion Docs\n", encoding="utf-8")
4077
+ return
4078
+ (companion_dir / "README.md").write_text("# Repo Companion\n", encoding="utf-8")
4079
+ for doc in (
4080
+ "review.md",
4081
+ "merge-ready.md",
4082
+ "closeout.md",
4083
+ "specialized-gates.md",
4084
+ "checkpoints.md",
4085
+ "metadata-contract.md",
4086
+ "context-schema.md",
4087
+ ):
4088
+ (companion_dir / doc).write_text(f"# {doc}\n", encoding="utf-8")
4089
+ if manifest is not None:
4090
+ write_json(companion_dir / "manifest.json", manifest)
4091
+ if repo_interface is not None:
4092
+ write_json(companion_dir / "repo-interface.json", repo_interface)
4093
+
4094
+ valid_manifest = {
4095
+ "schema_version": "loom-repo-companion-manifest/v1",
4096
+ "companion_entry": ".loom/companion/README.md",
4097
+ "repo_interface": ".loom/companion/repo-interface.json",
4098
+ }
4099
+ valid_interface_v1 = {
4100
+ "schema_version": "loom-repo-interface/v1",
4101
+ "companion_entry": ".loom/companion/README.md",
4102
+ "repo_specific_requirements": {
4103
+ "review": [
4104
+ {
4105
+ "id": "review-specialized-gate",
4106
+ "summary": "Run the repo-specific semantic review checklist.",
4107
+ "locator": ".loom/companion/review.md",
4108
+ "enforcement": "blocking",
4109
+ }
4110
+ ],
4111
+ "merge_ready": [
4112
+ {
4113
+ "id": "merge-ready-advisory-note",
4114
+ "summary": "Review the repo-specific merge advisory note.",
4115
+ "locator": ".loom/companion/merge-ready.md",
4116
+ "enforcement": "advisory",
4117
+ }
4118
+ ],
4119
+ "closeout": [
4120
+ {
4121
+ "id": "closeout-specialized-gate",
4122
+ "summary": "Confirm the repo-specific closeout checklist.",
4123
+ "locator": ".loom/companion/closeout.md",
4124
+ "enforcement": "blocking",
4125
+ }
4126
+ ],
4127
+ },
4128
+ "specialized_gates": [
4129
+ {
4130
+ "id": "specialized-release-gate",
4131
+ "summary": "Companion-owned release judgment.",
4132
+ "locator": ".loom/companion/specialized-gates.md",
4133
+ }
4134
+ ],
4135
+ }
4136
+ valid_interface_v2 = {
4137
+ "schema_version": "loom-repo-interface/v2",
4138
+ "companion_entry": ".loom/companion/README.md",
4139
+ "repo_specific_requirements": valid_interface_v1["repo_specific_requirements"],
4140
+ "specialized_gates": [
4141
+ {
4142
+ "id": "specialized-review-gate",
4143
+ "summary": "Companion-owned review specialization.",
4144
+ "locator": ".loom/companion/specialized-gates.md",
4145
+ "gate_type": "review",
4146
+ }
4147
+ ],
4148
+ "metadata_contract": {
4149
+ "fields": [
4150
+ {
4151
+ "id": "integration_check",
4152
+ "summary": "Declare repo-specific integration metadata.",
4153
+ "applicability_locator": ".loom/companion/metadata-contract.md",
4154
+ "authority_locator": ".loom/companion/review.md",
4155
+ "enforcement": "blocking",
4156
+ }
4157
+ ]
4158
+ },
4159
+ "context_schema": {
4160
+ "fields": [
4161
+ {
4162
+ "id": "item_key",
4163
+ "summary": "Repo-native item key.",
4164
+ "type": "string",
4165
+ "required": True,
4166
+ "mapping_rule_locator": ".loom/companion/context-schema.md",
4167
+ }
4168
+ ]
4169
+ },
4170
+ }
4171
+
4172
+ with tempfile.TemporaryDirectory(prefix="loom-check-repo-companion-") as tmp:
4173
+ base = Path(tmp)
4174
+
4175
+ absent_target = base / "absent"
4176
+ shutil.copytree(example_target, absent_target)
4177
+ absent_surface = build_governance_surface(absent_target)
4178
+ repo_interface = absent_surface.get("repo_interface")
4179
+ require_repo_interface_payload(
4180
+ failures,
4181
+ category="repo-companion",
4182
+ context="absent repo companion",
4183
+ payload=repo_interface,
4184
+ )
4185
+ if not isinstance(repo_interface, dict) or repo_interface.get("availability") != "absent":
4186
+ failures.append(Failure("repo-companion", "absent repo companion sample must report `availability: absent`"))
4187
+
4188
+ docs_only_target = base / "docs-only"
4189
+ shutil.copytree(example_target, docs_only_target)
4190
+ install_companion(docs_only_target, legacy_docs_only=True)
4191
+ docs_only_surface = build_governance_surface(docs_only_target)
4192
+ docs_only_interface = docs_only_surface.get("repo_interface")
4193
+ require_repo_interface_payload(
4194
+ failures,
4195
+ category="repo-companion",
4196
+ context="docs-only repo companion",
4197
+ payload=docs_only_interface,
4198
+ )
4199
+ if not isinstance(docs_only_interface, dict) or docs_only_interface.get("availability") != "companion_docs_only":
4200
+ failures.append(Failure("repo-companion", "docs-only repo companion sample must report `availability: companion_docs_only`"))
4201
+
4202
+ incomplete_target = base / "incomplete"
4203
+ shutil.copytree(example_target, incomplete_target)
4204
+ install_companion(
4205
+ incomplete_target,
4206
+ manifest={
4207
+ **valid_manifest,
4208
+ "current_stop": "forbidden authored state",
4209
+ "repo_interface": ".loom/companion/missing-interface.json",
4210
+ },
4211
+ )
4212
+ incomplete_surface = build_governance_surface(incomplete_target)
4213
+ incomplete_interface = incomplete_surface.get("repo_interface")
4214
+ require_repo_interface_payload(
4215
+ failures,
4216
+ category="repo-companion",
4217
+ context="incomplete repo companion",
4218
+ payload=incomplete_interface,
4219
+ )
4220
+ if not isinstance(incomplete_interface, dict) or incomplete_interface.get("availability") != "incomplete":
4221
+ failures.append(Failure("repo-companion", "incomplete repo companion sample must report `availability: incomplete`"))
4222
+
4223
+ invalid_interface_target = base / "invalid-interface"
4224
+ shutil.copytree(example_target, invalid_interface_target)
4225
+ install_companion(
4226
+ invalid_interface_target,
4227
+ manifest=valid_manifest,
4228
+ repo_interface={
4229
+ "schema_version": "loom-repo-interface/v1",
4230
+ "companion_entry": ".loom/companion/README.md",
4231
+ "repo_specific_requirements": {
4232
+ "review": [
4233
+ {
4234
+ "id": "bad-enforcement",
4235
+ "summary": "Broken requirement",
4236
+ "locator": ".loom/companion/review.md",
4237
+ "enforcement": "required",
4238
+ }
4239
+ ],
4240
+ "merge_ready": [],
4241
+ },
4242
+ "specialized_gates": [],
4243
+ },
4244
+ )
4245
+ invalid_interface_surface = build_governance_surface(invalid_interface_target)
4246
+ invalid_interface = invalid_interface_surface.get("repo_interface")
4247
+ require_repo_interface_payload(
4248
+ failures,
4249
+ category="repo-companion",
4250
+ context="invalid repo companion interface",
4251
+ payload=invalid_interface,
4252
+ )
4253
+ if not isinstance(invalid_interface, dict) or invalid_interface.get("availability") != "incomplete":
4254
+ failures.append(Failure("repo-companion", "invalid repo companion interface sample must report `availability: incomplete`"))
4255
+
4256
+ invalid_v2_target = base / "invalid-v2-interface"
4257
+ shutil.copytree(example_target, invalid_v2_target)
4258
+ install_companion(
4259
+ invalid_v2_target,
4260
+ manifest=valid_manifest,
4261
+ repo_interface={
4262
+ "schema_version": "loom-repo-interface/v2",
4263
+ "companion_entry": ".loom/companion/README.md",
4264
+ "repo_specific_requirements": valid_interface_v1["repo_specific_requirements"],
4265
+ "specialized_gates": [
4266
+ {
4267
+ "id": "bad-gate-type",
4268
+ "summary": "Broken gate type",
4269
+ "locator": ".loom/companion/specialized-gates.md",
4270
+ "gate_type": "guardian",
4271
+ }
4272
+ ],
4273
+ "metadata_contract": {
4274
+ "fields": [
4275
+ {
4276
+ "id": "bad-metadata",
4277
+ "summary": "Broken metadata field",
4278
+ "applicability_locator": ".loom/companion/metadata-contract.md",
4279
+ "authority_locator": ".loom/companion/review.md",
4280
+ "enforcement": "required",
4281
+ }
4282
+ ]
4283
+ },
4284
+ "context_schema": {
4285
+ "fields": [
4286
+ {
4287
+ "id": "bad-context",
4288
+ "summary": "Broken context field",
4289
+ "type": "object",
4290
+ "required": "yes",
4291
+ "mapping_rule_locator": ".loom/companion/context-schema.md",
4292
+ }
4293
+ ]
4294
+ },
4295
+ },
4296
+ )
4297
+ invalid_v2_surface = build_governance_surface(invalid_v2_target)
4298
+ invalid_v2_interface = invalid_v2_surface.get("repo_interface")
4299
+ require_repo_interface_payload(
4300
+ failures,
4301
+ category="repo-companion",
4302
+ context="invalid v2 repo companion interface",
4303
+ payload=invalid_v2_interface,
4304
+ )
4305
+ if not isinstance(invalid_v2_interface, dict) or invalid_v2_interface.get("availability") != "incomplete":
4306
+ failures.append(Failure("repo-companion", "invalid v2 repo companion interface sample must report `availability: incomplete`"))
4307
+
4308
+ present_v1_target = base / "present-v1"
4309
+ shutil.copytree(example_target, present_v1_target)
4310
+ install_companion(
4311
+ present_v1_target,
4312
+ manifest=valid_manifest,
4313
+ repo_interface=valid_interface_v1,
4314
+ )
4315
+ present_v1_surface = build_governance_surface(present_v1_target)
4316
+ present_v1_interface = present_v1_surface.get("repo_interface")
4317
+ require_repo_interface_payload(
4318
+ failures,
4319
+ category="repo-companion",
4320
+ context="present v1 repo companion",
4321
+ payload=present_v1_interface,
4322
+ )
4323
+ if not isinstance(present_v1_interface, dict) or present_v1_interface.get("availability") != "present":
4324
+ failures.append(Failure("repo-companion", "present v1 repo companion sample must report `availability: present`"))
4325
+
4326
+ present_target = base / "present-v2"
4327
+ shutil.copytree(example_target, present_target)
4328
+ install_companion(
4329
+ present_target,
4330
+ manifest=valid_manifest,
4331
+ repo_interface=valid_interface_v2,
4332
+ )
4333
+ present_surface = build_governance_surface(present_target)
4334
+ present_interface = present_surface.get("repo_interface")
4335
+ require_repo_interface_payload(
4336
+ failures,
4337
+ category="repo-companion",
4338
+ context="present v2 repo companion",
4339
+ payload=present_interface,
4340
+ )
4341
+ if not isinstance(present_interface, dict) or present_interface.get("availability") != "present":
4342
+ failures.append(Failure("repo-companion", "present v2 repo companion sample must report `availability: present`"))
4343
+
4344
+ review_requirements = repo_specific_requirements_payload(
4345
+ present_interface,
4346
+ target_root=present_target,
4347
+ surface="review",
4348
+ )
4349
+ require_repo_specific_requirements_payload(
4350
+ failures,
4351
+ category="repo-companion",
4352
+ context="present repo companion review requirements",
4353
+ payload=review_requirements,
4354
+ expected_surface="review",
4355
+ )
4356
+ if review_requirements.get("result") != "block":
4357
+ failures.append(Failure("repo-companion", "blocking review requirements must fail closed"))
4358
+
4359
+ merge_requirements = repo_specific_requirements_payload(
4360
+ present_interface,
4361
+ target_root=present_target,
4362
+ surface="merge_ready",
4363
+ )
4364
+ require_repo_specific_requirements_payload(
4365
+ failures,
4366
+ category="repo-companion",
4367
+ context="present repo companion merge-ready requirements",
4368
+ payload=merge_requirements,
4369
+ expected_surface="merge_ready",
4370
+ )
4371
+ if merge_requirements.get("result") != "pass":
4372
+ failures.append(Failure("repo-companion", "advisory merge-ready requirements must remain non-blocking"))
4373
+
4374
+ closeout_requirements = repo_specific_requirements_payload(
4375
+ present_interface,
4376
+ target_root=present_target,
4377
+ surface="closeout",
4378
+ )
4379
+ require_repo_specific_requirements_payload(
4380
+ failures,
4381
+ category="repo-companion",
4382
+ context="present repo companion closeout requirements",
4383
+ payload=closeout_requirements,
4384
+ expected_surface="closeout",
4385
+ )
4386
+ if closeout_requirements.get("result") != "block":
4387
+ failures.append(Failure("repo-companion", "blocking closeout requirements must fail closed"))
4388
+
4389
+ flow_review_payload, error = load_command_json(
4390
+ root,
4391
+ [
4392
+ "python3",
4393
+ "tools/loom_flow.py",
4394
+ "flow",
4395
+ "review",
4396
+ "--target",
4397
+ str(present_target),
4398
+ "--item",
4399
+ "INIT-0001",
4400
+ ],
4401
+ )
4402
+ if error:
4403
+ failures.append(Failure("repo-companion", f"`flow review` companion sample failed: {error}"))
4404
+ elif flow_review_payload.get("result") != "block":
4405
+ failures.append(Failure("repo-companion", "`flow review` must block when repo companion declares blocking review requirements"))
4406
+
4407
+ flow_merge_ready_payload, error = load_command_json(
4408
+ root,
4409
+ [
4410
+ "python3",
4411
+ "tools/loom_flow.py",
4412
+ "flow",
4413
+ "merge-ready",
4414
+ "--target",
4415
+ str(present_target),
4416
+ "--item",
4417
+ "INIT-0001",
4418
+ ],
4419
+ )
4420
+ if error:
4421
+ failures.append(Failure("repo-companion", f"`flow merge-ready` companion sample failed: {error}"))
4422
+ elif not isinstance(flow_merge_ready_payload.get("repo_specific_requirements"), dict):
4423
+ failures.append(Failure("repo-companion", "`flow merge-ready` companion sample must include `repo_specific_requirements`"))
4424
+ elif flow_merge_ready_payload["repo_specific_requirements"].get("result") != "pass":
4425
+ failures.append(Failure("repo-companion", "`flow merge-ready` advisory companion requirements must stay non-blocking"))
4426
+
4427
+ return failures
4428
+
4429
+
4430
+ def check_repo_interop_contracts(root: Path) -> list[Failure]:
4431
+ example_target = root / "examples/new-project"
4432
+ if not example_target.exists():
4433
+ return []
4434
+
4435
+ failures: list[Failure] = []
4436
+
4437
+ def write_json(path: Path, payload: object) -> None:
4438
+ path.parent.mkdir(parents=True, exist_ok=True)
4439
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
4440
+
4441
+ def install_interop(
4442
+ target: Path,
4443
+ *,
4444
+ interop: dict[str, object] | None = None,
4445
+ ) -> None:
4446
+ companion_dir = target / ".loom" / "companion"
4447
+ companion_dir.mkdir(parents=True, exist_ok=True)
4448
+ (target / "host").mkdir(parents=True, exist_ok=True)
4449
+ (target / "native").mkdir(parents=True, exist_ok=True)
4450
+ (target / ".loom" / "shadow").mkdir(parents=True, exist_ok=True)
4451
+ (target / "native" / "status").mkdir(parents=True, exist_ok=True)
4452
+ for relative, payload in {
4453
+ ".loom/shadow/admission-loom.json": {"result": "pass"},
4454
+ ".loom/shadow/admission-repo.json": {"result": "pass"},
4455
+ ".loom/shadow/review-loom.json": {"decision": "allow"},
4456
+ ".loom/shadow/review-repo.json": {"decision": "allow"},
4457
+ ".loom/shadow/merge-ready-loom.json": {"status": "pass"},
4458
+ ".loom/shadow/merge-ready-repo.json": {"status": "pass"},
4459
+ ".loom/shadow/closeout-loom.json": {"status": "done"},
4460
+ ".loom/shadow/closeout-repo.json": {"status": "done"},
4461
+ "host/guardian-review.json": {"verdict": "allow"},
4462
+ }.items():
4463
+ write_json(target / relative, payload)
4464
+ if interop is not None:
4465
+ write_json(companion_dir / "interop.json", interop)
4466
+
4467
+ valid_interop = {
4468
+ "schema_version": "loom-repo-interop/v1",
4469
+ "host_adapters": [
4470
+ {
4471
+ "id": "guardian-review",
4472
+ "summary": "Read guardian review verdicts without reimplementing the host action.",
4473
+ "surfaces": ["review", "merge_ready"],
4474
+ "locator": "host/guardian-review.json",
4475
+ }
4476
+ ],
4477
+ "repo_native_carriers": [
4478
+ {
4479
+ "id": "governance-status",
4480
+ "summary": "Read repo-native governance status output without migrating carriers.",
4481
+ "surfaces": ["admission", "review", "merge_ready", "closeout"],
4482
+ "locator": "native/status",
4483
+ }
4484
+ ],
4485
+ "shadow_surfaces": {
4486
+ "admission": {
4487
+ "summary": "Compare admission parity.",
4488
+ "loom_locator": ".loom/shadow/admission-loom.json",
4489
+ "repo_locator": ".loom/shadow/admission-repo.json",
4490
+ },
4491
+ "review": {
4492
+ "summary": "Compare review parity.",
4493
+ "loom_locator": ".loom/shadow/review-loom.json",
4494
+ "repo_locator": ".loom/shadow/review-repo.json",
4495
+ },
4496
+ "merge_ready": {
4497
+ "summary": "Compare merge-ready parity.",
4498
+ "loom_locator": ".loom/shadow/merge-ready-loom.json",
4499
+ "repo_locator": ".loom/shadow/merge-ready-repo.json",
4500
+ },
4501
+ "closeout": {
4502
+ "summary": "Compare closeout parity.",
4503
+ "loom_locator": ".loom/shadow/closeout-loom.json",
4504
+ "repo_locator": ".loom/shadow/closeout-repo.json",
4505
+ },
4506
+ },
4507
+ }
4508
+
4509
+ with tempfile.TemporaryDirectory(prefix="loom-check-repo-interop-") as tmp:
4510
+ base = Path(tmp)
4511
+
4512
+ absent_target = base / "absent"
4513
+ shutil.copytree(example_target, absent_target)
4514
+ absent_surface = build_governance_surface(absent_target)
4515
+ repo_interop = absent_surface.get("repo_interop")
4516
+ require_repo_interop_payload(
4517
+ failures,
4518
+ category="repo-interop",
4519
+ context="absent repo interop",
4520
+ payload=repo_interop,
4521
+ )
4522
+ if not isinstance(repo_interop, dict) or repo_interop.get("availability") != "absent":
4523
+ failures.append(Failure("repo-interop", "absent repo interop sample must report `availability: absent`"))
4524
+
4525
+ invalid_target = base / "invalid"
4526
+ shutil.copytree(example_target, invalid_target)
4527
+ install_interop(
4528
+ invalid_target,
4529
+ interop={
4530
+ "schema_version": "loom-repo-interop/v1",
4531
+ "host_adapters": [
4532
+ {
4533
+ "id": "bad-adapter",
4534
+ "summary": "Broken adapter",
4535
+ "surfaces": ["guardian"],
4536
+ "locator": "host/missing.json",
4537
+ }
4538
+ ],
4539
+ "repo_native_carriers": [],
4540
+ "shadow_surfaces": {
4541
+ "admission": {
4542
+ "summary": "Compare admission parity.",
4543
+ "loom_locator": ".loom/shadow/admission-loom.json",
4544
+ "repo_locator": ".loom/shadow/admission-repo.json",
4545
+ }
4546
+ },
4547
+ },
4548
+ )
4549
+ invalid_surface = build_governance_surface(invalid_target)
4550
+ invalid_interop = invalid_surface.get("repo_interop")
4551
+ require_repo_interop_payload(
4552
+ failures,
4553
+ category="repo-interop",
4554
+ context="invalid repo interop",
4555
+ payload=invalid_interop,
4556
+ )
4557
+ if not isinstance(invalid_interop, dict) or invalid_interop.get("availability") != "incomplete":
4558
+ failures.append(Failure("repo-interop", "invalid repo interop sample must report `availability: incomplete`"))
4559
+
4560
+ present_target = base / "present"
4561
+ shutil.copytree(example_target, present_target)
4562
+ install_interop(present_target, interop=valid_interop)
4563
+ present_surface = build_governance_surface(present_target)
4564
+ present_interop = present_surface.get("repo_interop")
4565
+ require_repo_interop_payload(
4566
+ failures,
4567
+ category="repo-interop",
4568
+ context="present repo interop",
4569
+ payload=present_interop,
4570
+ )
4571
+ if not isinstance(present_interop, dict) or present_interop.get("availability") != "present":
4572
+ failures.append(Failure("repo-interop", "present repo interop sample must report `availability: present`"))
4573
+
4574
+ parity_payload, error = load_command_json(
4575
+ root,
4576
+ ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(present_target)],
4577
+ )
4578
+ if error:
4579
+ failures.append(Failure("repo-interop", f"`shadow-parity` sample failed: {error}"))
4580
+ else:
4581
+ require_shadow_parity_payload(
4582
+ failures,
4583
+ category="repo-interop",
4584
+ context="`shadow-parity` present sample",
4585
+ payload=parity_payload,
4586
+ expected_reports=4,
4587
+ )
4588
+ if parity_payload.get("result") != "pass":
4589
+ failures.append(Failure("repo-interop", "`shadow-parity` must pass when all declared surfaces match"))
4590
+
4591
+ mismatch_target = base / "mismatch"
4592
+ shutil.copytree(present_target, mismatch_target)
4593
+ write_json(mismatch_target / ".loom/shadow/review-repo.json", {"decision": "block"})
4594
+ mismatch_payload, error = load_command_json(
4595
+ root,
4596
+ ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(mismatch_target), "--surface", "review"],
4597
+ )
4598
+ if error:
4599
+ failures.append(Failure("repo-interop", f"`shadow-parity` mismatch sample failed: {error}"))
4600
+ else:
4601
+ require_shadow_parity_payload(
4602
+ failures,
4603
+ category="repo-interop",
4604
+ context="`shadow-parity` mismatch sample",
4605
+ payload=mismatch_payload,
4606
+ expected_reports=1,
4607
+ )
4608
+ reports = mismatch_payload.get("reports")
4609
+ if not isinstance(reports, list) or not reports or reports[0].get("result") != "mismatch":
4610
+ failures.append(Failure("repo-interop", "`shadow-parity` mismatch sample must report `mismatch`"))
4611
+
4612
+ return failures
4613
+
4614
+
4615
+ def is_within(path: Path, root: Path) -> bool:
4616
+ try:
4617
+ path.relative_to(root)
4618
+ return True
4619
+ except ValueError:
4620
+ return False
4621
+
4622
+
4623
+ def collect_failures(root: Path) -> list[Failure]:
4624
+ failures: list[Failure] = []
4625
+ failures.extend(check_required_paths(root, "top-level-dirs", TOP_LEVEL_DIRS))
4626
+ failures.extend(check_required_paths(root, "top-level-files", TOP_LEVEL_FILES))
4627
+ failures.extend(check_required_paths(root, "area-readmes", AREA_READMES))
4628
+ failures.extend(check_required_paths(root, "core-docs", CORE_DOCS))
4629
+ failures.extend(
4630
+ check_required_paths(root, "automation-frontload-templates", AUTOMATION_FRONTLOAD_TEMPLATES)
4631
+ )
4632
+ failures.extend(check_required_paths(root, "automation-frontload-skills", AUTOMATION_FRONTLOAD_SKILLS))
4633
+ failures.extend(
4634
+ check_required_paths(
4635
+ root,
4636
+ "automation-frontload-execution-support",
4637
+ AUTOMATION_FRONTLOAD_EXECUTION_SUPPORT,
4638
+ )
4639
+ )
4640
+ failures.extend(check_skill_manifests(root))
4641
+ failures.extend(check_skill_routing(root))
4642
+ failures.extend(check_demo_assets(root))
4643
+ failures.extend(check_demo_fact_chain(root))
4644
+ failures.extend(check_demo_repo_local_cli(root))
4645
+ failures.extend(check_deep_existing_repo_bootstrap(root))
4646
+ failures.extend(check_daily_execution_cli(root))
4647
+ failures.extend(check_repo_companion_interface_contracts(root))
4648
+ failures.extend(check_repo_interop_contracts(root))
4649
+ failures.extend(check_markdown_links(root))
4650
+ return failures
4651
+
4652
+
4653
+ def print_report(root: Path, failures: list[Failure]) -> None:
4654
+ categories_checked = 15
4655
+ if not failures:
4656
+ print(f"loom_check: OK ({root})")
4657
+ print(f"checked {categories_checked} surfaces")
4658
+ return
4659
+
4660
+ grouped: dict[str, list[str]] = defaultdict(list)
4661
+ for failure in failures:
4662
+ grouped[failure.category].append(failure.detail)
4663
+
4664
+ print(f"loom_check: FAILED ({root})")
4665
+ for category in sorted(grouped):
4666
+ print(f"- {category}")
4667
+ for detail in grouped[category]:
4668
+ print(f" - {detail}")
4669
+ print(f"failures: {len(failures)} across {len(grouped)} categories")
4670
+
4671
+
4672
+ def main(argv: list[str]) -> int:
4673
+ root = repo_root_from_argv(argv)
4674
+ if not root.exists():
4675
+ print(f"loom_check: repo root does not exist: {root}", file=sys.stderr)
4676
+ return 2
4677
+ failures = collect_failures(root)
4678
+ print_report(root, failures)
4679
+ return 1 if failures else 0
4680
+
4681
+
4682
+ if __name__ == "__main__":
4683
+ sys.exit(main(sys.argv))