rig-create 0.0.0-stage → 1.0.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 (555) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -2
  3. package/docs/CHECKS.md +1682 -0
  4. package/docs/COLLECTIONS.md +337 -0
  5. package/docs/CRAFT.md +277 -0
  6. package/docs/INTERVIEW.md +886 -0
  7. package/docs/LESSONS.md +172 -0
  8. package/docs/PROJECT.md +2657 -0
  9. package/docs/SOURCE.md +225 -0
  10. package/package.json +44 -4
  11. package/src/agent-bounce.ts +27 -0
  12. package/src/agent-guide.ts +237 -0
  13. package/src/amend.ts +193 -0
  14. package/src/applied-review.ts +104 -0
  15. package/src/ask-coverage.ts +69 -0
  16. package/src/assemble-answers.ts +53 -0
  17. package/src/cg-gates.ts +211 -0
  18. package/src/check-reads.ts +106 -0
  19. package/src/check.ts +1059 -0
  20. package/src/checkpoint-evidence-writer.ts +125 -0
  21. package/src/checkpoint-evidence.ts +152 -0
  22. package/src/choice-writers.ts +271 -0
  23. package/src/choices.ts +378 -0
  24. package/src/cli.ts +371 -0
  25. package/src/collection-answer.ts +136 -0
  26. package/src/collection-binding-reader.ts +496 -0
  27. package/src/collection-binding.ts +478 -0
  28. package/src/collection.ts +348 -0
  29. package/src/config-inputs.ts +155 -0
  30. package/src/config-lost.ts +135 -0
  31. package/src/correction-writers.ts +207 -0
  32. package/src/corrections.ts +685 -0
  33. package/src/diff-classes.ts +219 -0
  34. package/src/diff-map.ts +248 -0
  35. package/src/direction-citation.ts +13 -0
  36. package/src/direction-writers.ts +119 -0
  37. package/src/direction.ts +266 -0
  38. package/src/drafts.ts +363 -0
  39. package/src/feedback.ts +744 -0
  40. package/src/figure-mask.ts +143 -0
  41. package/src/fix-ledger.ts +355 -0
  42. package/src/init.ts +265 -0
  43. package/src/inputs.ts +160 -0
  44. package/src/intake.ts +158 -0
  45. package/src/interview.ts +53 -0
  46. package/src/invariants.ts +132 -0
  47. package/src/job-acts.ts +80 -0
  48. package/src/job-advance.ts +65 -0
  49. package/src/job-close.ts +226 -0
  50. package/src/job-host.ts +529 -0
  51. package/src/job-outbox.ts +383 -0
  52. package/src/job-publication.ts +701 -0
  53. package/src/job-runtime.ts +99 -0
  54. package/src/job-writer.ts +123 -0
  55. package/src/meshes-bust.ts +63 -0
  56. package/src/next.ts +1506 -0
  57. package/src/overrides.ts +42 -0
  58. package/src/package-upgrades.ts +32 -0
  59. package/src/packages-upgrade-check.ts +6 -0
  60. package/src/packages-upgrade.ts +168 -0
  61. package/src/parts-act.ts +96 -0
  62. package/src/parts-check.ts +242 -0
  63. package/src/parts-review.ts +196 -0
  64. package/src/parts-sections.ts +98 -0
  65. package/src/parts-verdict-record.ts +265 -0
  66. package/src/parts-verdict.ts +33 -0
  67. package/src/pause.ts +118 -0
  68. package/src/pins-check.ts +619 -0
  69. package/src/pins.ts +17 -0
  70. package/src/plan-bar-amendments.ts +54 -0
  71. package/src/plan-history.ts +147 -0
  72. package/src/plan-writers.ts +202 -0
  73. package/src/plate-receipt.ts +110 -0
  74. package/src/pre-interview.ts +302 -0
  75. package/src/prepared-inputs.ts +113 -0
  76. package/src/progress-writers.ts +181 -0
  77. package/src/provenance.ts +301 -0
  78. package/src/record-direction-target.ts +26 -0
  79. package/src/record-input.ts +18 -0
  80. package/src/refusal-baseline.ts +55 -0
  81. package/src/refusal.ts +30 -0
  82. package/src/reopen-lapses.ts +80 -0
  83. package/src/reopen.ts +144 -0
  84. package/src/republish.ts +93 -0
  85. package/src/restart.ts +141 -0
  86. package/src/review-act-runtime.ts +16 -0
  87. package/src/review-act.ts +592 -0
  88. package/src/review-package.ts +524 -0
  89. package/src/review-read.ts +238 -0
  90. package/src/review.ts +432 -0
  91. package/src/rig-version-date.ts +22 -0
  92. package/src/rig-version.ts +1047 -0
  93. package/src/run-record.ts +232 -0
  94. package/src/scene-authority.ts +410 -0
  95. package/src/scene-writers.ts +144 -0
  96. package/src/scene.ts +284 -0
  97. package/src/setup-painting.ts +46 -0
  98. package/src/signable.ts +65 -0
  99. package/src/signoff-answers.ts +76 -0
  100. package/src/signoff-checkpoint-act.ts +95 -0
  101. package/src/signoff-target.ts +127 -0
  102. package/src/signoff.ts +331 -0
  103. package/src/split-captions.ts +154 -0
  104. package/src/stage-progress.ts +379 -0
  105. package/src/stages.ts +148 -0
  106. package/src/step-hash.ts +19 -0
  107. package/src/structure-snapshot.ts +109 -0
  108. package/src/structure.ts +166 -0
  109. package/src/templates.ts +16 -0
  110. package/src/waypoints.ts +163 -0
  111. package/templates/agent-guide.md +108 -0
  112. package/templates/collection-agent-guide.md +72 -0
  113. package/templates/collection.json +252 -0
  114. package/templates/example-chain/AGENTS.md +134 -0
  115. package/templates/example-chain/CLAUDE.md +3 -0
  116. package/templates/example-chain/README.md +13 -0
  117. package/templates/example-chain/chain.json +2869 -0
  118. package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +134 -0
  119. package/templates/example-chain/checkpoints/0a-interview-options/CLAUDE.md +3 -0
  120. package/templates/example-chain/checkpoints/0a-interview-options/evidence/.gitkeep +0 -0
  121. package/templates/example-chain/checkpoints/0a-interview-options/evidence/approval.txt +1 -0
  122. package/templates/example-chain/checkpoints/0a-interview-options/evidence/interview/rig_type.json +59 -0
  123. package/templates/example-chain/checkpoints/0a-interview-options/evidence/split-config.json +1 -0
  124. package/templates/example-chain/checkpoints/0a-interview-options/inputs/painting.png +0 -0
  125. package/templates/example-chain/checkpoints/0a-interview-options/inputs/references/.gitkeep +0 -0
  126. package/templates/example-chain/checkpoints/0a-interview-options/package.json +9 -0
  127. package/templates/example-chain/checkpoints/0a-interview-options/project.json +295 -0
  128. package/templates/example-chain/checkpoints/0a-interview-options.json +127 -0
  129. package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +134 -0
  130. package/templates/example-chain/checkpoints/0b-plan-pending/CLAUDE.md +3 -0
  131. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/.gitkeep +0 -0
  132. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/approval.txt +1 -0
  133. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/assemble.json +309 -0
  134. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/reference.json +59 -0
  135. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/rig_type.json +59 -0
  136. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/seethrough.json +186 -0
  137. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/split-config.json +1 -0
  138. package/templates/example-chain/checkpoints/0b-plan-pending/inputs/painting.png +0 -0
  139. package/templates/example-chain/checkpoints/0b-plan-pending/inputs/references/.gitkeep +0 -0
  140. package/templates/example-chain/checkpoints/0b-plan-pending/package.json +9 -0
  141. package/templates/example-chain/checkpoints/0b-plan-pending/project.json +685 -0
  142. package/templates/example-chain/checkpoints/0b-plan-pending.json +81 -0
  143. package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +134 -0
  144. package/templates/example-chain/checkpoints/1-parts-verdict/CLAUDE.md +3 -0
  145. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/.gitkeep +0 -0
  146. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/approval.txt +1 -0
  147. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/assemble.json +309 -0
  148. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/reference.json +59 -0
  149. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/rig_type.json +59 -0
  150. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/seethrough.json +186 -0
  151. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/parts-check.json +127 -0
  152. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/split-config.json +1 -0
  153. package/templates/example-chain/checkpoints/1-parts-verdict/inputs/painting.png +0 -0
  154. package/templates/example-chain/checkpoints/1-parts-verdict/inputs/references/.gitkeep +0 -0
  155. package/templates/example-chain/checkpoints/1-parts-verdict/package.json +9 -0
  156. package/templates/example-chain/checkpoints/1-parts-verdict/project.json +955 -0
  157. package/templates/example-chain/checkpoints/1-parts-verdict.json +204 -0
  158. package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +134 -0
  159. package/templates/example-chain/checkpoints/2-rigging-answers/CLAUDE.md +3 -0
  160. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/.gitkeep +0 -0
  161. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/approval.txt +1 -0
  162. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/diff-components.json +34 -0
  163. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/assemble.json +309 -0
  164. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/bones.json +190 -0
  165. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/reference.json +59 -0
  166. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/rig_type.json +59 -0
  167. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/seethrough.json +186 -0
  168. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-check.json +127 -0
  169. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-review.json +88 -0
  170. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
  171. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/split-config.json +1 -0
  172. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/structure.json +30 -0
  173. package/templates/example-chain/checkpoints/2-rigging-answers/inputs/painting.png +0 -0
  174. package/templates/example-chain/checkpoints/2-rigging-answers/inputs/references/.gitkeep +0 -0
  175. package/templates/example-chain/checkpoints/2-rigging-answers/package.json +9 -0
  176. package/templates/example-chain/checkpoints/2-rigging-answers/project.json +1198 -0
  177. package/templates/example-chain/checkpoints/2-rigging-answers.json +254 -0
  178. package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +134 -0
  179. package/templates/example-chain/checkpoints/3-rig-review/CLAUDE.md +3 -0
  180. package/templates/example-chain/checkpoints/3-rig-review/evidence/.gitkeep +0 -0
  181. package/templates/example-chain/checkpoints/3-rig-review/evidence/approval.txt +1 -0
  182. package/templates/example-chain/checkpoints/3-rig-review/evidence/diff-components.json +34 -0
  183. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/assemble.json +309 -0
  184. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/bones.json +190 -0
  185. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/constraints.json +272 -0
  186. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/meshes.json +190 -0
  187. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +272 -0
  188. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/reference.json +59 -0
  189. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/rig_type.json +59 -0
  190. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/seethrough.json +186 -0
  191. package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-check.json +127 -0
  192. package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-review.json +88 -0
  193. package/templates/example-chain/checkpoints/3-rig-review/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
  194. package/templates/example-chain/checkpoints/3-rig-review/evidence/setup.png +0 -0
  195. package/templates/example-chain/checkpoints/3-rig-review/evidence/split-config.json +1 -0
  196. package/templates/example-chain/checkpoints/3-rig-review/evidence/structure.json +30 -0
  197. package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/config.json +88 -0
  198. package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/interview-requirements.json +8 -0
  199. package/templates/example-chain/checkpoints/3-rig-review/inputs/painting.png +0 -0
  200. package/templates/example-chain/checkpoints/3-rig-review/inputs/references/.gitkeep +0 -0
  201. package/templates/example-chain/checkpoints/3-rig-review/package.json +9 -0
  202. package/templates/example-chain/checkpoints/3-rig-review/project.json +1909 -0
  203. package/templates/example-chain/checkpoints/3-rig-review.json +424 -0
  204. package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +134 -0
  205. package/templates/example-chain/checkpoints/4-direction-post/CLAUDE.md +3 -0
  206. package/templates/example-chain/checkpoints/4-direction-post/evidence/.gitkeep +0 -0
  207. package/templates/example-chain/checkpoints/4-direction-post/evidence/approval.txt +1 -0
  208. package/templates/example-chain/checkpoints/4-direction-post/evidence/diff-components.json +34 -0
  209. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/assemble.json +309 -0
  210. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/bones.json +190 -0
  211. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +102 -0
  212. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/constraints.json +272 -0
  213. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/meshes.json +190 -0
  214. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +272 -0
  215. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +122 -0
  216. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +102 -0
  217. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/reference.json +59 -0
  218. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/rig_type.json +59 -0
  219. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/seethrough.json +186 -0
  220. package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-check.json +127 -0
  221. package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-review.json +88 -0
  222. package/templates/example-chain/checkpoints/4-direction-post/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
  223. package/templates/example-chain/checkpoints/4-direction-post/evidence/setup.png +0 -0
  224. package/templates/example-chain/checkpoints/4-direction-post/evidence/split-config.json +1 -0
  225. package/templates/example-chain/checkpoints/4-direction-post/evidence/structure.json +30 -0
  226. package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/config.json +88 -0
  227. package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/interview-requirements.json +8 -0
  228. package/templates/example-chain/checkpoints/4-direction-post/inputs/painting.png +0 -0
  229. package/templates/example-chain/checkpoints/4-direction-post/inputs/references/.gitkeep +0 -0
  230. package/templates/example-chain/checkpoints/4-direction-post/package.json +9 -0
  231. package/templates/example-chain/checkpoints/4-direction-post/project.json +2302 -0
  232. package/templates/example-chain/checkpoints/4-direction-post.json +456 -0
  233. package/templates/example-chain/evidence/.gitkeep +0 -0
  234. package/templates/example-chain/evidence/approval.txt +1 -0
  235. package/templates/example-chain/evidence/captions.json +101 -0
  236. package/templates/example-chain/evidence/diff-components.json +34 -0
  237. package/templates/example-chain/evidence/direction/v2.json +74 -0
  238. package/templates/example-chain/evidence/direction-input.json +74 -0
  239. package/templates/example-chain/evidence/interview/assemble.json +309 -0
  240. package/templates/example-chain/evidence/interview/bones.json +190 -0
  241. package/templates/example-chain/evidence/interview/camera_depth.json +102 -0
  242. package/templates/example-chain/evidence/interview/constraints.json +272 -0
  243. package/templates/example-chain/evidence/interview/meshes.json +190 -0
  244. package/templates/example-chain/evidence/interview/motion.json +272 -0
  245. package/templates/example-chain/evidence/interview/post_composition.json +122 -0
  246. package/templates/example-chain/evidence/interview/post_timing.json +102 -0
  247. package/templates/example-chain/evidence/interview/reference.json +59 -0
  248. package/templates/example-chain/evidence/interview/rig_type.json +59 -0
  249. package/templates/example-chain/evidence/interview/seethrough.json +186 -0
  250. package/templates/example-chain/evidence/parts-check.json +127 -0
  251. package/templates/example-chain/evidence/parts-review.json +88 -0
  252. package/templates/example-chain/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
  253. package/templates/example-chain/evidence/scene/s1.json +65 -0
  254. package/templates/example-chain/evidence/scene-citations.json +9 -0
  255. package/templates/example-chain/evidence/setup.png +0 -0
  256. package/templates/example-chain/evidence/split-config.json +1 -0
  257. package/templates/example-chain/evidence/structure.json +30 -0
  258. package/templates/example-chain/evidence/v1/config.json +88 -0
  259. package/templates/example-chain/evidence/v1/interview-requirements.json +8 -0
  260. package/templates/example-chain/inputs/background.png +0 -0
  261. package/templates/example-chain/inputs/painting.png +0 -0
  262. package/templates/example-chain/inputs/references/.gitkeep +0 -0
  263. package/templates/example-chain/package.json +9 -0
  264. package/templates/example-chain/project.json +2593 -0
  265. package/templates/example-diff/diff-components.json +71 -0
  266. package/templates/example-diff/recomposite_rig.png +0 -0
  267. package/templates/example-direction/approval.txt +1 -0
  268. package/templates/example-direction/derived/review/v1/out/check/build/rectangle.png +0 -0
  269. package/templates/example-direction/derived/review/v1/out/check/build/run.json +40 -0
  270. package/templates/example-direction/derived/review/v1/out/check/build/skeleton.atlas +8 -0
  271. package/templates/example-direction/derived/review/v1/out/check/build/skeleton.json +73 -0
  272. package/templates/example-direction/derived/review/v1/out/check/build/skeleton.model.json +195 -0
  273. package/templates/example-direction/evidence/direction/v2.json +69 -0
  274. package/templates/example-direction/evidence/direction/v3.json +59 -0
  275. package/templates/example-direction/evidence/images/rectangle.png +0 -0
  276. package/templates/example-direction/evidence/input.json +69 -0
  277. package/templates/example-direction/evidence/motion.json +33 -0
  278. package/templates/example-direction/evidence/patch.json +23 -0
  279. package/templates/example-direction/evidence/rig-config.json +55 -0
  280. package/templates/example-direction/evidence/still.png +0 -0
  281. package/templates/example-direction/project.json +372 -0
  282. package/templates/example-direction/reasoning.txt +1 -0
  283. package/templates/example-direction/words.txt +1 -0
  284. package/templates/example-draft/README.md +28 -0
  285. package/templates/example-draft/config.json +71 -0
  286. package/templates/example-draft/derived/drafts/lantern/config.json +71 -0
  287. package/templates/example-draft/derived/drafts/lantern/input/painting_in_rig.png +0 -0
  288. package/templates/example-draft/derived/drafts/lantern/input/parts/background.png +0 -0
  289. package/templates/example-draft/derived/drafts/lantern/input/parts/eye_l.png +0 -0
  290. package/templates/example-draft/derived/drafts/lantern/input/parts/eye_r.png +0 -0
  291. package/templates/example-draft/derived/drafts/lantern/input/parts/face.png +0 -0
  292. package/templates/example-draft/derived/drafts/lantern/input/parts/lantern.png +0 -0
  293. package/templates/example-draft/derived/drafts/lantern/input/parts/torso.png +0 -0
  294. package/templates/example-draft/derived/drafts/lantern/input/parts.json +188 -0
  295. package/templates/example-draft/derived/drafts/lantern/input/recomposite_rig.png +0 -0
  296. package/templates/example-draft/derived/drafts/lantern/interview-requirements.json +8 -0
  297. package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +40 -0
  298. package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.atlas +28 -0
  299. package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.json +193 -0
  300. package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.model.json +483 -0
  301. package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.png +0 -0
  302. package/templates/example-draft/derived/drafts/lantern/out/images/background.png +0 -0
  303. package/templates/example-draft/derived/drafts/lantern/out/images/eye_l.png +0 -0
  304. package/templates/example-draft/derived/drafts/lantern/out/images/eye_r.png +0 -0
  305. package/templates/example-draft/derived/drafts/lantern/out/images/face.png +0 -0
  306. package/templates/example-draft/derived/drafts/lantern/out/images/lantern.png +0 -0
  307. package/templates/example-draft/derived/drafts/lantern/out/images/torso.png +0 -0
  308. package/templates/example-draft/derived/drafts/lantern/out/mesh_report.json +1 -0
  309. package/templates/example-draft/derived/drafts/lantern/out/motion.json +137 -0
  310. package/templates/example-draft/derived/drafts/lantern/out/rig.json +102 -0
  311. package/templates/example-draft/derived/drafts/lantern/out/run.json +52 -0
  312. package/templates/example-draft/derived/parts/rig/painting_in_rig.png +0 -0
  313. package/templates/example-draft/derived/parts/rig/parts/background.png +0 -0
  314. package/templates/example-draft/derived/parts/rig/parts/eye_l.png +0 -0
  315. package/templates/example-draft/derived/parts/rig/parts/eye_r.png +0 -0
  316. package/templates/example-draft/derived/parts/rig/parts/face.png +0 -0
  317. package/templates/example-draft/derived/parts/rig/parts/lantern.png +0 -0
  318. package/templates/example-draft/derived/parts/rig/parts/torso.png +0 -0
  319. package/templates/example-draft/derived/parts/rig/parts.json +188 -0
  320. package/templates/example-draft/derived/parts/rig/recomposite_rig.png +0 -0
  321. package/templates/example-draft/derived/review/v1/config.json +71 -0
  322. package/templates/example-draft/derived/review/v1/interview-requirements.json +8 -0
  323. package/templates/example-draft/derived/review/v1/out/check/build/run.json +40 -0
  324. package/templates/example-draft/derived/review/v1/out/check/build/skeleton.atlas +28 -0
  325. package/templates/example-draft/derived/review/v1/out/check/build/skeleton.json +193 -0
  326. package/templates/example-draft/derived/review/v1/out/check/build/skeleton.model.json +483 -0
  327. package/templates/example-draft/derived/review/v1/out/check/build/skeleton.png +0 -0
  328. package/templates/example-draft/derived/review/v1/out/parts/background.png +0 -0
  329. package/templates/example-draft/derived/review/v1/out/parts/eye_l.png +0 -0
  330. package/templates/example-draft/derived/review/v1/out/parts/eye_r.png +0 -0
  331. package/templates/example-draft/derived/review/v1/out/parts/face.png +0 -0
  332. package/templates/example-draft/derived/review/v1/out/parts/lantern.png +0 -0
  333. package/templates/example-draft/derived/review/v1/out/parts/torso.png +0 -0
  334. package/templates/example-draft/derived/review/v1/out/parts.json +188 -0
  335. package/templates/example-draft/derived/review/v1/out/rig/images/background.png +0 -0
  336. package/templates/example-draft/derived/review/v1/out/rig/images/eye_l.png +0 -0
  337. package/templates/example-draft/derived/review/v1/out/rig/images/eye_r.png +0 -0
  338. package/templates/example-draft/derived/review/v1/out/rig/images/face.png +0 -0
  339. package/templates/example-draft/derived/review/v1/out/rig/images/lantern.png +0 -0
  340. package/templates/example-draft/derived/review/v1/out/rig/images/torso.png +0 -0
  341. package/templates/example-draft/derived/review/v1/out/rig/mesh_report.json +1 -0
  342. package/templates/example-draft/derived/review/v1/out/rig/motion.json +137 -0
  343. package/templates/example-draft/derived/review/v1/out/rig/rig.json +102 -0
  344. package/templates/example-draft/derived/review/v1/out/rig/run.json +52 -0
  345. package/templates/example-draft/derived/review/v2/config.json +71 -0
  346. package/templates/example-draft/derived/review/v2/interview-requirements.json +8 -0
  347. package/templates/example-draft/derived/review/v2/out/check/build/run.json +40 -0
  348. package/templates/example-draft/derived/review/v2/out/check/build/skeleton.atlas +28 -0
  349. package/templates/example-draft/derived/review/v2/out/check/build/skeleton.json +193 -0
  350. package/templates/example-draft/derived/review/v2/out/check/build/skeleton.model.json +483 -0
  351. package/templates/example-draft/derived/review/v2/out/check/build/skeleton.png +0 -0
  352. package/templates/example-draft/derived/review/v2/out/parts/background.png +0 -0
  353. package/templates/example-draft/derived/review/v2/out/parts/eye_l.png +0 -0
  354. package/templates/example-draft/derived/review/v2/out/parts/eye_r.png +0 -0
  355. package/templates/example-draft/derived/review/v2/out/parts/face.png +0 -0
  356. package/templates/example-draft/derived/review/v2/out/parts/lantern.png +0 -0
  357. package/templates/example-draft/derived/review/v2/out/parts/torso.png +0 -0
  358. package/templates/example-draft/derived/review/v2/out/parts.json +188 -0
  359. package/templates/example-draft/derived/review/v2/out/rig/images/background.png +0 -0
  360. package/templates/example-draft/derived/review/v2/out/rig/images/eye_l.png +0 -0
  361. package/templates/example-draft/derived/review/v2/out/rig/images/eye_r.png +0 -0
  362. package/templates/example-draft/derived/review/v2/out/rig/images/face.png +0 -0
  363. package/templates/example-draft/derived/review/v2/out/rig/images/lantern.png +0 -0
  364. package/templates/example-draft/derived/review/v2/out/rig/images/torso.png +0 -0
  365. package/templates/example-draft/derived/review/v2/out/rig/mesh_report.json +1 -0
  366. package/templates/example-draft/derived/review/v2/out/rig/motion.json +137 -0
  367. package/templates/example-draft/derived/review/v2/out/rig/rig.json +102 -0
  368. package/templates/example-draft/derived/review/v2/out/rig/run.json +52 -0
  369. package/templates/example-draft/evidence/diff-components.json +71 -0
  370. package/templates/example-draft/evidence/direction/v4.json +64 -0
  371. package/templates/example-draft/evidence/direction-input-v4.json +64 -0
  372. package/templates/example-draft/evidence/fix-ledger/lantern.json +17 -0
  373. package/templates/example-draft/evidence/parts-check.json +127 -0
  374. package/templates/example-draft/evidence/parts-review.json +88 -0
  375. package/templates/example-draft/evidence/review-reads/4ed0507381809746443021554e012032be5b5f8417da0f2e525e9b6df16e9f05.json +59 -0
  376. package/templates/example-draft/evidence/scene/s1.json +65 -0
  377. package/templates/example-draft/evidence/scene-background.png +0 -0
  378. package/templates/example-draft/evidence/scene-citations.json +9 -0
  379. package/templates/example-draft/evidence/structure-decisions.json +9 -0
  380. package/templates/example-draft/evidence/v1/interview-config.json +71 -0
  381. package/templates/example-draft/evidence/v1/interview-requirements.json +8 -0
  382. package/templates/example-draft/evidence/v1/review-config.json +71 -0
  383. package/templates/example-draft/evidence/v2/interview-config.json +71 -0
  384. package/templates/example-draft/evidence/v2/interview-requirements.json +8 -0
  385. package/templates/example-draft/evidence/v2/review-config.json +71 -0
  386. package/templates/example-draft/input/painting_in_rig.png +0 -0
  387. package/templates/example-draft/input/parts/background.png +0 -0
  388. package/templates/example-draft/input/parts/eye_l.png +0 -0
  389. package/templates/example-draft/input/parts/eye_r.png +0 -0
  390. package/templates/example-draft/input/parts/face.png +0 -0
  391. package/templates/example-draft/input/parts/lantern.png +0 -0
  392. package/templates/example-draft/input/parts/torso.png +0 -0
  393. package/templates/example-draft/input/parts.json +188 -0
  394. package/templates/example-draft/input/recomposite_rig.png +0 -0
  395. package/templates/example-draft/inputs/painting.png +0 -0
  396. package/templates/example-draft/interview-requirements.json +8 -0
  397. package/templates/example-draft/out/check/build/run.json +40 -0
  398. package/templates/example-draft/out/check/build/skeleton.atlas +28 -0
  399. package/templates/example-draft/out/check/build/skeleton.json +193 -0
  400. package/templates/example-draft/out/check/build/skeleton.model.json +483 -0
  401. package/templates/example-draft/out/check/build/skeleton.png +0 -0
  402. package/templates/example-draft/out/images/background.png +0 -0
  403. package/templates/example-draft/out/images/eye_l.png +0 -0
  404. package/templates/example-draft/out/images/eye_r.png +0 -0
  405. package/templates/example-draft/out/images/face.png +0 -0
  406. package/templates/example-draft/out/images/lantern.png +0 -0
  407. package/templates/example-draft/out/images/torso.png +0 -0
  408. package/templates/example-draft/out/mesh_report.json +1 -0
  409. package/templates/example-draft/out/motion.json +137 -0
  410. package/templates/example-draft/out/rig.json +102 -0
  411. package/templates/example-draft/out/run.json +52 -0
  412. package/templates/example-draft/outputs/v1/out/check/build/run.json +40 -0
  413. package/templates/example-draft/outputs/v1/out/check/build/skeleton.atlas +28 -0
  414. package/templates/example-draft/outputs/v1/out/check/build/skeleton.json +193 -0
  415. package/templates/example-draft/outputs/v1/out/check/build/skeleton.model.json +483 -0
  416. package/templates/example-draft/outputs/v1/out/check/build/skeleton.png +0 -0
  417. package/templates/example-draft/outputs/v1/out/painting_in_rig.png +0 -0
  418. package/templates/example-draft/outputs/v1/out/parts/background.png +0 -0
  419. package/templates/example-draft/outputs/v1/out/parts/eye_l.png +0 -0
  420. package/templates/example-draft/outputs/v1/out/parts/eye_r.png +0 -0
  421. package/templates/example-draft/outputs/v1/out/parts/face.png +0 -0
  422. package/templates/example-draft/outputs/v1/out/parts/lantern.png +0 -0
  423. package/templates/example-draft/outputs/v1/out/parts/torso.png +0 -0
  424. package/templates/example-draft/outputs/v1/out/parts.json +188 -0
  425. package/templates/example-draft/outputs/v1/out/rig/images/background.png +0 -0
  426. package/templates/example-draft/outputs/v1/out/rig/images/eye_l.png +0 -0
  427. package/templates/example-draft/outputs/v1/out/rig/images/eye_r.png +0 -0
  428. package/templates/example-draft/outputs/v1/out/rig/images/face.png +0 -0
  429. package/templates/example-draft/outputs/v1/out/rig/images/lantern.png +0 -0
  430. package/templates/example-draft/outputs/v1/out/rig/images/torso.png +0 -0
  431. package/templates/example-draft/outputs/v1/out/rig/mesh_report.json +1 -0
  432. package/templates/example-draft/outputs/v1/out/rig/motion.json +137 -0
  433. package/templates/example-draft/outputs/v1/out/rig/rig.json +102 -0
  434. package/templates/example-draft/outputs/v1/out/rig/run.json +52 -0
  435. package/templates/example-draft/outputs/v2/out/check/build/run.json +40 -0
  436. package/templates/example-draft/outputs/v2/out/check/build/skeleton.atlas +28 -0
  437. package/templates/example-draft/outputs/v2/out/check/build/skeleton.json +193 -0
  438. package/templates/example-draft/outputs/v2/out/check/build/skeleton.model.json +483 -0
  439. package/templates/example-draft/outputs/v2/out/check/build/skeleton.png +0 -0
  440. package/templates/example-draft/outputs/v2/out/painting_in_rig.png +0 -0
  441. package/templates/example-draft/outputs/v2/out/parts/background.png +0 -0
  442. package/templates/example-draft/outputs/v2/out/parts/eye_l.png +0 -0
  443. package/templates/example-draft/outputs/v2/out/parts/eye_r.png +0 -0
  444. package/templates/example-draft/outputs/v2/out/parts/face.png +0 -0
  445. package/templates/example-draft/outputs/v2/out/parts/lantern.png +0 -0
  446. package/templates/example-draft/outputs/v2/out/parts/torso.png +0 -0
  447. package/templates/example-draft/outputs/v2/out/parts.json +188 -0
  448. package/templates/example-draft/outputs/v2/out/rig/images/background.png +0 -0
  449. package/templates/example-draft/outputs/v2/out/rig/images/eye_l.png +0 -0
  450. package/templates/example-draft/outputs/v2/out/rig/images/eye_r.png +0 -0
  451. package/templates/example-draft/outputs/v2/out/rig/images/face.png +0 -0
  452. package/templates/example-draft/outputs/v2/out/rig/images/lantern.png +0 -0
  453. package/templates/example-draft/outputs/v2/out/rig/images/torso.png +0 -0
  454. package/templates/example-draft/outputs/v2/out/rig/mesh_report.json +1 -0
  455. package/templates/example-draft/outputs/v2/out/rig/motion.json +137 -0
  456. package/templates/example-draft/outputs/v2/out/rig/rig.json +102 -0
  457. package/templates/example-draft/outputs/v2/out/rig/run.json +52 -0
  458. package/templates/example-interview-choice.json +1068 -0
  459. package/templates/example-interview-options/constraint.json +376 -0
  460. package/templates/example-interview-options/example-assemble.json +376 -0
  461. package/templates/example-interview-options/example-bones.json +190 -0
  462. package/templates/example-interview-options/example-camera-depth.json +99 -0
  463. package/templates/example-interview-options/example-constraints.json +272 -0
  464. package/templates/example-interview-options/example-meshes.json +190 -0
  465. package/templates/example-interview-options/example-motion.json +272 -0
  466. package/templates/example-interview-options/example-picks.json +15 -0
  467. package/templates/example-interview-options/example-post-composition.json +110 -0
  468. package/templates/example-interview-options/example-post-timing.json +105 -0
  469. package/templates/example-interview-options/example-reference.json +63 -0
  470. package/templates/example-interview-options/example-rig-type.json +63 -0
  471. package/templates/example-interview-options/example-seethrough.json +190 -0
  472. package/templates/example-interview-options/fullscreen.json +63 -0
  473. package/templates/example-interview-options/other-explicit.json +63 -0
  474. package/templates/example-interview-options/other-pending.json +63 -0
  475. package/templates/example-interview-options/other-picks.json +16 -0
  476. package/templates/example-interview.md +213 -0
  477. package/templates/example-invariants/invariants.json +1339 -0
  478. package/templates/example-prepared/prepared.svg +1 -0
  479. package/templates/example-prepared/source.svg +1 -0
  480. package/templates/example-project.json +4259 -0
  481. package/templates/example-remesh/AGENTS.md +134 -0
  482. package/templates/example-remesh/CLAUDE.md +3 -0
  483. package/templates/example-remesh/evidence/.gitkeep +0 -0
  484. package/templates/example-remesh/evidence/approval.txt +1 -0
  485. package/templates/example-remesh/evidence/captions.json +101 -0
  486. package/templates/example-remesh/evidence/diff-components.json +34 -0
  487. package/templates/example-remesh/evidence/direction/v4.json +74 -0
  488. package/templates/example-remesh/evidence/direction/v5.json +74 -0
  489. package/templates/example-remesh/evidence/direction-input-v4.json +74 -0
  490. package/templates/example-remesh/evidence/direction-input-v5.json +74 -0
  491. package/templates/example-remesh/evidence/interview/assemble.json +309 -0
  492. package/templates/example-remesh/evidence/interview/bones.json +190 -0
  493. package/templates/example-remesh/evidence/interview/camera_depth.json +103 -0
  494. package/templates/example-remesh/evidence/interview/constraints.json +272 -0
  495. package/templates/example-remesh/evidence/interview/meshes.json +190 -0
  496. package/templates/example-remesh/evidence/interview/motion.json +272 -0
  497. package/templates/example-remesh/evidence/interview/post_composition.json +123 -0
  498. package/templates/example-remesh/evidence/interview/post_timing.json +103 -0
  499. package/templates/example-remesh/evidence/interview/reference.json +59 -0
  500. package/templates/example-remesh/evidence/interview/rig_type.json +59 -0
  501. package/templates/example-remesh/evidence/interview/seethrough.json +186 -0
  502. package/templates/example-remesh/evidence/parts-check.json +127 -0
  503. package/templates/example-remesh/evidence/parts-review.json +88 -0
  504. package/templates/example-remesh/evidence/reopen.txt +1 -0
  505. package/templates/example-remesh/evidence/review-reads/6a17db694db96f1b6049a61a017e0c8e66757b982b8851e0187e561cfcf9af68.json +59 -0
  506. package/templates/example-remesh/evidence/scene/s1.json +69 -0
  507. package/templates/example-remesh/evidence/scene-citations.json +9 -0
  508. package/templates/example-remesh/evidence/setup.png +0 -0
  509. package/templates/example-remesh/evidence/split-config.json +1 -0
  510. package/templates/example-remesh/evidence/structure.json +30 -0
  511. package/templates/example-remesh/evidence/v1/registration/at-source.json +44 -0
  512. package/templates/example-remesh/evidence/v1/registration/citations.json +68 -0
  513. package/templates/example-remesh/evidence/v1/registration/compile-report.json +368 -0
  514. package/templates/example-remesh/evidence/v1/registration/config-source.json +88 -0
  515. package/templates/example-remesh/evidence/v1/registration/config.json +88 -0
  516. package/templates/example-remesh/evidence/v1/registration/interview-requirements.json +8 -0
  517. package/templates/example-remesh/evidence/v1/registration/package-lock.json +11 -0
  518. package/templates/example-remesh/evidence/v1/registration/path-adapter.json +1 -0
  519. package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +44 -0
  520. package/templates/example-remesh/evidence/v2/registration/at-source.json +44 -0
  521. package/templates/example-remesh/evidence/v2/registration/citations.json +68 -0
  522. package/templates/example-remesh/evidence/v2/registration/compile-report.json +368 -0
  523. package/templates/example-remesh/evidence/v2/registration/config-source.json +88 -0
  524. package/templates/example-remesh/evidence/v2/registration/config.json +88 -0
  525. package/templates/example-remesh/evidence/v2/registration/interview-requirements.json +8 -0
  526. package/templates/example-remesh/evidence/v2/registration/package-lock.json +11 -0
  527. package/templates/example-remesh/evidence/v2/registration/path-adapter.json +1 -0
  528. package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +44 -0
  529. package/templates/example-remesh/evidence/v3/registration/at-source.json +44 -0
  530. package/templates/example-remesh/evidence/v3/registration/citations.json +68 -0
  531. package/templates/example-remesh/evidence/v3/registration/compile-report.json +368 -0
  532. package/templates/example-remesh/evidence/v3/registration/config-source.json +88 -0
  533. package/templates/example-remesh/evidence/v3/registration/config.json +88 -0
  534. package/templates/example-remesh/evidence/v3/registration/interview-requirements.json +8 -0
  535. package/templates/example-remesh/evidence/v3/registration/package-lock.json +11 -0
  536. package/templates/example-remesh/evidence/v3/registration/path-adapter.json +1 -0
  537. package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +44 -0
  538. package/templates/example-remesh/inputs/background.png +0 -0
  539. package/templates/example-remesh/inputs/painting.png +0 -0
  540. package/templates/example-remesh/inputs/references/.gitkeep +0 -0
  541. package/templates/example-remesh/package.json +9 -0
  542. package/templates/example-remesh/project.json +3916 -0
  543. package/templates/example-reopened/project.json +4568 -0
  544. package/templates/example-review/config.json +1 -0
  545. package/templates/example-review/out/check/build/skeleton.model.json +1 -0
  546. package/templates/example-review/out/parts/piece.svg +1 -0
  547. package/templates/example-review/out/parts.json +1 -0
  548. package/templates/example-review/out/rig/mesh_report.json +1 -0
  549. package/templates/example-review/out/rig/motion.json +1 -0
  550. package/templates/example-review/out/rig/rig.json +1 -0
  551. package/templates/example-split/captions.json +357 -0
  552. package/templates/example-split/config.json +215 -0
  553. package/templates/legacy-example-project.json +1071 -0
  554. package/templates/project.json +199 -0
  555. package/templates/shared/conventions.json +23 -0
package/src/next.ts ADDED
@@ -0,0 +1,1506 @@
1
+ import { checkpointEnabled, checkpointRequired } from './plan-history.ts';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, readFileSync } from './check-reads.ts';
4
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
5
+ import {
6
+ asArray,
7
+ checkProject,
8
+ collectItems,
9
+ collectSignoffs,
10
+ isObject,
11
+ loadCollection,
12
+ postNotPassed,
13
+ type CheckReport,
14
+ type Finding,
15
+ } from './check.ts';
16
+ import { collectAmendments } from './amend.ts';
17
+ import {
18
+ DIFF_MAP_AGENT_SINCE,
19
+ DIFF_MAP_MAX_FLOOR_PX,
20
+ DIFF_MAP_MAX_THRESHOLD,
21
+ diffReportBytes,
22
+ diffStackCandidates,
23
+ reportShape,
24
+ resolveDiffMapPartsFile,
25
+ validateDiffMapRecord,
26
+ } from './diff-map.ts';
27
+ import { JOB_BINDING_FILE, type WriterBinding } from './job-outbox.ts';
28
+ import { isPartsCheckMove, partsCheckRecord } from './parts-check.ts';
29
+ import { recordedPartsVerdict } from './parts-verdict-record.ts';
30
+ import { pausedFrom } from './pause.ts';
31
+ import { carriedRefusals, findingKey, newFindings } from './refusal-baseline.ts';
32
+ import { refuse, USAGE_EXIT } from './refusal.ts';
33
+ import { reopenLapseWarnings } from './reopen-lapses.ts';
34
+ import { collectReopens, liveSignoff } from './reopen.ts';
35
+ import { acceptedReviewVerdict } from './review.ts';
36
+ import {
37
+ advanceOrder,
38
+ furthestStageSignoff,
39
+ isSignoffIdentity,
40
+ moveOwner,
41
+ signoffIdentity,
42
+ STEP_STATUSES,
43
+ } from './stage-progress.ts';
44
+ import {
45
+ hasStageOrder,
46
+ interviewStageOfGuide,
47
+ LEGACY_STAGE_VALUES,
48
+ PARTS_REVIEW_SINCE,
49
+ PAUSED_STAGE,
50
+ STAGE_VALUES,
51
+ stageIndex,
52
+ stageOfStep,
53
+ stepCoveredByApproval,
54
+ } from './stages.ts';
55
+ import type { Json, JsonObject } from './templates.ts';
56
+
57
+ export const NEXT_SCHEMA = 'rig-create/next@1';
58
+
59
+ export const NEXT_HELP = `next [<project dir>] [--json]
60
+ Derive the next moves and whose they are (agent | producer | human) from project.json.
61
+ Refusals come first with their CHECKS.md remedy; writes nothing.`;
62
+
63
+ export type NextRole = 'agent' | 'producer' | 'human';
64
+
65
+ export interface NextRefusal {
66
+ code: string;
67
+ field: string;
68
+ message: string;
69
+ fix: string;
70
+ }
71
+
72
+ export interface NextReviewAct {
73
+ kind: 'parts-verdict' | 'interview-picks' | 'review-verdict' | 'signoff' | 'signoff-checkpoint';
74
+ stage: string;
75
+ draft_id?: string;
76
+ question_id?: string;
77
+ option_set_id?: string;
78
+ version_id?: string;
79
+ review?: string;
80
+ step_id?: string;
81
+ job_id?: string;
82
+ }
83
+
84
+ export interface NextLapse {
85
+ code: string;
86
+ source_field: string;
87
+ target_field: string;
88
+ reopen_id?: string;
89
+ amendment_id?: string;
90
+ message: string;
91
+ }
92
+
93
+ export interface NextMove {
94
+ role: NextRole;
95
+ field: string;
96
+ awaits: string;
97
+ command: string | null;
98
+ act: string;
99
+ review_act?: NextReviewAct;
100
+ lapse?: NextLapse;
101
+ unanswered_by_record?: string;
102
+ }
103
+
104
+ export interface NextReport {
105
+ schema: typeof NEXT_SCHEMA;
106
+ project_id: string;
107
+ stage: string;
108
+ effective_stage: string;
109
+ accepted: boolean;
110
+ refusals: NextRefusal[];
111
+ warnings?: Finding[];
112
+ awaits: string[];
113
+ moves: NextMove[];
114
+ }
115
+
116
+ const FATAL_CODES = new Set([
117
+ 'PROJECT_JSON_MISSING',
118
+ 'PROJECT_JSON_INVALID',
119
+ 'SCHEMA_UNKNOWN',
120
+ 'STAGE_UNKNOWN',
121
+ ]);
122
+
123
+ let cachedRemedies: Map<string, string> | null = null;
124
+
125
+ function loadCheckRemedies(): Map<string, string> {
126
+ if (cachedRemedies !== null) return cachedRemedies;
127
+ const map = new Map<string, string>();
128
+ try {
129
+ const text = readFileSync(join(import.meta.dir, '../docs/CHECKS.md'), 'utf8');
130
+ for (const line of text.split('\n')) {
131
+ const trimmed = line.trim();
132
+ if (!trimmed.startsWith('| `')) continue;
133
+ const cells = trimmed
134
+ .split('|')
135
+ .slice(1, -1)
136
+ .map(cell => cell.trim());
137
+ if (cells.length < 2) continue;
138
+ const codeMatch = /^`([A-Z0-9_]+)`$/.exec(cells[0]!);
139
+ if (!codeMatch) continue;
140
+ const code = codeMatch[1]!;
141
+ const remedy = cells[cells.length - 1]!;
142
+ if (!map.has(code) && remedy.length > 0) map.set(code, remedy);
143
+ }
144
+ } catch {
145
+ // Fallback to message remedy extraction if docs/CHECKS.md is unavailable.
146
+ }
147
+ cachedRemedies = map;
148
+ return map;
149
+ }
150
+
151
+ export function remedyForFinding(code: string, message: string): string {
152
+ const fromDoc = loadCheckRemedies().get(code);
153
+ if (fromDoc !== undefined) return fromDoc;
154
+ const semi = message.lastIndexOf('; ');
155
+ return semi >= 0 ? message.slice(semi + 2).trim() : message;
156
+ }
157
+
158
+ function parseArgs(args: string[]): { dir: string; json: boolean } {
159
+ const positional: string[] = [];
160
+ let json = false;
161
+ for (const arg of args) {
162
+ if (arg === '--json') json = true;
163
+ else if (arg.startsWith('-')) {
164
+ refuse('NEXT_USAGE', `next: unsupported ${arg}; usage: rig-create next [<project dir>] [--json]`, USAGE_EXIT);
165
+ } else {
166
+ positional.push(arg);
167
+ }
168
+ }
169
+ if (positional.length > 1) {
170
+ refuse('NEXT_USAGE', 'next: expected at most one project directory; usage: rig-create next [<project dir>] [--json]', USAGE_EXIT);
171
+ }
172
+ return { dir: resolve(positional[0] ?? process.cwd()), json };
173
+ }
174
+
175
+ function readOptionalJobId(dir: string): string | undefined {
176
+ try {
177
+ const file = join(dir, JOB_BINDING_FILE);
178
+ if (!existsSync(file)) return undefined;
179
+ const raw = JSON.parse(readFileSync(file, 'utf8')) as Partial<WriterBinding>;
180
+ return typeof raw?.jobId === 'string' && raw.jobId.length > 0 ? raw.jobId : undefined;
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ }
185
+
186
+ function isOpenInterviewStage(stage: string): boolean {
187
+ return stage === 'interview' || stage === 'rigging-interview' || stage === 'post-interview';
188
+ }
189
+
190
+ function isOpenInterviewTodo(finding: Finding, effectiveStage: string): boolean {
191
+ return isOpenInterviewStage(effectiveStage) && finding.code === 'INTERVIEW_INCOMPLETE';
192
+ }
193
+
194
+ function candidateAdvance(
195
+ dir: string,
196
+ project: JsonObject,
197
+ targetStage: string,
198
+ now: number,
199
+ partsCheck?: { report: string; root: string },
200
+ ): string | null {
201
+ const clone = structuredClone(project);
202
+ const history: Finding[] = [];
203
+ const reopens = collectReopens(clone, history);
204
+ const latest = furthestStageSignoff(
205
+ collectSignoffs(clone, history).filter(s => liveSignoff(reopens, s.stage, s.at)),
206
+ );
207
+ if (latest === undefined || !Number.isFinite(latest.at)) return null;
208
+ const preParts =
209
+ !hasStageOrder(clone) &&
210
+ stageIndex(String(clone.stage)) > stageIndex('parts-review') &&
211
+ collectSignoffs(clone, []).some(
212
+ s => s.stage === 'plan-approved' && s.checkpoint === null && s.at < Date.parse(PARTS_REVIEW_SINCE),
213
+ );
214
+ const steps = asArray(isObject(clone.plan) ? clone.plan.steps : undefined).filter(isObject);
215
+ const entry: JsonObject = {
216
+ stage: targetStage,
217
+ from_stage: clone.stage ?? null,
218
+ at: new Date(now).toISOString(),
219
+ post_not_passed: postNotPassed({ items: collectItems(clone, { collection: loadCollection(dir) }) }),
220
+ order: preParts ? 'pre-parts' : hasStageOrder(clone) ? 'split' : 'legacy',
221
+ signoffs_before: asArray(clone.signoffs).filter(isObject).map(signoffIdentity).filter(isSignoffIdentity),
222
+ signoff: signoffIdentity(latest.record),
223
+ steps_before: steps.map(s => ({
224
+ id: s.id ?? null,
225
+ phase: s.phase ?? null,
226
+ ...(s.status === undefined ? {} : { status: s.status }),
227
+ })),
228
+ };
229
+ const order = advanceOrder(entry.order);
230
+ const owner = moveOwner(order, entry.from_stage);
231
+ if (
232
+ history.length === 0 &&
233
+ owner !== undefined &&
234
+ order.findIndex(s => s.id === latest.stage) < order.findIndex(s => s.id === owner)
235
+ ) {
236
+ return null;
237
+ }
238
+ if (clone.advances !== undefined && !Array.isArray(clone.advances)) return null;
239
+ const checkPath = `advances[${asArray(clone.advances).length}].parts_check`;
240
+ if (isPartsCheckMove(entry)) {
241
+ if (!partsCheck) return null;
242
+ const read = partsCheckRecord(dir, partsCheck.report, partsCheck.root, checkPath);
243
+ if ('refusal' in read) return null;
244
+ entry.parts_check = read.record;
245
+ } else if (partsCheck) {
246
+ return null;
247
+ }
248
+ clone.advances = [...asArray(clone.advances), entry];
249
+ clone.stage = targetStage;
250
+ clone.stage_values = [...(hasStageOrder(clone) ? STAGE_VALUES : LEGACY_STAGE_VALUES)];
251
+ const allowed = new Map<string, number>();
252
+ if (['rigging-interview', 'post-interview'].includes(targetStage)) {
253
+ for (const item of collectItems(clone, { collection: loadCollection(dir) })) {
254
+ if (
255
+ item.state.state !== 'silent' ||
256
+ interviewStageOfGuide(item.id, item.kind === 'post', hasStageOrder(clone)) !== targetStage
257
+ ) {
258
+ continue;
259
+ }
260
+ const key = findingKey({ code: 'INTERVIEW_INCOMPLETE', path: item.path });
261
+ allowed.set(key, (allowed.get(key) ?? 0) + 1);
262
+ }
263
+ }
264
+ const checked = checkProject(dir, clone, { now, forwardWrite: true, readOnly: true });
265
+ if (newFindings(allowed, checked.refusals).length > 0) return null;
266
+ return partsCheck
267
+ ? `rig-create advance ${targetStage} --parts-check ${partsCheck.report} --parts ${partsCheck.root}`
268
+ : `rig-create advance ${targetStage}`;
269
+ }
270
+
271
+ function candidateStepStatus(
272
+ dir: string,
273
+ project: JsonObject,
274
+ stepId: string,
275
+ nextStatus: string,
276
+ now: number,
277
+ ): string | null {
278
+ const before = structuredClone(project);
279
+ const clone = structuredClone(project);
280
+ const matches = asArray(isObject(clone.plan) ? clone.plan.steps : undefined).filter(
281
+ (s): s is JsonObject => isObject(s) && s.id === stepId,
282
+ );
283
+ const step = matches[0];
284
+ if (matches.length !== 1 || step === undefined || (step.phase !== 'rig' && step.phase !== 'post')) return null;
285
+ const stage = stageOfStep(step.phase, String(step.id));
286
+ const resumes =
287
+ clone.stage !== PAUSED_STAGE && collectAmendments(clone, []).length > 0
288
+ ? checkProject(dir, structuredClone(clone), { now, readOnly: true }).effective_stage
289
+ : null;
290
+ const running =
291
+ resumes !== null && resumes !== clone.stage ? [String(clone.stage), resumes] : [String(clone.stage)];
292
+ const oldOrder =
293
+ !hasStageOrder(clone) &&
294
+ running.includes('rigging') &&
295
+ collectSignoffs(clone, []).some(s => s.stage === 'plan-approved' && s.at < Date.parse(PARTS_REVIEW_SINCE));
296
+ if (!running.includes(stage) && !(oldOrder && step.phase === 'rig')) return null;
297
+
298
+ const enabled = checkpointRequired(clone, step, now);
299
+ const entry: JsonObject = {
300
+ from_status: step.status ?? 'pending',
301
+ status: nextStatus,
302
+ by: 'agent',
303
+ at: new Date(now).toISOString(),
304
+ checkpoint_required: enabled,
305
+ signoff: null,
306
+ };
307
+ const from = STEP_STATUSES.indexOf(entry.from_status as (typeof STEP_STATUSES)[number]);
308
+ if (from < 0 || STEP_STATUSES.indexOf(nextStatus as (typeof STEP_STATUSES)[number]) !== from + 1) return null;
309
+ if (nextStatus === 'done' && enabled) {
310
+ const reopens = collectReopens(clone, []);
311
+ const signature = collectSignoffs(clone, [])
312
+ .filter(
313
+ s =>
314
+ s.checkpoint === step.id &&
315
+ (s.stage === stage || (oldOrder && s.stage === 'rigging')) &&
316
+ liveSignoff(reopens, s.stage, s.at) &&
317
+ s.at <= Date.parse(String(entry.at)),
318
+ )
319
+ .sort((a, b) => b.at - a.at || b.index - a.index)[0];
320
+ if (signature === undefined) return null;
321
+ entry.signoff = signoffIdentity(signature.record);
322
+ }
323
+ if (step.status_history !== undefined && !Array.isArray(step.status_history)) return null;
324
+ step.status_history = [...asArray(step.status_history), entry];
325
+ step.status = nextStatus;
326
+ clone.stage_values = [...(hasStageOrder(clone) ? STAGE_VALUES : LEGACY_STAGE_VALUES)];
327
+ const allowed = carriedRefusals(dir, before, { now, forwardWrite: false });
328
+ const checked = checkProject(dir, clone, { now, forwardWrite: false, readOnly: true });
329
+ if (newFindings(allowed, checked.refusals).length > 0) return null;
330
+ return `rig-create step-status ${stepId} ${nextStatus} --by agent`;
331
+ }
332
+
333
+ function nextStageInOrder(project: JsonObject, currentStage: string): string | null {
334
+ const order = hasStageOrder(project) ? STAGE_VALUES : LEGACY_STAGE_VALUES;
335
+ const idx = order.indexOf(currentStage);
336
+ if (idx < 0 || idx + 1 >= order.length) return null;
337
+ const candidate = order[idx + 1]!;
338
+ return candidate === PAUSED_STAGE ? null : candidate;
339
+ }
340
+
341
+ function resolveLaggingAdvance(
342
+ dir: string,
343
+ project: JsonObject,
344
+ stage: string,
345
+ now: number,
346
+ ): { targetStage: string; command: string } | null {
347
+ const nextStage = nextStageInOrder(project, stage);
348
+ if (nextStage === null) return null;
349
+ if (stage === 'parts' && nextStage === 'parts-review') {
350
+ const drafts = asArray(project.drafts).filter(isObject);
351
+ const latestDraft = drafts[drafts.length - 1];
352
+ const partsRoot =
353
+ isObject(latestDraft?.parts) && typeof latestDraft.parts.path === 'string'
354
+ ? latestDraft.parts.path
355
+ : undefined;
356
+ if (partsRoot !== undefined && existsSync(join(dir, 'evidence/parts-check.json'))) {
357
+ const cmd = candidateAdvance(dir, project, nextStage, now, {
358
+ report: 'evidence/parts-check.json',
359
+ root: partsRoot,
360
+ });
361
+ if (cmd !== null) return { targetStage: nextStage, command: cmd };
362
+ }
363
+ return null;
364
+ }
365
+ const cmd = candidateAdvance(dir, project, nextStage, now);
366
+ return cmd !== null ? { targetStage: nextStage, command: cmd } : null;
367
+ }
368
+
369
+ function hasApprovedPartsVerdictReceipt(dir: string, project: JsonObject, draftId: string | undefined): boolean {
370
+ if (draftId === undefined) return false;
371
+ try {
372
+ recordedPartsVerdict(dir, project, draftId);
373
+ return true;
374
+ } catch {
375
+ return false;
376
+ }
377
+ }
378
+
379
+ function resolveDraftStackPath(
380
+ dir: string,
381
+ partsRoot: string,
382
+ draftReport: JsonObject | null,
383
+ ): { stackPath: string; singleCandidate: boolean } {
384
+ const candidates = diffStackCandidates(resolve(dir, partsRoot, 'parts.json'));
385
+ const present = candidates.filter(file => existsSync(file));
386
+ if (present.length === 1) {
387
+ return {
388
+ stackPath: relative(dir, present[0]!).split(sep).join('/'),
389
+ singleCandidate: true,
390
+ };
391
+ }
392
+ if (isObject(draftReport?.stack) && typeof draftReport.stack.path === 'string') {
393
+ const recorded = draftReport.stack.path.trim();
394
+ if (recorded.length > 0 && existsSync(join(dir, recorded))) {
395
+ return { stackPath: recorded, singleCandidate: false };
396
+ }
397
+ }
398
+ if (basename(partsRoot) === 'rig' && existsSync(resolve(dir, partsRoot, '..', 'render'))) {
399
+ return {
400
+ stackPath: relative(dir, resolve(dir, partsRoot, '..', 'render', 'recomposite_rig.png')).split(sep).join('/'),
401
+ singleCandidate: false,
402
+ };
403
+ }
404
+ return { stackPath: `${partsRoot}/recomposite_rig.png`, singleCandidate: false };
405
+ }
406
+
407
+ function resolveDraftFigureMaskPath(dir: string, partsRoot: string, stackPath: string): string | undefined {
408
+ let maskName: string | undefined;
409
+ try {
410
+ const rawParts = JSON.parse(readFileSync(resolve(dir, partsRoot, 'parts.json'), 'utf8')) as Json;
411
+ if (isObject(rawParts) && isObject(rawParts.scene) && typeof rawParts.scene.figure_mask === 'string') {
412
+ const trimmed = rawParts.scene.figure_mask.trim();
413
+ if (trimmed.length > 0) maskName = trimmed;
414
+ }
415
+ } catch {
416
+ // Fallback to checking figure_rig.png presence if parts.json is unreadable.
417
+ }
418
+ const names = maskName ? [maskName] : ['figure_rig.png'];
419
+ const searchDirs = [dirname(stackPath), partsRoot];
420
+ for (const name of names) {
421
+ for (const base of searchDirs) {
422
+ const candidate = resolve(dir, base, name);
423
+ if (existsSync(candidate)) {
424
+ return relative(dir, candidate).split(sep).join('/');
425
+ }
426
+ }
427
+ }
428
+ if (maskName !== undefined) {
429
+ return relative(dir, resolve(dir, dirname(stackPath), maskName)).split(sep).join('/');
430
+ }
431
+ return undefined;
432
+ }
433
+
434
+ function resolveDiffComponentsMove(
435
+ dir: string,
436
+ draftId: string,
437
+ partsRoot: string,
438
+ paintingPath: string,
439
+ ): { command: string; act: string } {
440
+ const draftReportPath = `derived/drafts/${draftId}/diff-components.json`;
441
+ let draftReport: JsonObject | null = null;
442
+ if (existsSync(join(dir, draftReportPath))) {
443
+ try {
444
+ const bytes = diffReportBytes(dir, draftReportPath);
445
+ const parsed = JSON.parse(bytes.toString('utf8')) as Json;
446
+ if (isObject(parsed) && reportShape(parsed)) {
447
+ draftReport = parsed;
448
+ }
449
+ } catch {
450
+ draftReport = null;
451
+ }
452
+ }
453
+
454
+ const { stackPath, singleCandidate } = resolveDraftStackPath(dir, partsRoot, draftReport);
455
+ const figureMaskPath = resolveDraftFigureMaskPath(dir, partsRoot, stackPath);
456
+
457
+ if (draftReport !== null && singleCandidate) {
458
+ try {
459
+ const components = draftReport.components as { id: string }[];
460
+ const uniqueIds = new Set(components.map(c => c.id)).size === components.length;
461
+ const definition = draftReport.definition as {
462
+ floor_px: number;
463
+ threshold: number;
464
+ inside_sha256: string | null;
465
+ };
466
+ const withinPolicy =
467
+ definition.floor_px <= DIFF_MAP_MAX_FLOOR_PX && definition.threshold <= DIFF_MAP_MAX_THRESHOLD;
468
+ const maskSatisfied = figureMaskPath === undefined || definition.inside_sha256 !== null;
469
+ if (uniqueIds && withinPolicy && maskSatisfied) {
470
+ const stackSha = createHash('sha256').update(diffReportBytes(dir, stackPath)).digest('hex');
471
+ const paintingSha = createHash('sha256').update(diffReportBytes(dir, paintingPath)).digest('hex');
472
+ const recordedStackSha = (draftReport.stack as { sha256: string }).sha256;
473
+ const recordedPaintingSha = (draftReport.painting as { sha256: string }).sha256;
474
+ if (stackSha === recordedStackSha && paintingSha === recordedPaintingSha) {
475
+ return {
476
+ command: `cp ${draftReportPath} evidence/diff-components.json`,
477
+ act: `copy ${draftReportPath} for drafts[${draftId}] to evidence/diff-components.json`,
478
+ };
479
+ }
480
+ }
481
+ } catch {
482
+ // Fall through to rig-parts diff-components command if verification fails.
483
+ }
484
+ }
485
+
486
+ const figureFlag = figureMaskPath !== undefined ? ` --figure ${figureMaskPath}` : '';
487
+ return {
488
+ command: `rig-parts diff-components --stack ${stackPath} --painting ${paintingPath} --parts ${partsRoot}/parts.json${figureFlag} --floor 2 --threshold 40 --out evidence/diff-components.json`,
489
+ act: `run rig-parts diff-components for drafts[${draftId}]`,
490
+ };
491
+ }
492
+
493
+ function hasValidDiffMap(dir: string, project: JsonObject): boolean {
494
+ const partsFile = resolveDiffMapPartsFile(dir, project);
495
+ if (isObject(project.diff_map)) {
496
+ const errors: Finding[] = [];
497
+ validateDiffMapRecord(dir, project.diff_map, 'diff_map', partsFile, errors, {
498
+ requireAgent: true,
499
+ });
500
+ if (errors.length === 0) return true;
501
+ }
502
+ const reopens = collectReopens(project, []);
503
+ const livePartsReview = collectSignoffs(project, []).filter(
504
+ s => s.stage === 'parts-review' && s.checkpoint === null && liveSignoff(reopens, s.stage, s.at),
505
+ );
506
+ const latest = livePartsReview[livePartsReview.length - 1];
507
+ if (
508
+ latest !== undefined &&
509
+ latest.at < Date.parse(DIFF_MAP_AGENT_SINCE) &&
510
+ isObject(latest.record.diff_map)
511
+ ) {
512
+ const errors: Finding[] = [];
513
+ validateDiffMapRecord(dir, latest.record.diff_map, `signoffs[${latest.index}].diff_map`, partsFile, errors, {
514
+ requireAgent: false,
515
+ });
516
+ if (errors.length === 0) return true;
517
+ }
518
+ return false;
519
+ }
520
+
521
+ function resolveDiffClassesCommand(dir: string): string {
522
+ if (!existsSync(join(dir, 'evidence/diff-triage.json'))) {
523
+ try {
524
+ const bytes = diffReportBytes(dir, 'evidence/diff-components.json');
525
+ const parsed = JSON.parse(bytes.toString('utf8')) as Json;
526
+ if (isObject(parsed) && reportShape(parsed) && (parsed.components as unknown[]).length === 0) {
527
+ return 'rig-create diff-classes --diff-report evidence/diff-components.json --by agent';
528
+ }
529
+ } catch {
530
+ // Fall through to default command with --triage-file.
531
+ }
532
+ }
533
+ return 'rig-create diff-classes --diff-report evidence/diff-components.json --triage-file evidence/diff-triage.json --by agent';
534
+ }
535
+
536
+ export function nextProject(projectDirArg: string, now: number = Date.now()): NextReport {
537
+ const dir = resolve(projectDirArg);
538
+ const report: CheckReport = checkProject(dir, undefined, { now, readOnly: true });
539
+ const fatal = report.refusals.find(r => FATAL_CODES.has(r.code));
540
+ if (fatal !== undefined) refuse(fatal.code, fatal.message);
541
+
542
+ const project = JSON.parse(readFileSync(join(dir, 'project.json'), 'utf8')) as JsonObject;
543
+ const stage = String(project.stage ?? '');
544
+ const effectiveStage = String(report.effective_stage ?? stage);
545
+ const structureWarnings = report.warnings.filter(w => w.code === 'STRUCTURE_DELTA_REVIEW');
546
+ const warningFields = structureWarnings.length ? { warnings: structureWarnings } : {};
547
+ const jobId = readOptionalJobId(dir);
548
+ const holes = report.holes ?? [];
549
+
550
+ const allRefusalFindings: Finding[] = [...report.refusals, ...holes];
551
+ const refusals: NextRefusal[] = allRefusalFindings.map(r => ({
552
+ code: r.code,
553
+ field: r.path,
554
+ message: r.message,
555
+ fix: remedyForFinding(r.code, r.message),
556
+ }));
557
+
558
+ const laggingAdvance = report.refusals.some(r => r.code === 'STAGE_LAGGING')
559
+ ? resolveLaggingAdvance(dir, project, stage, now)
560
+ : null;
561
+ const isResolvableLagging = (finding: Finding): boolean =>
562
+ finding.code === 'STAGE_LAGGING' && laggingAdvance !== null;
563
+
564
+ const hasBlockingRefusal =
565
+ holes.length > 0 ||
566
+ report.refusals.some(r => !isOpenInterviewTodo(r, effectiveStage) && !isResolvableLagging(r));
567
+ if (hasBlockingRefusal) {
568
+ return {
569
+ schema: NEXT_SCHEMA,
570
+ project_id: String(project.name ?? ''),
571
+ stage,
572
+ effective_stage: effectiveStage,
573
+ accepted: false,
574
+ refusals,
575
+ ...warningFields,
576
+ awaits: [],
577
+ moves: [],
578
+ };
579
+ }
580
+
581
+ const awaitsList: string[] = [];
582
+ const addAwait = (target: string): void => {
583
+ if (!awaitsList.includes(target)) awaitsList.push(target);
584
+ };
585
+ const moves: NextMove[] = [];
586
+ // The check owns bounce/withdrawal/answer logic; consume its open findings.
587
+ const openFeedback = report.warnings.filter(w => w.code === 'FEEDBACK_OPEN');
588
+ if (openFeedback.length > 0) {
589
+ const versions = asArray(project.versions).filter(isObject);
590
+ const n = Math.max(0, ...versions.map(v => Number(v.n) || 0)) + 1;
591
+ const groups = new Map<string, { version: number; verdict: number }[]>();
592
+ for (const finding of openFeedback) {
593
+ const summary = report.feedback.find(f => f.verdict === finding.path);
594
+ if (!summary) continue;
595
+ const kind = versions.find(v => v.n === summary.version)?.kind;
596
+ if (kind !== 'rig' && kind !== 'post') continue;
597
+ groups.set(kind, [...(groups.get(kind) ?? []), { version: summary.version, verdict: summary.index }]);
598
+ }
599
+ for (const [kind, refs] of groups) {
600
+ const field = `versions[n=${n}]`;
601
+ addAwait(field);
602
+ const writer = kind === 'rig' ? `rig-version ${n}` : `direction write ${n}`;
603
+ const inputs = kind === 'rig'
604
+ ? '--parts <dir> --rig <dir> --build <dir> --idle <dir> --config <file> --path-adapter <file> --compile-report <file> --toolchain-lock <file> --at-source <file> --at-pointer <pointer> --by <agent>'
605
+ : '--file <direction.json> --rig <rig-version>';
606
+ moves.push({ role: 'agent', field: 'versions', awaits: field,
607
+ command: `rig-create ${writer} ${inputs} ${refs.map(r => `--answers ${r.version}:${r.verdict}`).join(' ')}`,
608
+ act: `build the next ${kind} version with ${writer}, recording answers_feedback ${JSON.stringify(refs)}` });
609
+ }
610
+ if (moves.length > 0) return { schema: NEXT_SCHEMA, project_id: String(project.name ?? ''), stage, effective_stage: effectiveStage, accepted: false, refusals, ...warningFields, awaits: awaitsList, moves };
611
+ }
612
+ const recordedLapseTargets = new Set<string>();
613
+ const steps = asArray(isObject(project.plan) ? project.plan.steps : undefined).filter(isObject);
614
+ const withdrawnCheckpoint = (id: string): boolean => steps.some(step => step.id === id
615
+ && checkpointEnabled(step) && !checkpointRequired(project, step, now));
616
+
617
+ // 1. Derive lapsed approvals from reopens[] and amendments[].
618
+ const rawSignoffs = asArray(project.signoffs).filter(isObject);
619
+ const rawReopens = asArray(project.reopens).filter(isObject);
620
+ for (const warning of reopenLapseWarnings(project)) {
621
+ const reopenMatch = /\(reopens\[(\d+)\]\)/.exec(warning.message);
622
+ const reopenIdx = reopenMatch ? Number(reopenMatch[1]) : Math.max(0, rawReopens.length - 1);
623
+ const reopenEntry = rawReopens[reopenIdx];
624
+ const reopenId = typeof reopenEntry?.id === 'string' ? reopenEntry.id : undefined;
625
+ const reopenField = `reopens[${reopenIdx}]`;
626
+
627
+ if (warning.code === 'REOPEN_SIGNOFF_LAPSED') {
628
+ const signoffMatch = /^signoffs\[(\d+)\]$/.exec(warning.path);
629
+ const signoffEntry = signoffMatch ? rawSignoffs[Number(signoffMatch[1])] : undefined;
630
+ const lapsedStage = typeof signoffEntry?.stage === 'string' ? signoffEntry.stage : effectiveStage;
631
+ const checkpoint = typeof signoffEntry?.checkpoint === 'string' ? signoffEntry.checkpoint : null;
632
+ if (checkpoint !== null && withdrawnCheckpoint(checkpoint)) continue;
633
+ const awaitTarget = checkpoint
634
+ ? `signoffs[checkpoint=${checkpoint}]`
635
+ : `signoffs[stage=${lapsedStage}]`;
636
+ addAwait(awaitTarget);
637
+ recordedLapseTargets.add(awaitTarget);
638
+ moves.push({
639
+ role: 'human',
640
+ field: reopenField,
641
+ awaits: awaitTarget,
642
+ command: null,
643
+ act: checkpoint
644
+ ? `re-approve lapsed checkpoint ${checkpoint} (${warning.path}, lapsed by ${reopenField}${reopenId ? ` ${reopenId}` : ''}: ${warning.code}) via review-tool act signoff-checkpoint`
645
+ : `re-approve lapsed stage ${lapsedStage} (${warning.path}, lapsed by ${reopenField}${reopenId ? ` ${reopenId}` : ''}: ${warning.code}) via review-tool act signoff`,
646
+ review_act: {
647
+ kind: checkpoint ? 'signoff-checkpoint' : 'signoff',
648
+ stage: lapsedStage,
649
+ ...(checkpoint ? { step_id: checkpoint } : {}),
650
+ ...(jobId ? { job_id: jobId } : {}),
651
+ },
652
+ lapse: {
653
+ code: warning.code,
654
+ source_field: reopenField,
655
+ target_field: warning.path,
656
+ ...(reopenId ? { reopen_id: reopenId } : {}),
657
+ message: warning.message,
658
+ },
659
+ });
660
+ } else if (warning.code === 'REOPEN_VERDICT_LAPSED') {
661
+ const vMatch = /^versions\[v(\d+)\]\.verdicts\[(\d+)\]$/.exec(warning.path);
662
+ const n = vMatch ? Number(vMatch[1]) : 1;
663
+ const versionEntry = asArray(project.versions)
664
+ .filter(isObject)
665
+ .find(v => v.n === n);
666
+ const verdictIdx = vMatch ? Number(vMatch[2]) : 0;
667
+ const verdictEntry = isObject(versionEntry)
668
+ ? asArray(versionEntry.verdicts).filter(isObject)[verdictIdx]
669
+ : undefined;
670
+ const reviewStage =
671
+ typeof verdictEntry?.review === 'string'
672
+ ? verdictEntry.review
673
+ : versionEntry?.kind === 'post'
674
+ ? 'direction-review'
675
+ : 'rig-review';
676
+ const awaitTarget = `versions[n=${n}].verdicts[review=${reviewStage}]`;
677
+ addAwait(awaitTarget);
678
+ recordedLapseTargets.add(awaitTarget);
679
+ moves.push({
680
+ role: 'human',
681
+ field: reopenField,
682
+ awaits: awaitTarget,
683
+ command: null,
684
+ act: `re-approve lapsed ${reviewStage} verdict on versions[v${n}] (${warning.path}, lapsed by ${reopenField}${reopenId ? ` ${reopenId}` : ''}: ${warning.code}) via review-tool act review-verdict`,
685
+ review_act: {
686
+ kind: 'review-verdict',
687
+ stage: reviewStage,
688
+ version_id: `v${n}`,
689
+ review: reviewStage,
690
+ ...(jobId ? { job_id: jobId } : {}),
691
+ },
692
+ lapse: {
693
+ code: warning.code,
694
+ source_field: reopenField,
695
+ target_field: warning.path,
696
+ ...(reopenId ? { reopen_id: reopenId } : {}),
697
+ message: warning.message,
698
+ },
699
+ });
700
+ }
701
+ }
702
+
703
+ for (const amendment of report.amendments ?? []) {
704
+ const amendField = amendment.path;
705
+ const targetId = typeof amendment.target.id === 'string' ? amendment.target.id : undefined;
706
+ for (const lapsedApproval of amendment.lapsed) {
707
+ const targetStage = lapsedApproval.stage;
708
+ const checkpoint = lapsedApproval.checkpoint ?? null;
709
+ if (checkpoint !== null && withdrawnCheckpoint(checkpoint)) continue;
710
+ const awaitTarget = checkpoint
711
+ ? `signoffs[checkpoint=${checkpoint}]`
712
+ : `signoffs[stage=${targetStage}]`;
713
+ if (recordedLapseTargets.has(awaitTarget)) continue;
714
+ addAwait(awaitTarget);
715
+ recordedLapseTargets.add(awaitTarget);
716
+ moves.push({
717
+ role: 'human',
718
+ field: amendField,
719
+ awaits: awaitTarget,
720
+ command: null,
721
+ act: `re-approve ${lapsedApproval.path} (${targetStage}) lapsed by ${amendField}${targetId ? ` (${targetId})` : ''} via review-tool act ${checkpoint ? 'signoff-checkpoint' : 'signoff'}`,
722
+ review_act: {
723
+ kind: checkpoint ? 'signoff-checkpoint' : 'signoff',
724
+ stage: targetStage,
725
+ ...(checkpoint ? { step_id: checkpoint } : {}),
726
+ ...(jobId ? { job_id: jobId } : {}),
727
+ },
728
+ lapse: {
729
+ code: 'AMENDED_STEP',
730
+ source_field: amendField,
731
+ target_field: lapsedApproval.path,
732
+ ...(targetId ? { amendment_id: targetId } : {}),
733
+ message: lapsedApproval.reason,
734
+ },
735
+ });
736
+ }
737
+ }
738
+
739
+ // 2. If accepted and no lapses remain, there are no remaining moves.
740
+ if (effectiveStage === 'accepted' && moves.length === 0) {
741
+ return {
742
+ schema: NEXT_SCHEMA,
743
+ project_id: String(project.name ?? ''),
744
+ stage,
745
+ effective_stage: effectiveStage,
746
+ accepted: true,
747
+ refusals,
748
+ ...warningFields,
749
+ awaits: [],
750
+ moves: [],
751
+ };
752
+ }
753
+
754
+ // 3. Stage-specific derivation from project.json.
755
+ const reopens = collectReopens(project, []);
756
+ const signoffs = collectSignoffs(project, []);
757
+ const hasLiveStageSignoff = (targetStage: string): boolean =>
758
+ signoffs.some(s => s.stage === targetStage && s.checkpoint === null && liveSignoff(reopens, s.stage, s.at));
759
+ const hasLiveCheckpointSignoff = (targetStage: string, stepId: string): boolean =>
760
+ signoffs.some(s => s.stage === targetStage && s.checkpoint === stepId && liveSignoff(reopens, s.stage, s.at));
761
+
762
+ const collection = loadCollection(dir);
763
+ const items = collectItems(project, { collection });
764
+ const splitOrder = hasStageOrder(project);
765
+ const drafts = asArray(project.drafts).filter(isObject);
766
+ const versions = asArray(project.versions).filter(isObject);
767
+
768
+ if (stage === PAUSED_STAGE) {
769
+ const resumeTarget = pausedFrom(project);
770
+ addAwait('project.stage');
771
+ moves.push({
772
+ role: 'human',
773
+ field: 'pauses',
774
+ awaits: 'project.stage',
775
+ command: null,
776
+ act: `resume paused project to ${resumeTarget} via human pause/resume act`,
777
+ review_act: {
778
+ kind: 'signoff',
779
+ stage: resumeTarget,
780
+ ...(jobId ? { job_id: jobId } : {}),
781
+ },
782
+ });
783
+ } else if (isOpenInterviewStage(effectiveStage)) {
784
+ const stageItems = items.filter(
785
+ item => interviewStageOfGuide(item.id, item.kind === 'post', splitOrder) === effectiveStage,
786
+ );
787
+ for (const item of stageItems) {
788
+ const answerField = item.path;
789
+ const questionPath = item.path.replace(/\.(answer|guide)$/, '');
790
+ if (item.state.state === 'silent') {
791
+ addAwait(answerField);
792
+ const pendingOpts = isObject(item.record.pending_options) ? item.record.pending_options : null;
793
+ if (pendingOpts !== null) {
794
+ let optSetId = typeof pendingOpts.id === 'string' ? pendingOpts.id : '';
795
+ if (!optSetId && typeof pendingOpts.path === 'string') {
796
+ try {
797
+ const rawSet = JSON.parse(readFileSync(join(dir, pendingOpts.path), 'utf8')) as JsonObject;
798
+ if (typeof rawSet.id === 'string') optSetId = rawSet.id;
799
+ } catch {
800
+ optSetId = pendingOpts.path;
801
+ }
802
+ }
803
+ moves.push({
804
+ role: 'human',
805
+ field: `${questionPath}.pending_options`,
806
+ awaits: answerField,
807
+ command: null,
808
+ act: `review-tool act interview-picks for ${questionPath} (option set ${optSetId})`,
809
+ review_act: {
810
+ kind: 'interview-picks',
811
+ stage: effectiveStage,
812
+ question_id: item.id,
813
+ option_set_id: optSetId,
814
+ ...(jobId ? { job_id: jobId } : {}),
815
+ },
816
+ });
817
+ } else {
818
+ moves.push({
819
+ role: 'human',
820
+ field: answerField,
821
+ awaits: answerField,
822
+ command: null,
823
+ act: `review-tool act interview-picks for ${questionPath} (unanswered by record: ${questionPath}.pending_options is not recorded in project.json)`,
824
+ review_act: {
825
+ kind: 'interview-picks',
826
+ stage: effectiveStage,
827
+ question_id: item.id,
828
+ ...(jobId ? { job_id: jobId } : {}),
829
+ },
830
+ unanswered_by_record: `${questionPath}.pending_options`,
831
+ });
832
+ }
833
+ } else if (item.state.state === 'delegated' && !item.state.decided) {
834
+ const decisionField = `${answerField}.agent_decision`;
835
+ addAwait(decisionField);
836
+ moves.push({
837
+ role: 'human',
838
+ field: answerField,
839
+ awaits: decisionField,
840
+ command: null,
841
+ act: `unanswered by record: ${decisionField} requires authored decision and rationale`,
842
+ unanswered_by_record: decisionField,
843
+ });
844
+ }
845
+ }
846
+
847
+ const needsPlanSteps =
848
+ (effectiveStage === 'interview' && steps.length === 0) ||
849
+ (effectiveStage === 'post-interview' && !steps.some(s => s.phase === 'post')) ||
850
+ (effectiveStage === 'rigging-interview' && !steps.some(step => stepCoveredByApproval(project, step, effectiveStage)));
851
+ if (needsPlanSteps) {
852
+ addAwait('plan.steps');
853
+ if (effectiveStage === 'rigging-interview') {
854
+ for (const id of ['bones', 'meshes', 'constraints', 'motion']) {
855
+ const item = stageItems.find(item => item.id === id);
856
+ if (!item || item.state.state === 'silent' || (item.state.state === 'delegated' && !item.state.decided) || steps.some(step => step.id === id)) continue;
857
+ moves.push({
858
+ role: 'agent',
859
+ field: 'plan.steps',
860
+ awaits: 'plan.steps',
861
+ command: `rig-create plan add ${id} '${dir.replaceAll("'", "'\\''")}' --by agent --phase rig …`,
862
+ act: `author the ${id} plan step for approval at rigging-interview; supply order, does, produces, rests-on, bar and checkpoint-proposed`,
863
+ unanswered_by_record: 'plan.steps',
864
+ });
865
+ }
866
+ }
867
+ }
868
+
869
+ const signoffAwait = `signoffs[stage=${effectiveStage}]`;
870
+ if (!hasLiveStageSignoff(effectiveStage) && !recordedLapseTargets.has(signoffAwait)) {
871
+ addAwait(signoffAwait);
872
+ moves.push({
873
+ role: 'human',
874
+ field: 'signoffs',
875
+ awaits: signoffAwait,
876
+ command: null,
877
+ act: needsPlanSteps
878
+ ? effectiveStage === 'rigging-interview'
879
+ ? 'review-tool act signoff for stage rigging-interview (unanswered by record: plan.steps has no rig steps approved at rigging-interview)'
880
+ : `review-tool act signoff for stage ${effectiveStage} (unanswered by record: plan.steps is empty in project.json)`
881
+ : effectiveStage === 'interview'
882
+ ? 'review-tool act signoff for stage interview on the record page with job open --plan (source-linked Review); ingest signoff interview --from-review <actId>, then advance plan-approved'
883
+ : `review-tool act signoff for stage ${effectiveStage}`,
884
+ review_act: {
885
+ kind: 'signoff',
886
+ stage: effectiveStage,
887
+ ...(jobId ? { job_id: jobId } : {}),
888
+ },
889
+ ...(needsPlanSteps ? { unanswered_by_record: 'plan.steps' } : {}),
890
+ });
891
+ } else if (hasLiveStageSignoff(effectiveStage)) {
892
+ const nextStage = nextStageInOrder(project, stage);
893
+ if (nextStage !== null) {
894
+ const cmd = candidateAdvance(dir, project, nextStage, now);
895
+ if (cmd !== null) {
896
+ addAwait('project.stage');
897
+ moves.push({
898
+ role: 'agent',
899
+ field: 'stage',
900
+ awaits: 'project.stage',
901
+ command: cmd,
902
+ act: `advance stage from ${stage} to ${nextStage}`,
903
+ });
904
+ }
905
+ }
906
+ }
907
+ } else if (effectiveStage === 'plan-approved') {
908
+ const signoffAwait = 'signoffs[stage=plan-approved]';
909
+ if (!hasLiveStageSignoff('plan-approved') && !recordedLapseTargets.has(signoffAwait)) {
910
+ addAwait(signoffAwait);
911
+ moves.push({
912
+ role: 'human',
913
+ field: 'signoffs',
914
+ awaits: signoffAwait,
915
+ command: null,
916
+ act: 'review-tool act signoff for stage plan-approved',
917
+ review_act: {
918
+ kind: 'signoff',
919
+ stage: 'plan-approved',
920
+ ...(jobId ? { job_id: jobId } : {}),
921
+ },
922
+ });
923
+ } else if (hasLiveStageSignoff('plan-approved')) {
924
+ const nextStage = nextStageInOrder(project, stage);
925
+ if (nextStage !== null) {
926
+ const cmd = candidateAdvance(dir, project, nextStage, now);
927
+ if (cmd !== null) {
928
+ addAwait('project.stage');
929
+ moves.push({
930
+ role: 'agent',
931
+ field: 'stage',
932
+ awaits: 'project.stage',
933
+ command: cmd,
934
+ act: `advance stage from ${stage} to ${nextStage}`,
935
+ });
936
+ }
937
+ }
938
+ }
939
+ } else if (effectiveStage === 'parts' || effectiveStage === 'parts-review') {
940
+ const partsSteps = steps.filter(
941
+ s => (s.phase === 'rig' || s.phase === 'post') && stageOfStep(s.phase, String(s.id)) === 'parts',
942
+ );
943
+ const openPartsSteps = partsSteps.filter(s => s.status !== 'done');
944
+
945
+ if (effectiveStage === 'parts' && openPartsSteps.length > 0) {
946
+ for (const step of openPartsSteps) {
947
+ const stepId = String(step.id);
948
+ const currentStatus = String(step.status ?? 'pending');
949
+ const fromIdx = STEP_STATUSES.indexOf(currentStatus as (typeof STEP_STATUSES)[number]);
950
+ const nextStatus = fromIdx >= 0 ? STEP_STATUSES[fromIdx + 1] : undefined;
951
+ if (nextStatus === undefined) continue;
952
+ const checkpointEnabled = checkpointRequired(project, step, now);
953
+ if (nextStatus === 'done' && checkpointEnabled && !hasLiveCheckpointSignoff('parts', stepId)) {
954
+ const cpAwait = `signoffs[checkpoint=${stepId}]`;
955
+ addAwait(cpAwait);
956
+ if (!recordedLapseTargets.has(cpAwait)) {
957
+ moves.push({
958
+ role: 'human',
959
+ field: `plan.steps[${stepId}].checkpoint`,
960
+ awaits: cpAwait,
961
+ command: null,
962
+ act: `review-tool act signoff-checkpoint for plan.steps[${stepId}]`,
963
+ review_act: {
964
+ kind: 'signoff-checkpoint',
965
+ stage: 'parts',
966
+ step_id: stepId,
967
+ ...(jobId ? { job_id: jobId } : {}),
968
+ },
969
+ });
970
+ }
971
+ continue;
972
+ }
973
+ const cmd = candidateStepStatus(dir, project, stepId, nextStatus, now);
974
+ if (cmd !== null) {
975
+ const statusField = `plan.steps[${stepId}].status`;
976
+ addAwait(statusField);
977
+ moves.push({
978
+ role: 'agent',
979
+ field: statusField,
980
+ awaits: statusField,
981
+ command: cmd,
982
+ act: `advance plan.steps[${stepId}].status from ${currentStatus} to ${nextStatus}`,
983
+ });
984
+ }
985
+ }
986
+ } else {
987
+ const latestDraft = drafts[drafts.length - 1];
988
+ const draftId = typeof latestDraft?.id === 'string' ? latestDraft.id : undefined;
989
+ const partsRoot =
990
+ isObject(latestDraft?.parts) && typeof latestDraft.parts.path === 'string'
991
+ ? latestDraft.parts.path
992
+ : undefined;
993
+ const hasApprovedPartsVerdict = hasApprovedPartsVerdictReceipt(dir, project, draftId);
994
+
995
+ if (effectiveStage === 'parts' && partsRoot !== undefined && existsSync(join(dir, 'evidence/parts-check.json'))) {
996
+ const advCmd = candidateAdvance(dir, project, 'parts-review', now, {
997
+ report: 'evidence/parts-check.json',
998
+ root: partsRoot,
999
+ });
1000
+ if (advCmd !== null) {
1001
+ moves.push({
1002
+ role: 'agent',
1003
+ field: 'stage',
1004
+ awaits: 'project.stage',
1005
+ command: advCmd,
1006
+ act: 'advance stage from parts to parts-review',
1007
+ });
1008
+ }
1009
+ }
1010
+
1011
+ if (draftId !== undefined && !hasApprovedPartsVerdict) {
1012
+ const verdictAwait = `review_acts[kind=parts-verdict,draft_id=${draftId}]`;
1013
+ addAwait(verdictAwait);
1014
+ moves.push({
1015
+ role: 'human',
1016
+ field: 'review_acts',
1017
+ awaits: verdictAwait,
1018
+ command: null,
1019
+ act: `review-tool act parts-verdict for drafts[${draftId}]`,
1020
+ review_act: {
1021
+ kind: 'parts-verdict',
1022
+ stage: 'parts-review',
1023
+ draft_id: draftId,
1024
+ ...(jobId ? { job_id: jobId } : {}),
1025
+ },
1026
+ });
1027
+ }
1028
+
1029
+ const hasStructureFile =
1030
+ existsSync(join(dir, 'evidence/structure.json')) ||
1031
+ existsSync(join(dir, 'evidence/structure-decisions.json'));
1032
+ if (draftId !== undefined && hasApprovedPartsVerdict && !hasStructureFile) {
1033
+ addAwait('evidence/structure.json');
1034
+ moves.push({
1035
+ role: 'agent',
1036
+ field: `drafts[${draftId}]`,
1037
+ awaits: 'evidence/structure.json',
1038
+ command: `rig-create structure-snapshot --draft ${draftId} --out evidence/structure.json`,
1039
+ act: `write structure-snapshot for drafts[${draftId}] to evidence/structure.json`,
1040
+ });
1041
+ }
1042
+
1043
+ const inputsList = asArray(project.inputs).filter(isObject);
1044
+ const paintingPath =
1045
+ typeof inputsList[0]?.path === 'string'
1046
+ ? inputsList[0].path
1047
+ : existsSync(join(dir, 'inputs/painting.png'))
1048
+ ? 'inputs/painting.png'
1049
+ : undefined;
1050
+ if (
1051
+ draftId !== undefined &&
1052
+ partsRoot !== undefined &&
1053
+ paintingPath !== undefined &&
1054
+ !existsSync(join(dir, 'evidence/diff-components.json'))
1055
+ ) {
1056
+ const diffMove = resolveDiffComponentsMove(dir, draftId, partsRoot, paintingPath);
1057
+ addAwait('evidence/diff-components.json');
1058
+ moves.push({
1059
+ role: 'producer',
1060
+ field: `drafts[${draftId}].parts`,
1061
+ awaits: 'evidence/diff-components.json',
1062
+ command: diffMove.command,
1063
+ act: diffMove.act,
1064
+ });
1065
+ } else if (
1066
+ existsSync(join(dir, 'evidence/diff-components.json')) &&
1067
+ !hasValidDiffMap(dir, project)
1068
+ ) {
1069
+ addAwait('diff_map');
1070
+ moves.push({
1071
+ role: 'agent',
1072
+ field: 'diff_map',
1073
+ awaits: 'diff_map',
1074
+ command: resolveDiffClassesCommand(dir),
1075
+ act: 'record agent component classification for evidence/diff-components.json in diff_map',
1076
+ });
1077
+ }
1078
+
1079
+ const signoffAwait = 'signoffs[stage=parts-review]';
1080
+ if (!hasLiveStageSignoff('parts-review') && !recordedLapseTargets.has(signoffAwait)) {
1081
+ addAwait(signoffAwait);
1082
+ moves.push({
1083
+ role: 'human',
1084
+ field: 'signoffs',
1085
+ awaits: signoffAwait,
1086
+ command: null,
1087
+ act: hasApprovedPartsVerdict
1088
+ ? 'review-tool act signoff for stage parts-review'
1089
+ : `review-tool act signoff for stage parts-review (unanswered by record: review_acts[kind=parts-verdict,draft_id=${draftId ?? 'unknown'}] is absent)`,
1090
+ review_act: {
1091
+ kind: 'signoff',
1092
+ stage: 'parts-review',
1093
+ ...(draftId ? { draft_id: draftId } : {}),
1094
+ ...(jobId ? { job_id: jobId } : {}),
1095
+ },
1096
+ ...(!hasApprovedPartsVerdict
1097
+ ? {
1098
+ unanswered_by_record: `review_acts[kind=parts-verdict,draft_id=${draftId ?? 'unknown'}], evidence/structure.json`,
1099
+ }
1100
+ : {}),
1101
+ });
1102
+ }
1103
+
1104
+ if (effectiveStage === 'parts') {
1105
+ addAwait('project.stage');
1106
+ } else if (hasLiveStageSignoff('parts-review')) {
1107
+ const nextStage = nextStageInOrder(project, stage);
1108
+ if (nextStage !== null) {
1109
+ const cmd = candidateAdvance(dir, project, nextStage, now);
1110
+ if (cmd !== null) {
1111
+ addAwait('project.stage');
1112
+ moves.push({
1113
+ role: 'agent',
1114
+ field: 'stage',
1115
+ awaits: 'project.stage',
1116
+ command: cmd,
1117
+ act: `advance stage from ${stage} to ${nextStage}`,
1118
+ });
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ } else if (effectiveStage === 'rigging' || effectiveStage === 'rig-review') {
1124
+ const rigSteps = steps.filter(
1125
+ s => (s.phase === 'rig' || s.phase === 'post') && stageOfStep(s.phase, String(s.id)) === 'rigging',
1126
+ );
1127
+ const openRigSteps = rigSteps.filter(s => s.status !== 'done');
1128
+
1129
+ if (effectiveStage === 'rigging' && openRigSteps.length > 0) {
1130
+ for (const step of openRigSteps) {
1131
+ const stepId = String(step.id);
1132
+ const currentStatus = String(step.status ?? 'pending');
1133
+ const fromIdx = STEP_STATUSES.indexOf(currentStatus as (typeof STEP_STATUSES)[number]);
1134
+ const nextStatus = fromIdx >= 0 ? STEP_STATUSES[fromIdx + 1] : undefined;
1135
+ if (nextStatus === undefined) continue;
1136
+ const checkpointEnabled = checkpointRequired(project, step, now);
1137
+ if (nextStatus === 'done' && checkpointEnabled && !hasLiveCheckpointSignoff('rigging', stepId)) {
1138
+ const cpAwait = `signoffs[checkpoint=${stepId}]`;
1139
+ addAwait(cpAwait);
1140
+ if (!recordedLapseTargets.has(cpAwait)) {
1141
+ moves.push({
1142
+ role: 'human',
1143
+ field: `plan.steps[${stepId}].checkpoint`,
1144
+ awaits: cpAwait,
1145
+ command: null,
1146
+ act: `review-tool act signoff-checkpoint for plan.steps[${stepId}]`,
1147
+ review_act: {
1148
+ kind: 'signoff-checkpoint',
1149
+ stage: 'rigging',
1150
+ step_id: stepId,
1151
+ ...(jobId ? { job_id: jobId } : {}),
1152
+ },
1153
+ });
1154
+ }
1155
+ continue;
1156
+ }
1157
+ const cmd = candidateStepStatus(dir, project, stepId, nextStatus, now);
1158
+ if (cmd !== null) {
1159
+ const statusField = `plan.steps[${stepId}].status`;
1160
+ addAwait(statusField);
1161
+ moves.push({
1162
+ role: 'agent',
1163
+ field: statusField,
1164
+ awaits: statusField,
1165
+ command: cmd,
1166
+ act: `advance plan.steps[${stepId}].status from ${currentStatus} to ${nextStatus}`,
1167
+ });
1168
+ }
1169
+ }
1170
+ } else {
1171
+ const rigVersions = versions.filter(v => v.kind === 'rig' && typeof v.n === 'number');
1172
+ const latestRig = rigVersions[rigVersions.length - 1];
1173
+ const rigN = typeof latestRig?.n === 'number' ? latestRig.n : null;
1174
+
1175
+ if (effectiveStage === 'rigging' && rigN !== null) {
1176
+ const advCmd = candidateAdvance(dir, project, 'rig-review', now);
1177
+ if (advCmd !== null) {
1178
+ addAwait('project.stage');
1179
+ moves.push({
1180
+ role: 'agent',
1181
+ field: 'stage',
1182
+ awaits: 'project.stage',
1183
+ command: advCmd,
1184
+ act: 'advance stage from rigging to rig-review',
1185
+ });
1186
+ }
1187
+ }
1188
+
1189
+ if (rigN === null) {
1190
+ const verdictAwait = 'versions[kind=rig].verdicts[review=rig-review]';
1191
+ addAwait(verdictAwait);
1192
+ moves.push({
1193
+ role: 'human',
1194
+ field: 'versions',
1195
+ awaits: verdictAwait,
1196
+ command: null,
1197
+ act: 'review-tool act review-verdict for rig-review (unanswered by record: versions[kind=rig] is not recorded in project.json)',
1198
+ review_act: {
1199
+ kind: 'review-verdict',
1200
+ stage: 'rig-review',
1201
+ review: 'rig-review',
1202
+ ...(jobId ? { job_id: jobId } : {}),
1203
+ },
1204
+ unanswered_by_record: 'versions[kind=rig]',
1205
+ });
1206
+ } else {
1207
+ const acceptedIdx = latestRig !== undefined ? acceptedReviewVerdict(latestRig, 'rig-review') : -1;
1208
+ const acceptedEntry = acceptedIdx >= 0 ? asArray(latestRig?.verdicts)[acceptedIdx] : undefined;
1209
+ const hasLiveAcceptedRigVerdict =
1210
+ isObject(acceptedEntry) &&
1211
+ liveSignoff(reopens, 'rigging', Date.parse(String(acceptedEntry.at)));
1212
+ const verdictAwait = `versions[n=${rigN}].verdicts[review=rig-review]`;
1213
+ if (!hasLiveAcceptedRigVerdict && !recordedLapseTargets.has(verdictAwait)) {
1214
+ addAwait(verdictAwait);
1215
+ moves.push({
1216
+ role: 'human',
1217
+ field: `versions[v${rigN}].verdicts`,
1218
+ awaits: verdictAwait,
1219
+ command: null,
1220
+ act: `review-tool act review-verdict for versions[v${rigN}] (rig-review)${structureWarnings.some(w => w.path === `versions[v${rigN}].provenance.structure_delta`) ? '; verdict covers changed structure' : ''}`,
1221
+ review_act: {
1222
+ kind: 'review-verdict',
1223
+ stage: 'rig-review',
1224
+ version_id: `v${rigN}`,
1225
+ review: 'rig-review',
1226
+ ...(jobId ? { job_id: jobId } : {}),
1227
+ },
1228
+ });
1229
+ }
1230
+ }
1231
+
1232
+ const signoffAwait = 'signoffs[stage=rig-review]';
1233
+ if (!hasLiveStageSignoff('rig-review') && !recordedLapseTargets.has(signoffAwait)) {
1234
+ addAwait(signoffAwait);
1235
+ moves.push({
1236
+ role: 'human',
1237
+ field: 'signoffs',
1238
+ awaits: signoffAwait,
1239
+ command: null,
1240
+ act: 'review-tool act signoff for stage rig-review',
1241
+ review_act: {
1242
+ kind: 'signoff',
1243
+ stage: 'rig-review',
1244
+ ...(rigN !== null ? { version_id: `v${rigN}` } : {}),
1245
+ ...(jobId ? { job_id: jobId } : {}),
1246
+ },
1247
+ ...(rigN === null ? { unanswered_by_record: 'versions[kind=rig]' } : {}),
1248
+ });
1249
+ } else if (effectiveStage === 'rig-review' && hasLiveStageSignoff('rig-review')) {
1250
+ const nextStage = nextStageInOrder(project, stage);
1251
+ if (nextStage !== null) {
1252
+ const cmd = candidateAdvance(dir, project, nextStage, now);
1253
+ if (cmd !== null) {
1254
+ addAwait('project.stage');
1255
+ moves.push({
1256
+ role: 'agent',
1257
+ field: 'stage',
1258
+ awaits: 'project.stage',
1259
+ command: cmd,
1260
+ act: `advance stage from ${stage} to ${nextStage}`,
1261
+ });
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1266
+ } else if (effectiveStage === 'post' || effectiveStage === 'direction-review') {
1267
+ const postSteps = steps.filter(
1268
+ s => (s.phase === 'rig' || s.phase === 'post') && stageOfStep(s.phase, String(s.id)) === 'post',
1269
+ );
1270
+ const openPostSteps = postSteps.filter(s => s.status !== 'done');
1271
+
1272
+ if (effectiveStage === 'post' && openPostSteps.length > 0) {
1273
+ for (const step of openPostSteps) {
1274
+ const stepId = String(step.id);
1275
+ const currentStatus = String(step.status ?? 'pending');
1276
+ const fromIdx = STEP_STATUSES.indexOf(currentStatus as (typeof STEP_STATUSES)[number]);
1277
+ const nextStatus = fromIdx >= 0 ? STEP_STATUSES[fromIdx + 1] : undefined;
1278
+ if (nextStatus === undefined) continue;
1279
+ const checkpointEnabled = checkpointRequired(project, step, now);
1280
+ if (nextStatus === 'done' && checkpointEnabled && !hasLiveCheckpointSignoff('post', stepId)) {
1281
+ const cpAwait = `signoffs[checkpoint=${stepId}]`;
1282
+ addAwait(cpAwait);
1283
+ if (!recordedLapseTargets.has(cpAwait)) {
1284
+ moves.push({
1285
+ role: 'human',
1286
+ field: `plan.steps[${stepId}].checkpoint`,
1287
+ awaits: cpAwait,
1288
+ command: null,
1289
+ act: `review-tool act signoff-checkpoint for plan.steps[${stepId}]`,
1290
+ review_act: {
1291
+ kind: 'signoff-checkpoint',
1292
+ stage: 'post',
1293
+ step_id: stepId,
1294
+ ...(jobId ? { job_id: jobId } : {}),
1295
+ },
1296
+ });
1297
+ }
1298
+ continue;
1299
+ }
1300
+ const cmd = candidateStepStatus(dir, project, stepId, nextStatus, now);
1301
+ if (cmd !== null) {
1302
+ const statusField = `plan.steps[${stepId}].status`;
1303
+ addAwait(statusField);
1304
+ moves.push({
1305
+ role: 'agent',
1306
+ field: statusField,
1307
+ awaits: statusField,
1308
+ command: cmd,
1309
+ act: `advance plan.steps[${stepId}].status from ${currentStatus} to ${nextStatus}`,
1310
+ });
1311
+ }
1312
+ }
1313
+ } else {
1314
+ const scenes = asArray(project.scenes).filter(isObject);
1315
+ const postVersions = versions.filter(
1316
+ v => v.kind === 'post' && typeof v.n === 'number' && liveSignoff(reopens, 'post', Date.parse(String(v.at))),
1317
+ );
1318
+ const latestPost = postVersions[postVersions.length - 1];
1319
+ const postN = typeof latestPost?.n === 'number' ? latestPost.n : null;
1320
+ const maxVersionN = Math.max(
1321
+ 0,
1322
+ ...versions.map(v => (typeof v.n === 'number' ? v.n : 0)),
1323
+ );
1324
+ const nextPostN = postN ?? maxVersionN + 1;
1325
+
1326
+ if (effectiveStage === 'post') {
1327
+ const advCmd = candidateAdvance(dir, project, 'direction-review', now);
1328
+ if (advCmd !== null) {
1329
+ moves.push({
1330
+ role: 'agent',
1331
+ field: 'stage',
1332
+ awaits: 'project.stage',
1333
+ command: advCmd,
1334
+ act: 'advance stage from post to direction-review',
1335
+ });
1336
+ }
1337
+ }
1338
+
1339
+ if (scenes.length === 0) {
1340
+ addAwait('scenes[n=1]');
1341
+ }
1342
+ if (postN === null) {
1343
+ addAwait(`versions[n=${nextPostN}]`);
1344
+ const verdictAwait = `versions[n=${nextPostN}].verdicts[review=direction-review]`;
1345
+ addAwait(verdictAwait);
1346
+ moves.push({
1347
+ role: 'human',
1348
+ field: 'versions',
1349
+ awaits: verdictAwait,
1350
+ command: null,
1351
+ act: `review-tool act review-verdict for direction-review (unanswered by record: ${scenes.length === 0 ? 'scenes[n=1] and ' : ''}versions[n=${nextPostN}] are not recorded in project.json)`,
1352
+ review_act: {
1353
+ kind: 'review-verdict',
1354
+ stage: 'direction-review',
1355
+ version_id: `v${nextPostN}`,
1356
+ review: 'direction-review',
1357
+ ...(jobId ? { job_id: jobId } : {}),
1358
+ },
1359
+ unanswered_by_record: scenes.length === 0 ? `scenes[n=1], versions[n=${nextPostN}]` : `versions[n=${nextPostN}]`,
1360
+ });
1361
+ } else {
1362
+ const acceptedIdx = latestPost !== undefined ? acceptedReviewVerdict(latestPost, 'direction-review') : -1;
1363
+ const acceptedEntry = acceptedIdx >= 0 ? asArray(latestPost?.verdicts)[acceptedIdx] : undefined;
1364
+ const hasLiveAcceptedPostVerdict =
1365
+ isObject(acceptedEntry) &&
1366
+ liveSignoff(reopens, 'post', Date.parse(String(acceptedEntry.at)));
1367
+ const verdictAwait = `versions[n=${postN}].verdicts[review=direction-review]`;
1368
+ if (!hasLiveAcceptedPostVerdict && !recordedLapseTargets.has(verdictAwait)) {
1369
+ addAwait(verdictAwait);
1370
+ moves.push({
1371
+ role: 'human',
1372
+ field: `versions[v${postN}].verdicts`,
1373
+ awaits: verdictAwait,
1374
+ command: null,
1375
+ act: `review-tool act review-verdict for versions[v${postN}] (direction-review) on the post page: job open --rig ${postN}; ingest with review ${postN} --from-review <actId>; then request record-page sign-off with job open --plan (source-linked Review)`,
1376
+ review_act: {
1377
+ kind: 'review-verdict',
1378
+ stage: 'direction-review',
1379
+ version_id: `v${postN}`,
1380
+ review: 'direction-review',
1381
+ ...(jobId ? { job_id: jobId } : {}),
1382
+ },
1383
+ });
1384
+ }
1385
+ }
1386
+
1387
+ const signoffAwait = 'signoffs[stage=direction-review]';
1388
+ if (!hasLiveStageSignoff('direction-review') && !recordedLapseTargets.has(signoffAwait)) {
1389
+ addAwait(signoffAwait);
1390
+ moves.push({
1391
+ role: 'human',
1392
+ field: 'signoffs',
1393
+ awaits: signoffAwait,
1394
+ command: null,
1395
+ act:
1396
+ postN === null
1397
+ ? `after recording the current post and accepted direction verdict, request review-tool act signoff for direction-review on the record page with job open --plan (source-linked Review; unanswered by record: ${scenes.length === 0 ? 'scenes[n=1] and ' : ''}versions[n=${nextPostN}] are not recorded in project.json)`
1398
+ : `after ingesting the accepted direction-review verdict for v${postN}, request review-tool act signoff on the record page with job open --plan (source-linked Review), ingest signoff direction-review --from-review <actId>, then advance accepted`,
1399
+ review_act: {
1400
+ kind: 'signoff',
1401
+ stage: 'direction-review',
1402
+ version_id: `v${nextPostN}`,
1403
+ ...(jobId ? { job_id: jobId } : {}),
1404
+ },
1405
+ ...(postN === null
1406
+ ? {
1407
+ unanswered_by_record:
1408
+ scenes.length === 0 ? `scenes[n=1], versions[n=${nextPostN}]` : `versions[n=${nextPostN}]`,
1409
+ }
1410
+ : {}),
1411
+ });
1412
+ }
1413
+
1414
+ if (effectiveStage === 'post') {
1415
+ addAwait('project.stage');
1416
+ } else if (hasLiveStageSignoff('direction-review')) {
1417
+ const nextStage = nextStageInOrder(project, stage);
1418
+ if (nextStage !== null) {
1419
+ const cmd = candidateAdvance(dir, project, nextStage, now);
1420
+ if (cmd !== null) {
1421
+ addAwait('project.stage');
1422
+ moves.push({
1423
+ role: 'agent',
1424
+ field: 'stage',
1425
+ awaits: 'project.stage',
1426
+ command: cmd,
1427
+ act: `advance stage from ${stage} to ${nextStage}`,
1428
+ });
1429
+ }
1430
+ }
1431
+ }
1432
+ }
1433
+ }
1434
+
1435
+ if (laggingAdvance !== null && !moves.some(m => m.role === 'agent' && m.command === laggingAdvance.command)) {
1436
+ addAwait('project.stage');
1437
+ moves.unshift({
1438
+ role: 'agent',
1439
+ field: 'stage',
1440
+ awaits: 'project.stage',
1441
+ command: laggingAdvance.command,
1442
+ act: `advance stage from ${stage} to ${laggingAdvance.targetStage}`,
1443
+ });
1444
+ }
1445
+
1446
+ return {
1447
+ schema: NEXT_SCHEMA,
1448
+ project_id: String(project.name ?? ''),
1449
+ stage,
1450
+ effective_stage: effectiveStage,
1451
+ accepted: false,
1452
+ refusals,
1453
+ ...warningFields,
1454
+ awaits: awaitsList,
1455
+ moves,
1456
+ };
1457
+ }
1458
+
1459
+ export function formatNext(report: NextReport): string {
1460
+ const lines: string[] = [];
1461
+ lines.push(
1462
+ `next: ${report.project_id || '(unnamed)'} — stage: ${report.stage}${
1463
+ report.effective_stage !== report.stage ? ` (effective: ${report.effective_stage})` : ''
1464
+ }`,
1465
+ );
1466
+ for (const w of report.warnings ?? []) lines.push(` WARN [${w.code}]: ${w.message}`);
1467
+ if (report.refusals.length > 0) {
1468
+ lines.push(`refusals (${report.refusals.length}):`);
1469
+ for (const r of report.refusals) {
1470
+ lines.push(` [${r.code}] ${r.field}: ${r.message}`);
1471
+ lines.push(` fix: ${r.fix}`);
1472
+ }
1473
+ }
1474
+ if (report.accepted && report.moves.length === 0 && report.refusals.length === 0) {
1475
+ lines.push('no remaining move (project is accepted)');
1476
+ return lines.join('\n');
1477
+ }
1478
+ if (report.moves.length === 0) {
1479
+ lines.push('no forward move (resolve refusals first)');
1480
+ return lines.join('\n');
1481
+ }
1482
+ if (report.awaits.length > 0) {
1483
+ lines.push(`awaits: ${report.awaits.join(', ')}`);
1484
+ }
1485
+ lines.push(`moves (${report.moves.length}):`);
1486
+ for (const move of report.moves) {
1487
+ const label = move.command ?? move.act;
1488
+ lines.push(` [${move.role}] (${move.field} -> ${move.awaits}) ${label}`);
1489
+ if (move.unanswered_by_record) {
1490
+ lines.push(` unanswered by record: ${move.unanswered_by_record}`);
1491
+ }
1492
+ if (move.lapse) {
1493
+ lines.push(` lapse [${move.lapse.code}]: ${move.lapse.source_field} -> ${move.lapse.target_field}`);
1494
+ }
1495
+ }
1496
+ return lines.join('\n');
1497
+ }
1498
+
1499
+ export function nextCommand(args: string[], now: number = Date.now()): { stdout: string; exit: number } {
1500
+ const { dir, json } = parseArgs(args);
1501
+ const report = nextProject(dir, now);
1502
+ return {
1503
+ stdout: json ? JSON.stringify(report, null, 2) : formatNext(report),
1504
+ exit: 0,
1505
+ };
1506
+ }