@garygentry/feature-forge 0.3.8 → 0.3.9

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 (196) hide show
  1. package/README.md +1 -1
  2. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  3. package/adapters/claude/.feature-forge-bundle.json +1 -1
  4. package/adapters/claude/hooks/hooks.json +15 -0
  5. package/adapters/claude/references/forge-config-schema.json +2 -2
  6. package/adapters/claude/references/preflight-and-self-heal.md +1 -1
  7. package/adapters/claude/references/ralph-loop-contract.md +5 -3
  8. package/adapters/claude/references/stage-exit-protocol.md +1 -1
  9. package/adapters/claude/references/vendor-construct-inventory.md +1 -1
  10. package/adapters/claude/scripts/forge-session.py +6 -1
  11. package/adapters/claude/scripts/forge_session/cli.py +2 -1
  12. package/adapters/claude/scripts/forge_session/doctor.py +42 -2
  13. package/adapters/claude/scripts/forge_session/exit.py +5 -5
  14. package/adapters/claude/scripts/forge_session/routes.py +30 -3
  15. package/adapters/claude/scripts/session-check.sh +16 -0
  16. package/adapters/claude/skills/forge/references/stage-exit-protocol.md +1 -1
  17. package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +1 -1
  18. package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +1 -1
  19. package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +1 -1
  20. package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +1 -1
  21. package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +1 -1
  22. package/adapters/claude/skills/forge-5-loop/SKILL.md +12 -22
  23. package/adapters/claude/skills/forge-5-loop/references/preflight-and-self-heal.md +1 -1
  24. package/adapters/claude/skills/forge-5-loop/references/ralph-loop-contract.md +5 -3
  25. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +78 -16
  26. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +111 -12
  27. package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +1 -1
  28. package/adapters/claude/skills/forge-6-docs/references/stage-exit-protocol.md +1 -1
  29. package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +1 -1
  30. package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +2 -2
  31. package/adapters/claude/skills/forge-guide/references/preflight-and-self-heal.md +1 -1
  32. package/adapters/claude/skills/forge-guide/references/ralph-loop-contract.md +5 -3
  33. package/adapters/claude/skills/forge-init/SKILL.md +7 -5
  34. package/adapters/claude/skills/forge-init/references/preflight-and-self-heal.md +1 -1
  35. package/adapters/claude/skills/forge-verify/references/stage-exit-protocol.md +1 -1
  36. package/adapters/codex/.feature-forge-bundle.json +1 -1
  37. package/adapters/codex/references/forge-config-schema.json +2 -2
  38. package/adapters/codex/references/preflight-and-self-heal.md +1 -1
  39. package/adapters/codex/references/ralph-loop-contract.md +5 -3
  40. package/adapters/codex/references/stage-exit-protocol.md +1 -1
  41. package/adapters/codex/references/vendor-construct-inventory.md +1 -1
  42. package/adapters/codex/scripts/forge-session.py +6 -1
  43. package/adapters/codex/scripts/forge_session/cli.py +2 -1
  44. package/adapters/codex/scripts/forge_session/doctor.py +42 -2
  45. package/adapters/codex/scripts/forge_session/exit.py +5 -5
  46. package/adapters/codex/scripts/forge_session/routes.py +30 -3
  47. package/adapters/codex/skills/forge/references/stage-exit-protocol.md +1 -1
  48. package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +1 -1
  49. package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +1 -1
  50. package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +1 -1
  51. package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +1 -1
  52. package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +1 -1
  53. package/adapters/codex/skills/forge-5-loop/SKILL.md +12 -22
  54. package/adapters/codex/skills/forge-5-loop/references/preflight-and-self-heal.md +1 -1
  55. package/adapters/codex/skills/forge-5-loop/references/ralph-loop-contract.md +5 -3
  56. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +78 -16
  57. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +111 -12
  58. package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +1 -1
  59. package/adapters/codex/skills/forge-6-docs/references/stage-exit-protocol.md +1 -1
  60. package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +1 -1
  61. package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +2 -2
  62. package/adapters/codex/skills/forge-guide/references/preflight-and-self-heal.md +1 -1
  63. package/adapters/codex/skills/forge-guide/references/ralph-loop-contract.md +5 -3
  64. package/adapters/codex/skills/forge-init/SKILL.md +7 -5
  65. package/adapters/codex/skills/forge-init/references/preflight-and-self-heal.md +1 -1
  66. package/adapters/codex/skills/forge-verify/references/stage-exit-protocol.md +1 -1
  67. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  68. package/adapters/copilot/references/forge-config-schema.json +2 -2
  69. package/adapters/copilot/references/preflight-and-self-heal.md +1 -1
  70. package/adapters/copilot/references/ralph-loop-contract.md +5 -3
  71. package/adapters/copilot/references/stage-exit-protocol.md +1 -1
  72. package/adapters/copilot/references/vendor-construct-inventory.md +1 -1
  73. package/adapters/copilot/scripts/forge-session.py +6 -1
  74. package/adapters/copilot/scripts/forge_session/cli.py +2 -1
  75. package/adapters/copilot/scripts/forge_session/doctor.py +42 -2
  76. package/adapters/copilot/scripts/forge_session/exit.py +5 -5
  77. package/adapters/copilot/scripts/forge_session/routes.py +30 -3
  78. package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +1 -1
  79. package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +1 -1
  80. package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +1 -1
  81. package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +1 -1
  82. package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +1 -1
  83. package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +1 -1
  84. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +12 -22
  85. package/adapters/copilot/skills/forge-5-loop/references/preflight-and-self-heal.md +1 -1
  86. package/adapters/copilot/skills/forge-5-loop/references/ralph-loop-contract.md +5 -3
  87. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +78 -16
  88. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +111 -12
  89. package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +1 -1
  90. package/adapters/copilot/skills/forge-6-docs/references/stage-exit-protocol.md +1 -1
  91. package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +1 -1
  92. package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +2 -2
  93. package/adapters/copilot/skills/forge-guide/references/preflight-and-self-heal.md +1 -1
  94. package/adapters/copilot/skills/forge-guide/references/ralph-loop-contract.md +5 -3
  95. package/adapters/copilot/skills/forge-init/forge-init.md +7 -5
  96. package/adapters/copilot/skills/forge-init/references/preflight-and-self-heal.md +1 -1
  97. package/adapters/copilot/skills/forge-verify/references/stage-exit-protocol.md +1 -1
  98. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  99. package/adapters/cursor/references/forge-config-schema.json +2 -2
  100. package/adapters/cursor/references/preflight-and-self-heal.md +1 -1
  101. package/adapters/cursor/references/ralph-loop-contract.md +5 -3
  102. package/adapters/cursor/references/stage-exit-protocol.md +1 -1
  103. package/adapters/cursor/references/vendor-construct-inventory.md +1 -1
  104. package/adapters/cursor/scripts/forge-session.py +6 -1
  105. package/adapters/cursor/scripts/forge_session/cli.py +2 -1
  106. package/adapters/cursor/scripts/forge_session/doctor.py +42 -2
  107. package/adapters/cursor/scripts/forge_session/exit.py +5 -5
  108. package/adapters/cursor/scripts/forge_session/routes.py +30 -3
  109. package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +1 -1
  110. package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +1 -1
  111. package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +1 -1
  112. package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +1 -1
  113. package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +1 -1
  114. package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +1 -1
  115. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +12 -22
  116. package/adapters/cursor/skills/forge-5-loop/references/preflight-and-self-heal.md +1 -1
  117. package/adapters/cursor/skills/forge-5-loop/references/ralph-loop-contract.md +5 -3
  118. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +78 -16
  119. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +111 -12
  120. package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +1 -1
  121. package/adapters/cursor/skills/forge-6-docs/references/stage-exit-protocol.md +1 -1
  122. package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +1 -1
  123. package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +2 -2
  124. package/adapters/cursor/skills/forge-guide/references/preflight-and-self-heal.md +1 -1
  125. package/adapters/cursor/skills/forge-guide/references/ralph-loop-contract.md +5 -3
  126. package/adapters/cursor/skills/forge-init/forge-init.mdc +7 -5
  127. package/adapters/cursor/skills/forge-init/references/preflight-and-self-heal.md +1 -1
  128. package/adapters/cursor/skills/forge-verify/references/stage-exit-protocol.md +1 -1
  129. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  130. package/adapters/gemini/gemini-extension.json +1 -1
  131. package/adapters/gemini/references/forge-config-schema.json +2 -2
  132. package/adapters/gemini/references/preflight-and-self-heal.md +1 -1
  133. package/adapters/gemini/references/ralph-loop-contract.md +5 -3
  134. package/adapters/gemini/references/stage-exit-protocol.md +1 -1
  135. package/adapters/gemini/references/vendor-construct-inventory.md +1 -1
  136. package/adapters/gemini/scripts/forge-session.py +6 -1
  137. package/adapters/gemini/scripts/forge_session/cli.py +2 -1
  138. package/adapters/gemini/scripts/forge_session/doctor.py +42 -2
  139. package/adapters/gemini/scripts/forge_session/exit.py +5 -5
  140. package/adapters/gemini/scripts/forge_session/routes.py +30 -3
  141. package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +1 -1
  142. package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +1 -1
  143. package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +1 -1
  144. package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +1 -1
  145. package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +1 -1
  146. package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +1 -1
  147. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +12 -22
  148. package/adapters/gemini/skills/forge-5-loop/references/preflight-and-self-heal.md +1 -1
  149. package/adapters/gemini/skills/forge-5-loop/references/ralph-loop-contract.md +5 -3
  150. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +78 -16
  151. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +111 -12
  152. package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +1 -1
  153. package/adapters/gemini/skills/forge-6-docs/references/stage-exit-protocol.md +1 -1
  154. package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +1 -1
  155. package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +2 -2
  156. package/adapters/gemini/skills/forge-guide/references/preflight-and-self-heal.md +1 -1
  157. package/adapters/gemini/skills/forge-guide/references/ralph-loop-contract.md +5 -3
  158. package/adapters/gemini/skills/forge-init/forge-init.md +7 -5
  159. package/adapters/gemini/skills/forge-init/references/preflight-and-self-heal.md +1 -1
  160. package/adapters/gemini/skills/forge-verify/references/stage-exit-protocol.md +1 -1
  161. package/adapters/pi/.feature-forge-bundle.json +1 -1
  162. package/adapters/pi/references/forge-config-schema.json +2 -2
  163. package/adapters/pi/references/preflight-and-self-heal.md +1 -1
  164. package/adapters/pi/references/ralph-loop-contract.md +5 -3
  165. package/adapters/pi/references/stage-exit-protocol.md +1 -1
  166. package/adapters/pi/references/vendor-construct-inventory.md +1 -1
  167. package/adapters/pi/scripts/forge-session.py +6 -1
  168. package/adapters/pi/scripts/forge_session/cli.py +2 -1
  169. package/adapters/pi/scripts/forge_session/doctor.py +42 -2
  170. package/adapters/pi/scripts/forge_session/exit.py +5 -5
  171. package/adapters/pi/scripts/forge_session/routes.py +30 -3
  172. package/adapters/pi/skills/forge/references/stage-exit-protocol.md +1 -1
  173. package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +1 -1
  174. package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +1 -1
  175. package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +1 -1
  176. package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +1 -1
  177. package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +1 -1
  178. package/adapters/pi/skills/forge-5-loop/SKILL.md +12 -22
  179. package/adapters/pi/skills/forge-5-loop/references/preflight-and-self-heal.md +1 -1
  180. package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +5 -3
  181. package/adapters/pi/skills/forge-5-loop/references/result-reporting.md +78 -16
  182. package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +111 -12
  183. package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +1 -1
  184. package/adapters/pi/skills/forge-6-docs/references/stage-exit-protocol.md +1 -1
  185. package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +1 -1
  186. package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +2 -2
  187. package/adapters/pi/skills/forge-guide/references/preflight-and-self-heal.md +1 -1
  188. package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +5 -3
  189. package/adapters/pi/skills/forge-init/SKILL.md +7 -5
  190. package/adapters/pi/skills/forge-init/references/preflight-and-self-heal.md +1 -1
  191. package/adapters/pi/skills/forge-verify/references/stage-exit-protocol.md +1 -1
  192. package/dist/manifest.d.ts +1 -1
  193. package/dist/rauf.d.ts +3 -3
  194. package/dist/rauf.js +2 -2
  195. package/dist/types.d.ts +1 -1
  196. package/package.json +1 -1
@@ -329,10 +329,10 @@ def stage_exit(
329
329
  capability is permission, not tool presence: a dispatch permitted
330
330
  only once the user has asked is still `interactive`, because the
331
331
  `standard` gate's own prompt supplies that request.
332
- cause: Pending-attribution annotation (`dependency-starvation`), valid
333
- only with `--stage forge-5-loop --outcome partial` (REQ-ATTR-04).
334
- It swaps the partial next-steps sentence for the starvation variant
335
- and changes no routing.
332
+ cause: Pending-attribution annotation (`dependency-starvation`,
333
+ `review-pending` or `runner-stopped`), valid only with `--stage forge-5-loop --outcome
334
+ partial` (REQ-ATTR-04, #339). It swaps the partial next-steps sentence
335
+ for the matching variant and changes no routing.
336
336
 
337
337
  Returns:
338
338
  A JSON-serializable `StageExitPayload` dictionary.
@@ -486,7 +486,7 @@ def stage_exit(
486
486
  # argparse `choices` already restricts the value; this restricts the combination.
487
487
  if cause is not None and not (stage == "forge-5-loop" and outcome == "partial"):
488
488
  raise UsageError(
489
- "--cause dependency-starvation is valid only with "
489
+ f"--cause {cause} is valid only with "
490
490
  "--stage forge-5-loop --outcome partial"
491
491
  )
492
492
 
@@ -799,6 +799,27 @@ _LOOP_PARTIAL_STARVED_TEXT: Final[str] = (
799
799
  "resumable and nothing downstream is ready: unblock the roots named in the "
800
800
  "starvation report above, then run the loop again below to continue."
801
801
  )
802
+ #: The pending-review variant of the `partial` next-steps sentence: every item may be
803
+ #: done, but the runner's review pass failed or was interrupted, so the run is not
804
+ #: complete. Selected only by ``--cause review-pending`` (issue #339); the route is
805
+ #: still the loop resume, whose Step 2a re-runs exactly that review.
806
+ _LOOP_PARTIAL_REVIEW_PENDING_TEXT: Final[str] = (
807
+ "The loop for {feature} is not finished — its review pass failed or was "
808
+ "interrupted, so the review is still pending even if every item is done. The "
809
+ "recorded state is resumable and nothing downstream is ready: run the loop again "
810
+ "below to re-run the pending review."
811
+ )
812
+ #: The unfinished-runner variant of the `partial` next-steps sentence: the runner's
813
+ #: own terminal state (a crash, a stop, a stale lock, a limit halt) says it did not
814
+ #: finish cleanly, so the run is not complete even when every item reads `done`.
815
+ #: Selected only by ``--cause runner-stopped`` (issue #339); the route is still the
816
+ #: loop resume, whose Step 2a offers the runner's own resume.
817
+ _LOOP_PARTIAL_RUNNER_STOPPED_TEXT: Final[str] = (
818
+ "The loop runner for {feature} did not reach a clean finish — it crashed, was "
819
+ "stopped, or halted on a limit — so the run is not complete even if every item "
820
+ "reads done. The recorded state is resumable and nothing downstream is ready: run "
821
+ "the loop again below to resume the runner."
822
+ )
802
823
  #: The `complete` preamble, selected by where the handoff actually lands. The epic
803
824
  #: rows name the epic and its live rollup, so the operator can see WHY the handoff is
804
825
  #: this member's own documentation rather than another member (or vice versa).
@@ -956,9 +977,11 @@ def _loop_route(
956
977
  handoff: findings already exist at this exact revision, so the fenced
957
978
  action is applying them, exactly as on a production re-exit.
958
979
  cause: The already-validated attribution annotation — only
959
- ``"dependency-starvation"`` with ``outcome == "partial"``, else None.
960
- Swaps the partial next-steps sentence for the starvation variant; the
961
- route itself is unchanged (partial stays a resume either way).
980
+ ``"dependency-starvation"``, ``"review-pending"`` or
981
+ ``"runner-stopped"`` with
982
+ ``outcome == "partial"``, else None. Swaps the partial next-steps
983
+ sentence for the matching variant; the route itself is unchanged
984
+ (partial stays a resume either way).
962
985
 
963
986
  Returns:
964
987
  `(primary_canonical, deferred_canonical, outcome_text, advancing)`, matching
@@ -985,6 +1008,10 @@ def _loop_route(
985
1008
  )
986
1009
  if outcome == "partial" and cause == "dependency-starvation":
987
1010
  text = _LOOP_PARTIAL_STARVED_TEXT.format(feature=feature)
1011
+ elif outcome == "partial" and cause == "review-pending":
1012
+ text = _LOOP_PARTIAL_REVIEW_PENDING_TEXT.format(feature=feature)
1013
+ elif outcome == "partial" and cause == "runner-stopped":
1014
+ text = _LOOP_PARTIAL_RUNNER_STOPPED_TEXT.format(feature=feature)
988
1015
  else:
989
1016
  text = _LOOP_OUTCOME_TEXT[outcome].format(feature=feature)
990
1017
  return primary, None, text, False
@@ -47,7 +47,7 @@ an epic member. Only the flags below are stage-specific; pass no others.
47
47
  |---|---|---|
48
48
  | `forge-0-epic` | `forge-0-epic` | `--next-feature "{member}"` when a concrete member exists |
49
49
  | `forge-1-prd` … `forge-4-backlog` | that stage's own id | none beyond identity/capability |
50
- | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation` with `--outcome partial` |
50
+ | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation`, `review-pending` or `runner-stopped` with `--outcome partial` |
51
51
  | `forge-6-docs` | `forge-6-docs` | `--outcome` — `complete`, `blocked`, or `skipped` (deliberate docs skip, persisted via `state-skip` before the exit; routes like `complete` with honest wording) |
52
52
  | direct `forge-verify` | `forge-verify` | `--owner direct`, `--outcome` (`passed`, `findings`, `skipped`, `failed`), and served-stage metadata |
53
53
  | nested `forge-verify` | `forge-verify` | `--owner nested`, plus the same outcome and served-stage metadata |
@@ -47,7 +47,7 @@ an epic member. Only the flags below are stage-specific; pass no others.
47
47
  |---|---|---|
48
48
  | `forge-0-epic` | `forge-0-epic` | `--next-feature "{member}"` when a concrete member exists |
49
49
  | `forge-1-prd` … `forge-4-backlog` | that stage's own id | none beyond identity/capability |
50
- | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation` with `--outcome partial` |
50
+ | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation`, `review-pending` or `runner-stopped` with `--outcome partial` |
51
51
  | `forge-6-docs` | `forge-6-docs` | `--outcome` — `complete`, `blocked`, or `skipped` (deliberate docs skip, persisted via `state-skip` before the exit; routes like `complete` with honest wording) |
52
52
  | direct `forge-verify` | `forge-verify` | `--owner direct`, `--outcome` (`passed`, `findings`, `skipped`, `failed`), and served-stage metadata |
53
53
  | nested `forge-verify` | `forge-verify` | `--owner nested`, plus the same outcome and served-stage metadata |
@@ -47,7 +47,7 @@ an epic member. Only the flags below are stage-specific; pass no others.
47
47
  |---|---|---|
48
48
  | `forge-0-epic` | `forge-0-epic` | `--next-feature "{member}"` when a concrete member exists |
49
49
  | `forge-1-prd` … `forge-4-backlog` | that stage's own id | none beyond identity/capability |
50
- | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation` with `--outcome partial` |
50
+ | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation`, `review-pending` or `runner-stopped` with `--outcome partial` |
51
51
  | `forge-6-docs` | `forge-6-docs` | `--outcome` — `complete`, `blocked`, or `skipped` (deliberate docs skip, persisted via `state-skip` before the exit; routes like `complete` with honest wording) |
52
52
  | direct `forge-verify` | `forge-verify` | `--owner direct`, `--outcome` (`passed`, `findings`, `skipped`, `failed`), and served-stage metadata |
53
53
  | nested `forge-verify` | `forge-verify` | `--owner nested`, plus the same outcome and served-stage metadata |
@@ -47,7 +47,7 @@ an epic member. Only the flags below are stage-specific; pass no others.
47
47
  |---|---|---|
48
48
  | `forge-0-epic` | `forge-0-epic` | `--next-feature "{member}"` when a concrete member exists |
49
49
  | `forge-1-prd` … `forge-4-backlog` | that stage's own id | none beyond identity/capability |
50
- | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation` with `--outcome partial` |
50
+ | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation`, `review-pending` or `runner-stopped` with `--outcome partial` |
51
51
  | `forge-6-docs` | `forge-6-docs` | `--outcome` — `complete`, `blocked`, or `skipped` (deliberate docs skip, persisted via `state-skip` before the exit; routes like `complete` with honest wording) |
52
52
  | direct `forge-verify` | `forge-verify` | `--owner direct`, `--outcome` (`passed`, `findings`, `skipped`, `failed`), and served-stage metadata |
53
53
  | nested `forge-verify` | `forge-verify` | `--owner nested`, plus the same outcome and served-stage metadata |
@@ -47,7 +47,7 @@ an epic member. Only the flags below are stage-specific; pass no others.
47
47
  |---|---|---|
48
48
  | `forge-0-epic` | `forge-0-epic` | `--next-feature "{member}"` when a concrete member exists |
49
49
  | `forge-1-prd` … `forge-4-backlog` | that stage's own id | none beyond identity/capability |
50
- | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation` with `--outcome partial` |
50
+ | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation`, `review-pending` or `runner-stopped` with `--outcome partial` |
51
51
  | `forge-6-docs` | `forge-6-docs` | `--outcome` — `complete`, `blocked`, or `skipped` (deliberate docs skip, persisted via `state-skip` before the exit; routes like `complete` with honest wording) |
52
52
  | direct `forge-verify` | `forge-verify` | `--owner direct`, `--outcome` (`passed`, `findings`, `skipped`, `failed`), and served-stage metadata |
53
53
  | nested `forge-verify` | `forge-verify` | `--owner nested`, plus the same outcome and served-stage metadata |
@@ -47,7 +47,7 @@ an epic member. Only the flags below are stage-specific; pass no others.
47
47
  |---|---|---|
48
48
  | `forge-0-epic` | `forge-0-epic` | `--next-feature "{member}"` when a concrete member exists |
49
49
  | `forge-1-prd` … `forge-4-backlog` | that stage's own id | none beyond identity/capability |
50
- | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation` with `--outcome partial` |
50
+ | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation`, `review-pending` or `runner-stopped` with `--outcome partial` |
51
51
  | `forge-6-docs` | `forge-6-docs` | `--outcome` — `complete`, `blocked`, or `skipped` (deliberate docs skip, persisted via `state-skip` before the exit; routes like `complete` with honest wording) |
52
52
  | direct `forge-verify` | `forge-verify` | `--owner direct`, `--outcome` (`passed`, `findings`, `skipped`, `failed`), and served-stage metadata |
53
53
  | nested `forge-verify` | `forge-verify` | `--owner nested`, plus the same outcome and served-stage metadata |
@@ -110,7 +110,7 @@ The runner commits each item onto the current branch. Skip if not a git repo or
110
110
 
111
111
  ### 1g. Stranded-Work Pre-flight (if using git)
112
112
 
113
- Run `git status --porcelain`. If it reports changes **and** `{backlogDir}/{loopRunner.stateDir}/state.json` exists from a previous run, **STOP**: name that run (its `startedAt`, `currentItem`, and `blockedItems` from `state.json`) and point the user at the **Post-Run Tree Reconciliation** section of `references/recovery-procedure.md` to commit / stash / discard the stranded work before relaunch — never auto-pass `--force`. If the tree is dirty with **no** prior-run `state.json`, keep today's behavior (surface it; let the user commit/stash or pass `--force`). A clean tree is silent. rauf's own launch refusal remains the backstop.
113
+ Run `git status --porcelain`. If it reports changes **and** `{backlogDir}/{loopRunner.stateDir}/state.json` exists from a previous run, **STOP**: name that run (its `startedAt`, `currentItem`, and `blockedItems` from `state.json`) and point the user at the **Post-Run Tree Reconciliation** section of `references/recovery-procedure.md` to commit / stash / discard the stranded work before relaunch — never auto-pass `--force`. If the tree is dirty with **no** prior-run `state.json`, keep today's behavior (surface it; let the user commit/stash or pass `--force`). A clean tree is silent.
114
114
 
115
115
  ## Step 2: Construct the Loop Command
116
116
 
@@ -120,7 +120,7 @@ Run the **list command** (`loopRunner.listCommand`, default `rauf backlog list .
120
120
 
121
121
  Calculate the iteration count: `ceil((pending + in_progress) * loopIterationMultiplier)` where `loopIterationMultiplier` comes from `forge.config.json` (default: 1.5, headroom for retries).
122
122
 
123
- If there are no pending or in_progress items, STOP and tell the user: "All backlog items are already done or blocked. Nothing to run."
123
+ Then, **whatever the counts**, run the **status-json command**: `reviewPending: true` (optional field) means a prior review pass failed or was interrupted — follow **Pending review** in `references/runner-contract.md` before any fresh run (which would drop it). Otherwise, with no pending or in_progress items: a reported `loopState` that is not a clean finish → follow **Unfinished runner** in that file; else STOP and tell the user: "All backlog items are already done or blocked. Nothing to run."
124
124
 
125
125
  If there are `blocked` items, note them — the user may want `--retry-blocked`.
126
126
 
@@ -186,11 +186,11 @@ R="$(bash -c '[ -z "${FEATURE_FORGE_ROOT:-}" ] || [ -x "$FEATURE_FORGE_ROOT/scri
186
186
  python3 "$R/scripts/forge-session.py" state-enter --feature "{feature}" --stage forge-5-loop --specs-dir "{specsDir}"
187
187
  ```
188
188
 
189
- Then commit this state write before launching (mandatory). The runner refuses to run with uncommitted changes (*"…pass --force"*), and this marker is itself one — so an otherwise-clean repo fails its first launch unless committed. Commit it via the shared-conventions **Git Commit Protocol** (epic members: stage `{specsDir}/{epic}/`): `{commitPrefix}({feature}): forge-5-loop in-progress` — a launch precondition, required regardless of `gitCommitAfterStage`. Unrelated leftover changes still trip the refusal; surface it, never auto-pass `--force`. See `references/runner-contract.md`.
189
+ Then commit this state write before launching (mandatory — the runner refuses a dirty tree, and this marker is itself a change) via the shared-conventions **Git Commit Protocol** (epic members: stage `{specsDir}/{epic}/`): `{commitPrefix}({feature}): forge-5-loop in-progress`, regardless of `gitCommitAfterStage`. Unrelated leftover changes still trip the refusal; surface it, never auto-pass `--force`.
190
190
 
191
191
  ### 3b. Launch Background Process
192
192
 
193
- Launch the loop **backgrounded** (host's background-execution mechanism: true) so it survives session end and does not block the session. For a runner that **persists its own structured event file** (the default — rauf writes `{stateDir}/events.ndjson` natively and rotates it per run), launch the **plain `runCommand`** with **no stdout redirect** and supervise the runner's **native** `events.ndjson` directly; do **not** redirect `--ndjson` into `{stateDir}` (it is redundant and collides with the runner's own writer — see `references/runner-contract.md`). Only a stdout-only runner (no native event file) uses `eventStreamCommand`, redirected to a file **outside** `{stateDir}`. The background task's exit notification is the single authoritative terminal signal (Step 4). For the exact launch commands (incl. the `mkdir -p` state-dir guard and the root→`IS_SANDBOX` sandbox guard) and the self-persisting vs. stdout-only detail, read `references/runner-contract.md`.
193
+ Launch the loop **backgrounded** (host's background-execution mechanism: true) so it survives session end and does not block the session. For a runner that **persists its own structured event file** (the default — rauf writes and rotates `{stateDir}/events.ndjson` natively), launch the **plain `runCommand`** with **no stdout redirect** and supervise that native file; never redirect `--ndjson` into `{stateDir}` (it collides with the runner's own writer). Only a stdout-only runner (no native event file) uses `eventStreamCommand`, redirected to a file **outside** `{stateDir}`. The background task's exit notification is the single authoritative terminal signal (Step 4). For the exact launch commands (incl. the `mkdir -p` state-dir guard and the root→`IS_SANDBOX` sandbox guard) and the self-persisting vs. stdout-only detail, read `references/runner-contract.md`.
194
194
 
195
195
  ### 3c. Inform User
196
196
 
@@ -198,21 +198,11 @@ Follow the **Inform-user output template (Step 3c)** section of `references/runn
198
198
 
199
199
  ### 3d. Arm a Monitor on the event stream, and react to events
200
200
 
201
- Arm the **host's monitoring mechanism** on the structured event stream (the NDJSON file, or the
202
- human log as fallback) with **`persistent: true`**, a coverage-complete filter
203
- matching every terminal and exception state (silence is not success), and react to
204
- each event as it arrives. Each NDJSON line is a JSON object whose event kind lives
205
- under the **`type`** field (rauf's schema — e.g. `{"type":"item_completed",…}`), so
206
- the filter must select on **`.type`**, **not** `kind`: a `kind`-keyed filter surfaces
207
- nothing (no `grep` match; `jq` aborts on the null `.kind`), so the watch stays dark. The exact Monitor
208
- commands, the filter event list, and the full per-event reaction rules (`needs_human`
209
- / `loop_error` surfaced immediately with a `PushNotification`, `item_completed`
210
- coalesced into milestones, `llm_stuck_warning` as a hang warning) are in
211
- `references/runner-contract.md` — follow them verbatim.
201
+ Arm the **host's monitoring mechanism** (`persistent: true`) on the structured event stream (the NDJSON file, or the human log as fallback) with a coverage-complete filter matching every terminal and exception state (silence is not success). The filter selects on each NDJSON line's **`.type`** field, **never** `kind` (a `kind`-keyed filter matches nothing and the watch stays dark). The exact commands, the filter list (incl. `review_failed` and usage-limit events), and the per-event reactions (`needs_human` / `loop_error` / `review_failed` surfaced with a `PushNotification`, `item_completed` coalesced into milestones, `llm_stuck_warning` reported with its in-flight tool) are in `references/runner-contract.md` — follow them verbatim.
212
202
 
213
203
  ### 3f. Reach completion
214
204
 
215
- Step 4 is reached when the backgrounded process exits (its completion notification is authoritative); the `loop_completed` / `loop_error` / `loop_cancelled` event is the live heads-up that it's imminent. Stop the Monitor (it ends on its own when `tail` sees the process-ended log, or via `TaskStop`) and proceed to Step 4. Do NOT foreground-sleep or poll — the harness drives both the Monitor events and the completion notification.
205
+ Step 4 is reached when the backgrounded process exits (its completion notification is authoritative; a `loop_completed` / `loop_error` / `loop_cancelled` event is the live heads-up). Stop the Monitor (`TaskStop` if it has not ended) and proceed. Do NOT foreground-sleep or poll — the harness drives both signals.
216
206
 
217
207
  ## Step 4: Check Results
218
208
 
@@ -224,7 +214,7 @@ Run the **status-json command** (`loopRunner.statusJsonCommand`) and read
224
214
  `backlogSummary` for the authoritative counts — it separates the three non-done
225
215
  outcomes: genuine `blocked`, `needsHuman`, and runner-`deferred` ("false blocks").
226
216
  Fall back to the **list command** (`loopRunner.listCommand`) if `statusJsonCommand`
227
- is not configured. If the run used a review flag (e.g. rauf's `--review`), also read any `review_completed` event (event stream, or `{loopRunner.stateDir}/events.ndjson`) for its `itemsCreated`/`summary` to surface in 4b — see `references/result-reporting.md`.
217
+ is not configured. Also read the optional `loopState`, `reviewPending`, `lock` fields (absent on older runners = unset); **Runner terminal states** in `references/result-reporting.md` maps them. **`reviewPending: true` is never complete, whatever the counts** — offer the resume per **Pending review** in `references/runner-contract.md` before 4b. If the run used a review flag (e.g. rauf's `--review`), also read any `review_completed` event (event stream, or `{loopRunner.stateDir}/events.ndjson`) for its `itemsCreated`/`summary` to surface in 4b — see `references/result-reporting.md`.
228
218
 
229
219
  ### 4b. Report Results
230
220
 
@@ -233,11 +223,11 @@ blocked and needs-human) and render its report. The five verbatim result-report
233
223
 
234
224
  ### 4c. Post-Run Recovery Pass (unconditional)
235
225
 
236
- Run the **Post-Run Recovery Procedure** (`references/recovery-procedure.md`) now — on **every** run close, before Step 5 writes state, so the tree it inspects is exactly what the run left. The live `needs_human` handler (3d) collects answers early but is **not** the entry condition: a run that emitted no event still enters here — this is what makes the plain-blocked unblock reachable on blocked-only runs. With nothing to decide, its step 1 skips straight to the §4 tree reconciliation — silent on a clean tree. A run can strand uncommitted work with no signal (items failing a shared final acceptance criterion are never committed); this pass reconciles it — 1g's pre-flight is only the next-launch backstop. Its step-7 gate feeds Step 7's `resolved` rung; the stage still closes exactly once, in Step 7.
226
+ Run the **Post-Run Recovery Procedure** (`references/recovery-procedure.md`) now — on **every** run close, before Step 5 writes state, so the tree it inspects is exactly what the run left. The live `needs_human` handler (3d) is **not** the entry condition: a run that emitted no event still enters here, so blocked-only runs reach the unblock. With nothing to decide, its step 1 skips to the §4 tree reconciliation — silent on a clean tree — which also reconciles work a run stranded uncommitted with no signal (1g is only the next-launch backstop). Its step-7 gate feeds Step 7's `resolved` rung; the stage still closes exactly once, in Step 7.
237
227
 
238
228
  ## Step 5: Update Pipeline State
239
229
 
240
- Record completion by running `state-complete` (below). Evaluate "all backlog items are `done`" yourself and pass the result as `--status`: `complete` if every item is `done`, else `in-progress`. The verb records `completedAt`, the version, `basedOnVersions` and `artifacts`, and refreshes `updatedAt`. Add `--epic "{epic}"` when this feature is an epic member — required, per the Pipeline State Protocol.
230
+ Record completion by running `state-complete` (below). Evaluate "all backlog items are `done`" yourself and pass the result as `--status`: `complete` only when Step 7's ladder selects `complete` (every item `done`, `total > 0`, no pending review, a clean runner finish per `references/result-reporting.md`), else `in-progress`. The verb records `completedAt`, the version, `basedOnVersions` and `artifacts`, and refreshes `updatedAt`. Add `--epic "{epic}"` when this feature is an epic member — required, per the Pipeline State Protocol.
241
231
 
242
232
  ```bash
243
233
  R="$(bash -c '[ -z "${FEATURE_FORGE_ROOT:-}" ] || [ -x "$FEATURE_FORGE_ROOT/scripts/forge-root.sh" ] || { echo "feature-forge: FEATURE_FORGE_ROOT=$FEATURE_FORGE_ROOT has no scripts/forge-root.sh" >&2; exit 2; }; for d in "${FEATURE_FORGE_ROOT:-}" "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
@@ -272,7 +262,7 @@ python3 "$R/scripts/forge-session.py" state-verify --feature "{feature}" --stage
272
262
 
273
263
  Every loop run ends here, and ends here **exactly once** — standalone or epic member, complete or not.
274
264
 
275
- First select the single `LoopOutcome` with the ladder in `references/result-reporting.md` (`resolved` → `needs-human` → `blocked` → `deferred` → `partial` → `complete`, first match wins), reading it from Step 4a's authoritative counts and never from the runner's process exit code. If those counts were never obtained, follow that file's operational-failure rule instead: report the failure and its recovery and run no exit at all.
265
+ First select the single `LoopOutcome` with the ladder in `references/result-reporting.md` (`resolved` → unfinished-runner `partial` → `needs-human` → `blocked` → `deferred` → `partial` → `complete`, first match wins, plus any `--cause` it names), reading it from Step 4a's authoritative counts and never from the runner's process exit code. If those counts were never obtained, follow that file's operational-failure rule instead: report the failure and its recovery and run no exit at all.
276
266
 
277
267
  **Close this stage with the Scripted Stage Exit** (contract: `references/stage-exit-protocol.md`; do not improvise a "Next steps" list). Run:
278
268
 
@@ -291,8 +281,8 @@ Add `--epic "{epic}"` when this feature is an epic member — required, per the
291
281
  - **Plugin-root discovery (1b-epic helper) covers installed paths, not workspace-dev checkouts.** The `forge-root.sh` search probes the locations of an **installed** plugin only, so a feature-forge **source checkout** (e.g. `~/workspace/feature-forge`) exits "cannot locate plugin root" unless `FEATURE_FORGE_ROOT` points at it (#323). Expected in a dev environment; run the epic-manifest script from the checkout directly (`python3 <checkout>/scripts/epic-manifest.py …`).
292
282
  - `{backlogDir}` is a **directory path**, not a file path. Pass `specs/auth`, not `specs/auth/backlog.json`.
293
283
  - rauf resolves `RAUF.md` with fallback (`{backlogDir}/.rauf/RAUF.md` first, then the project's `.rauf/RAUF.md`). State files (state.json, {loopRunner.logFile}, etc.) land at `{backlogDir}/{loopRunner.stateDir}/`, isolated per backlog dir, so concurrent features don't collide.
294
- - If the session disconnects mid-loop, the runner process continues independently — check results later with the status / list commands. A stale lock from a previous run may need `--force` to clear.
295
- - Never run the run command in the foreground (without host's background-execution mechanism) — it blocks and will hit the Bash tool timeout for any non-trivial backlog. "Don't block the foreground" is NOT "stay silent": supervise via the host's monitoring mechanism (3d). A `needs_human`/`blocked`/`review` signal does **not** pause the loop — the runner sets the item aside and keeps going; surface it live but don't tell the user the loop is waiting. See `references/runner-contract.md` for the full monitoring rules.
284
+ - If the session disconnects mid-loop, the runner process continues independently — check results later with the status / list commands. A crashed run's stale lock (`PAUSED` + `lock.stale`) is cleared by rauf's `resume`, not `--force` (see `references/result-reporting.md`).
285
+ - Never run the run command in the foreground (without host's background-execution mechanism) — it blocks and will hit the Bash tool timeout for any non-trivial backlog. "Don't block the foreground" is NOT "stay silent": supervise via the host's monitoring mechanism (3d). A `needs_human`/`blocked`/`review` signal does **not** pause the loop — the runner sets the item aside and keeps going; surface it live but don't tell the user the loop is waiting.
296
286
  - The version gate (1c) uses the `--json` form on purpose; never parse `rauf version`'s human output.
297
287
  - **Implementation artifacts must not cite specs.** The loop should **read** specs and `backlog.json` freely — they are the source of truth, and the backlog rightly cites specs for provenance. But artifacts the loop **writes into the target repo** (source code, generated `SKILL.md`/agent files, configs, code comments) must be **self-contained**: no references to feature-forge spec files (no `See specs/{feature}/NN-*.md`, no "source spec" provenance notes) — specs are pre-implementation inputs that may be archived or deleted once the feature ships. This applies only to shipped implementation output, never to the backlog or spec documents, which keep citing specs.
298
288
 
@@ -6,7 +6,7 @@ silent unasked mutation. It runs whenever a skill gates on `doctor`'s structured
6
6
  `checks[]` (`roadmap/self-healing-resilience.md` §5.2); today that is `forge-5-loop`'s
7
7
  gates 1c/1d (`skills/forge-5-loop/SKILL.md`), which resolve the loop runner **before**
8
8
  touching it, `forge-init`'s install preflight (`skills/forge-init/SKILL.md`), where
9
- neither check is a stop, and `forge-guide --doctor` (`skills/forge-guide/SKILL.md`), the
9
+ no check is a stop, and `forge-guide --doctor` (`skills/forge-guide/SKILL.md`), the
10
10
  operator-facing repair surface, which gates nothing at all. All three are callers, not
11
11
  the procedure's scope: any skill that gates on `checks[]` follows it in full. Its seven
12
12
  ordered steps: **enumerate → cluster → consolidated prompts → record → apply → prove →
@@ -40,9 +40,11 @@ defined authoritatively in rauf's
40
40
  - **A machine-readable event stream** for live supervision (`loopRunner.eventStreamCommand`,
41
41
  rauf: `loop run … --ndjson`): one JSON event per line with a stable `type`
42
42
  vocabulary — `item_completed` / `item_blocked` / `needs_human` / `signal_parsed`
43
- / `loop_completed` / `loop_error` / `loop_cancelled` / `llm_stuck_warning` (a
44
- circuit-breaker halt surfaces as `loop_error`) — plus a
45
- derived-status JSON (`loopRunner.statusJsonCommand`, rauf: `status … --json`) and
43
+ / `loop_completed` / `loop_error` / `loop_cancelled` / `review_failed` /
44
+ `llm_stuck_warning` (a circuit-breaker halt surfaces as `loop_error`) — plus a
45
+ derived-status JSON (`loopRunner.statusJsonCommand`, rauf: `status … --json`;
46
+ forge-5-loop also reads its optional `loopState` / `lock` / `sleepUntil` /
47
+ `reviewPending` fields when present, and degrades to the counts when absent) and
46
48
  per-iteration telemetry with a `stuckWarning` flag (`loopRunner.watchCommand`,
47
49
  rauf: `status … --json` — the `loop watch` verb was removed in v0.5.0). `forge-5-loop` supervises the run through these,
48
50
  **not** by parsing the human log. `followCommand` / `logCommand` are
@@ -34,6 +34,16 @@ Runner review pass: {itemsCreated} fix item(s) created and implemented.
34
34
  Omit this line when no `review_completed` event was emitted (no review flag passed).
35
35
  The created items are already counted in the totals above.
36
36
 
37
+ **Review pending** (`reviewPending: true` — the review pass failed or was interrupted,
38
+ and the user chose **Stop here** at the Pending review offer in
39
+ `references/runner-contract.md`). Never render the all-done report for this run, even
40
+ when every item is `done`:
41
+ ```
42
+ Loop finished the backlog for {feature}, but its review pass did not complete.
43
+ Completed: {done}/{total}
44
+ Review pending: {reviewItemIds count} item(s) ({reason from the last `review_failed` event, if any})
45
+ ```
46
+
37
47
  **Some items need a human:**
38
48
  ```
39
49
  Loop completed for {feature}.
@@ -71,8 +81,11 @@ Loop completed for {feature}.
71
81
  Pending: {pending} items ({cause})
72
82
  Blocked: {blocked} items
73
83
  ```
74
- Render `{cause}` as "iteration limit reached" **only** when `iteration == maxIterations`
75
- AND `selectable > 0` — cite the `iteration`/`maxIterations` counters from
84
+ Render `{cause}` from the runner's terminal state first (see **Runner terminal states**
85
+ below): "stopped on request", "the run crashed (stale lock)", or "usage limit — resume
86
+ after {sleepUntil}". Otherwise render it as "iteration limit reached" **only** when
87
+ `loopState` is `ITERATIONS_COMPLETE` (the runner's own attestation, when reported) or
88
+ `iteration == maxIterations`, AND `selectable > 0` — cite the `iteration`/`maxIterations` counters from
76
89
  `{loopRunner.stateDir}/state.json` and `selectable` from `backlog-topology --items-stdin
77
90
  --json` run over the same authoritative item JSON as the counts above. Otherwise —
78
91
  `selectable == 0` with items still pending while `iteration < maxIterations` — the
@@ -93,6 +106,37 @@ the `backlog-topology` output (`selectable`, `blockingRoots`, `gatedCount`,
93
106
  `itemCount`). A cause any of those counters contradicts — e.g. "iteration limit
94
107
  reached" while `iteration < maxIterations` — is a reportable defect.
95
108
 
109
+ ## Runner terminal states (Step 4a)
110
+
111
+ `status --json` may also carry `loopState`, `lock`, `sleepUntil`, `reviewPending` and
112
+ `reviewItemIds`. All are optional: when a field is absent, skip its row and read the
113
+ counts as before. These mirror the rows of rauf's supervisor decision table
114
+ (`drive-rauf-loop`) that forge-5-loop decides the same way.
115
+
116
+ **Clean runner finish.** When `loopState` is reported, only `COMPLETE` or `IDLE` is a
117
+ clean terminal success. Every other value — `ERROR`, `PAUSED` (on request or with a
118
+ stale lock), `ITERATIONS_COMPLETE`, `PAUSED_USAGE_LIMIT`, `WEEKLY_LIMIT`,
119
+ `SLEEPING_LIMIT`, `PAUSED_HUMAN`, a still-live `RUNNING`/`REVIEWING`, or anything
120
+ unrecognized — means the runner did **not** finish cleanly, and the run is **never**
121
+ `complete`, even when `done == total > 0` (e.g. a process killed after its last commit
122
+ but before writing its final state reads `PAUSED` + stale lock with every item done).
123
+ The ladder's rung 2 (runner not finished) catches it, above the needs-human / blocked / deferred rungs. When `loopState` is absent (an older or
124
+ non-rauf runner), this gate does not apply and the counts decide, as before.
125
+
126
+ | Runner state | What it means here |
127
+ |---|---|
128
+ | `backlogSummary.total == 0` (any `loopState`) | Empty **or unreadable** backlog — a read failure reports all-zero counts. Never `complete`: run the **validate command** and treat it as an **operational failure** (below). |
129
+ | `reviewPending: true` | Not complete, whatever the counts: offer the resume (**Pending review**, `references/runner-contract.md`). If the user stops here, ladder rung 2 closes `partial --cause review-pending` — even with blocked, needs-human or deferred items, which are still reported alongside. |
130
+ | `ITERATIONS_COMPLETE` | Iteration budget spent with eligible work left — the "iteration limit reached" `partial` cause. The next loop run gets a fresh budget. |
131
+ | `PAUSED`, `lock.stale` not true | Stopped on request (Ctrl-C, `SIGTERM`, a stop command). Report "stopped on request"; do not offer to relaunch unless the user asks. |
132
+ | `PAUSED`, `lock.stale: true` | The run died mid-iteration and left its lock. rauf's `{bin} resume . --backlog {backlogDir}` clears the stale lock and continues. Never reach for `--force` or `reset` first. |
133
+ | `PAUSED_USAGE_LIMIT` / `WEEKLY_LIMIT` | Halted on a usage limit: "resume after {sleepUntil}". Not a failure. |
134
+ | `ERROR` | Crash or circuit-breaker halt (`loop_error`). Surface the error alongside the count reports; never `complete`. With work left, the next loop run re-runs it; with none, re-entry offers rauf's resume (**Unfinished runner**, `references/runner-contract.md`). |
135
+ | `COMPLETE` with `done < total` | No eligible work left, but items are unfinished (blocked, needs-human, deferred, or pending behind a blocked dependency). The ladder's non-complete rungs apply; never reset the backlog. |
136
+
137
+ **Exit codes are not the outcome.** rauf exits 1 both for a crash and for a failed review
138
+ pass, and 0 for a requested stop or a spent budget. Read the fields above, never `$?`.
139
+
96
140
  ## Selecting the one `LoopOutcome` (Step 7)
97
141
 
98
142
  After Step 5's `state-complete`, select exactly **one** `LoopOutcome` from Step 4a's
@@ -106,26 +150,43 @@ authoritative final counts. Walk this ladder in order and stop at the first matc
106
150
  stop the recovery just cleared is not re-reported as still needing a human. (Step
107
151
  4c runs the procedure on every close, so an empty affected set is the common case —
108
152
  it never selects `resolved`; fall through.)
109
- 2. **`needs-human`** — otherwise, `needsHuman > 0`. This wins even when blocked
153
+ 2. **`partial` (runner not finished)** — otherwise, `reviewPending` is true, **or**
154
+ `loopState` is reported and is not a clean finish (**Clean runner finish**, above).
155
+ Pass `--cause review-pending` when `reviewPending` is true. Otherwise pass
156
+ `--cause runner-stopped`, except for `ITERATIONS_COMPLETE`, which is the plain
157
+ iteration-limit `partial` (no `--cause`). This rung fires **whatever the counts
158
+ say** — including `done == total`, and **above** the needs-human / blocked / deferred
159
+ rungs, following rauf's supervisor table: a crashed, stopped, limit-halted or
160
+ budget-spent runner, or an unfinished review, is recovered or resumed first. It does
161
+ not hide the backlog: the Step 4b needs-human / blocked / deferred reports still
162
+ render alongside it, so every set-aside item is surfaced in the same close (e.g.
163
+ **Stop here** on a pending review with blocked items closes `partial --cause
164
+ review-pending` and still lists the blocked items).
165
+ 3. **`needs-human`** — otherwise, `needsHuman > 0`. This wins even when blocked
110
166
  items also exist: a decision only a human can make outranks work that merely
111
167
  could not proceed.
112
- 3. **`blocked`** — otherwise, genuine `blocked > 0`.
113
- 4. **`deferred`** — otherwise, runner-deferred items exist (the "false blocks" the
168
+ 4. **`blocked`** — otherwise, genuine `blocked > 0`.
169
+ 5. **`deferred`** — otherwise, runner-deferred items exist (the "false blocks" the
114
170
  runner gave up on after retries).
115
- 5. **`partial`** — otherwise, `pending`/`in_progress` items remain because the
116
- iteration limit was reached.
117
- 6. **`complete`** — otherwise, and **only** when every item is `done`.
171
+ 6. **`partial`** — otherwise, `pending`/`in_progress` items remain (the report above
172
+ names the cause). Pass `--cause dependency-starvation` only on the starvation report.
173
+ 7. **`complete`** — otherwise, and **only** when `total > 0`, every item is `done`, no
174
+ review is pending, and the runner finished cleanly (or reports no `loopState`).
118
175
 
119
176
  This is a priority order, not a set. A run reporting both a needs-human and a blocked
120
- count renders both reports above and still exits `needs-human`.
177
+ count renders both reports above and still exits `needs-human`; a run whose runner did
178
+ not finish renders every applicable count report and still exits rung 2's `partial`.
121
179
 
122
180
  **The runner's process exit code is not the outcome.** A loop runner that exits 0 has
123
181
  reported only that its process finished; the final backlog state decides. A clean
124
- exit 0 that still leaves pending items is `partial`, never `complete` — and
125
- `complete` is legitimate only when the counts show every item `done`.
126
-
127
- **Retrying the non-complete outcomes.** `partial`, `deferred`, and `resolved` fence
128
- the loop resume; `blocked` and `needs-human` fence the navigator. Whichever you land on, the
182
+ exit 0 that still leaves pending items is `partial`, never `complete`; an exit 1 from a
183
+ failed review with every item `done` is `partial` (`--cause review-pending`), not an
184
+ error; and a crash or stale-lock stop with every item `done` is `partial` (`--cause
185
+ runner-stopped`). `complete` is legitimate only when the counts show every item `done`,
186
+ no review is pending, and the runner finished cleanly.
187
+
188
+ **Retrying the non-complete outcomes.** `partial` (every cause), `deferred`, and
189
+ `resolved` fence the loop resume; `blocked` and `needs-human` fence the navigator. Whichever you land on, the
129
190
  runner's own retry flags still apply to the next run — e.g. rauf's `--retry-blocked`
130
191
  picks the set-aside blocked and deferred items back up at Step 2d. Mention that as
131
192
  plain prose in the report if it helps; never as a second command block.
@@ -133,8 +194,9 @@ plain prose in the report if it helps; never as a second command block.
133
194
  ## Operational failure before the counts are known
134
195
 
135
196
  If the run cannot produce authoritative counts at all — the status/list command fails,
136
- its output does not parse, the state directory is gone, or the process died in a way
137
- that leaves the backlog unreadable — **do not pick an outcome and do not close the
197
+ its output does not parse, the state directory is gone, the summary reports
198
+ `total == 0` (an empty or unreadable backlog — show the validate command's output), or
199
+ the process died in a way that leaves the backlog unreadable — **do not pick an outcome and do not close the
138
200
  stage.** There is nothing to select from, and guessing one would record a pipeline
139
201
  position that never happened.
140
202
 
@@ -152,12 +152,18 @@ maximum `timeout_ms` (1 hour), and a bounded timeout would silently stop watchin
152
152
  still-running loop.
153
153
 
154
154
  **Coverage-complete filter (silence is not success).** The filter MUST match every
155
- terminal and exception state, not just the happy path — otherwise a crash or hang
156
- looks identical to "still running." Monitor command (NDJSON path):
155
+ terminal, exception, and pause/limit event, not just the happy path — otherwise a crash,
156
+ hang, or hours-long usage sleep looks identical to "still running." The list below covers
157
+ every such type in rauf's event schema (terminal: `loop_completed` / `loop_error` /
158
+ `loop_cancelled` / `loop_paused`; exception: `item_blocked` / `needs_human` /
159
+ `review_failed` / `llm_stuck_warning`; pause/limit: `usage_limit_hit` /
160
+ `usage_limit_cleared` / `sleep_start` / `sleep_end`); only per-iteration narration
161
+ (`iteration_start`, `llm_*` activity, `item_selected`, …) is left out. Monitor command
162
+ (NDJSON path):
157
163
 
158
164
  ```
159
165
  tail -n +1 -F {backlogDir}/{loopRunner.stateDir}/events.ndjson 2>/dev/null \
160
- | jq -rc --unbuffered 'select(.type | test("item_completed|item_blocked|needs_human|signal_parsed|loop_completed|loop_error|loop_cancelled|llm_stuck_warning"))'
166
+ | jq -rc --unbuffered 'select(.type | test("item_completed|item_blocked|needs_human|signal_parsed|loop_completed|loop_error|loop_cancelled|loop_paused|review_failed|llm_stuck_warning|usage_limit_hit|usage_limit_cleared|sleep_start|sleep_end"))'
161
167
  ```
162
168
 
163
169
  > **Use `tail -F` (follow by name), not `-f` (follow by descriptor).** The runner
@@ -174,15 +180,20 @@ tail -n +1 -F {backlogDir}/{loopRunner.stateDir}/events.ndjson 2>/dev/null \
174
180
 
175
181
  ```
176
182
  tail -n +1 -F {backlogDir}/{loopRunner.stateDir}/{loopRunner.logFile} 2>/dev/null \
177
- | grep -E --line-buffered 'Item [^ ]+ (completed|blocked):|Item [^ ]+ needs human input|Loop completed|Loop error:|Circuit breaker:'
183
+ | grep -E --line-buffered 'Item [^ ]+ (completed|blocked):|Item [^ ]+ needs human input|Loop completed|Loop error:|Circuit breaker:|Loop cancelled|Review pass (cancelled|stopped)|Review returned unexpected signal|for review:|[Uu]sage limit'
178
184
  ```
179
185
 
180
186
  (Match `needs human input` **without** a trailing colon — the runner writes
181
- `needs human input (set aside):`.)
187
+ `needs human input (set aside):`. `Loop cancelled` also matches *"Loop cancelled during
188
+ review pass (review pending)"* and the between-iteration / mid-sleep cancels;
189
+ `[Uu]sage limit` matches the 5-hour sleep, weekly limit, detection and wake lines.)
182
190
 
183
191
  If the Monitor is ever auto-stopped for event volume, re-arm with a tighter filter
184
192
  (drop `item_completed`, keep the exception/terminal events).
185
193
 
194
+ An older runner that never emits a listed type (e.g. `review_failed`) simply never
195
+ matches it — the filter needs no version gate.
196
+
186
197
  ## React to events as they land (Step 3e)
187
198
 
188
199
  Each Monitor event arrives as a message. React per type — but keep the user signal
@@ -210,13 +221,101 @@ high and the noise low:
210
221
  - **`loop_error`** → a real failure (this is also what a circuit-breaker halt — too many
211
222
  consecutive infra failures — emits). Surface now and `PushNotification`. Offer
212
223
  inspection / `--force` / re-run as appropriate.
213
- - **Stall detection** → rauf emits an **`llm_stuck_warning`** event when an iteration
214
- stops making progress; the filter above includes it, so surface it live (a hang
215
- warning, not yet a failure) and offer `--force` if it persists. If you instead want to
216
- probe on quiet, run `{rendered watchCommand}` (or read
217
- `{backlogDir}/{loopRunner.stateDir}/iteration-status.json`) and key off its
218
- `stuckWarning` flag. Do **not** infer a stall from `state.json.updatedAt` alone — it is
219
- not a liveness proof.
224
+ - **`review_failed`** → the post-loop review pass failed or was stopped by a usage limit
225
+ (payload `reason`). Surface it now with a `PushNotification`. The run is **not**
226
+ complete: the review stays pending (`status --json` → `reviewPending: true`). A failed
227
+ review makes `loop run --review` exit **1**; a usage-limit stop is a resumable limit
228
+ stop, not that exit-1 case. A review **cancelled** mid-pass emits **no**
229
+ `review_failed` — only `loop_cancelled` (human log: *"Loop cancelled during review pass
230
+ (review pending)"*) with `reviewPending` still set — so never rely on this event alone.
231
+ Nothing to do live — Step 4a reads `reviewPending` and offers the resume (**Pending
232
+ review**, below).
233
+ - **Stall detection** → rauf emits an **`llm_stuck_warning`** event when an iteration's
234
+ output stream goes silent. Surface it live as a stall warning, not yet a failure. Read
235
+ its optional `currentTool` / `toolRunningMs` (absent on older runners — then report
236
+ the plain `silentMs` and treat it as a possible hang):
237
+ - `currentTool: null` → **the model itself went silent** with no tool in flight — a
238
+ likely hang. Offer `--force` / re-run if it persists.
239
+ - `currentTool` set → **a quiet tool call outlived the tool ceiling** — report it as
240
+ e.g. *"Bash running 31m"* (`toolRunningMs` rounded to minutes). This is usually a
241
+ slow verification gate, not a hung LLM: say so, and let it run unless it is clearly
242
+ wedged.
243
+
244
+ The thresholds are the runner's `.rauf.json` `options.stuckThresholdMs` (silence
245
+ before the warning, default 5 min) and `options.toolStuckThresholdMs` (how long a
246
+ quiet in-flight tool holds the warning off, default 30 min, measured from the tool's
247
+ start). A repo whose verification gate legitimately runs longer than 30 min should
248
+ raise `toolStuckThresholdMs` rather than learn to ignore the warning. If you instead
249
+ want to probe on quiet, run `{rendered watchCommand}` and key off `health.stuckWarning`.
250
+ Do **not** infer a stall from `state.json.updatedAt` alone — it is not a liveness
251
+ proof.
252
+ - **Usage-limit waits are not stalls.** `usage_limit_hit` (`limitType`, optional
253
+ `reason: "usage_api_disagreement"`) followed by `sleep_start` (`sleepUntil`, `reason`)
254
+ means the runner is sleeping until the limit resets (`SLEEPING_LIMIT`) and will resume
255
+ on its own: surface it once with its `sleepUntil`, and send a `PushNotification` when
256
+ the sleep is long (a 5-hour window). A `sleep_start` whose `reason` begins *"Usage-limit
257
+ banner unconfirmed"* is a 30 s / 60 s backoff (`status --json` still `RUNNING` with
258
+ `sleepUntil`) — narrate it at most briefly. `sleep_end` / `usage_limit_cleared` → the
259
+ loop is working again. A weekly or no-sleep limit halts the run instead (the process
260
+ exits; Step 4a reads `WEEKLY_LIMIT` / `PAUSED_USAGE_LIMIT`). None of these is a stall:
261
+ never offer `--force`.
262
+ - **`loop_paused`** (`reason: "needs_human"`, `itemId`) → the run halted on a needs-human
263
+ item (only under rauf's opt-in `--pause-on-needs-human`; forge does not pass it by
264
+ default). Unlike the default set-aside mode, the loop **is** now stopped: surface it
265
+ with a `PushNotification`; the process exits and Step 4c's recovery pass handles the
266
+ answer.
267
+
268
+ **Pending review (Steps 2a / 4a, rauf).** A rauf `--review` run whose review pass failed, was cancelled, or was stopped by a usage
269
+ limit leaves `status --json` with `reviewPending: true` and `reviewItemIds` (the review's
270
+ exact scope). The loop state can read `COMPLETE`/`IDLE` with every item `done`, or
271
+ `PAUSED`/`PAUSED_USAGE_LIMIT` — either way the run is **not complete**, and the process
272
+ exit code (1 for a failed review) does not decide it. Both fields are optional: a runner
273
+ that never reports them never takes this path.
274
+
275
+ rauf's `resume` re-runs exactly that review (`rauf loop review --items <reviewItemIds>`),
276
+ not a fresh loop. So when Step 4a or Step 2a sees `reviewPending: true`:
277
+
278
+ 1. Report it: *"The review pass for {feature} did not finish ({reason from the last
279
+ `review_failed` event, if any}); it covers {reviewItemIds}."*
280
+ 2. Via host's question mechanism, offer **Resume the review now (recommended)** · **Stop here**. On a
281
+ usage-limit stop (`PAUSED_USAGE_LIMIT` / `WEEKLY_LIMIT`), say the resume only helps
282
+ once the limit resets (`sleepUntil`). A stop the user requested (`PAUSED`, lock
283
+ released) is theirs: offer, never auto-resume.
284
+ 3. **Resume:** launch `{bin} resume . --backlog {backlogDir}` backgrounded, exactly as a
285
+ run command (Step 3b launch guards, 3d Monitor, 3f completion), then return to Step 4a.
286
+ A successful review may file fix items (`review_completed.itemsCreated`); those are
287
+ ordinary pending work for the next loop run. rauf reviews **before** relaunching: when
288
+ pending items also remain (the review ran after the iteration budget ran out, or
289
+ blocked items strand their dependents), `resume` re-runs only the review and the
290
+ remaining items wait for the next loop run — say so, never launch a fresh `loop run`
291
+ first (it overwrites the state and drops the pending review).
292
+ 4. **Stop here:** Step 7 closes `partial` with `--cause review-pending` (see
293
+ `references/result-reporting.md`) — a resume route whose Step 2a re-offers this.
294
+
295
+ At rung 3 (no question mechanism), do not launch: print the rendered resume command and
296
+ close as in 4.
297
+
298
+ **Unfinished runner (Step 2a re-entry, rauf).** When Step 2a finds no pending or
299
+ in_progress items and no pending review, but `status --json` reports a `loopState` that
300
+ is not a clean finish (**Clean runner finish** in `references/result-reporting.md` —
301
+ e.g. `ERROR`, `PAUSED` with `lock.stale`, `ITERATIONS_COMPLETE`, a usage halt), the
302
+ prior run closed as `partial` with `--cause runner-stopped` (or the plain iteration-limit
303
+ `partial`), and "Nothing to run" would strand it. Instead:
304
+
305
+ 1. Report the state: *"The last run for {feature} did not finish cleanly ({loopState}
306
+ {— stale lock | — resets {sleepUntil}})."*
307
+ 2. Via host's question mechanism, offer **Resume the runner (recommended)** · **Stop here**. For a
308
+ usage halt, the resume helps only once the limit resets; for `PAUSED` on request
309
+ (lock released), the stop was the user's — offer, never auto-resume.
310
+ 3. **Resume:** launch `{bin} resume . --backlog {backlogDir}` backgrounded exactly as in
311
+ **Pending review** step 3 (it clears a stale lock and finishes the run's bookkeeping,
312
+ including any post-loop review), then continue at Step 4a. rauf's recovery for
313
+ `ERROR` is `resume` or `reset` + re-run — never reach for `reset` or `--force` first.
314
+ 4. **Stop here**, or rung 3 (print the rendered command, do not launch): STOP without
315
+ touching the stage — it is already recorded `in-progress`.
316
+
317
+ When `loopState` is absent, this never fires: an older or non-rauf runner keeps the
318
+ plain "Nothing to run" stop.
220
319
 
221
320
  ## Inform-user output template (Step 3c)
222
321
 
@@ -47,7 +47,7 @@ an epic member. Only the flags below are stage-specific; pass no others.
47
47
  |---|---|---|
48
48
  | `forge-0-epic` | `forge-0-epic` | `--next-feature "{member}"` when a concrete member exists |
49
49
  | `forge-1-prd` … `forge-4-backlog` | that stage's own id | none beyond identity/capability |
50
- | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation` with `--outcome partial` |
50
+ | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation`, `review-pending` or `runner-stopped` with `--outcome partial` |
51
51
  | `forge-6-docs` | `forge-6-docs` | `--outcome` — `complete`, `blocked`, or `skipped` (deliberate docs skip, persisted via `state-skip` before the exit; routes like `complete` with honest wording) |
52
52
  | direct `forge-verify` | `forge-verify` | `--owner direct`, `--outcome` (`passed`, `findings`, `skipped`, `failed`), and served-stage metadata |
53
53
  | nested `forge-verify` | `forge-verify` | `--owner nested`, plus the same outcome and served-stage metadata |
@@ -47,7 +47,7 @@ an epic member. Only the flags below are stage-specific; pass no others.
47
47
  |---|---|---|
48
48
  | `forge-0-epic` | `forge-0-epic` | `--next-feature "{member}"` when a concrete member exists |
49
49
  | `forge-1-prd` … `forge-4-backlog` | that stage's own id | none beyond identity/capability |
50
- | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation` with `--outcome partial` |
50
+ | `forge-5-loop` | `forge-5-loop` | `--outcome` — one of `complete`, `partial`, `blocked`, `needs-human`, `deferred`, `resolved`; optional `--cause dependency-starvation`, `review-pending` or `runner-stopped` with `--outcome partial` |
51
51
  | `forge-6-docs` | `forge-6-docs` | `--outcome` — `complete`, `blocked`, or `skipped` (deliberate docs skip, persisted via `state-skip` before the exit; routes like `complete` with honest wording) |
52
52
  | direct `forge-verify` | `forge-verify` | `--owner direct`, `--outcome` (`passed`, `findings`, `skipped`, `failed`), and served-stage metadata |
53
53
  | nested `forge-verify` | `forge-verify` | `--owner nested`, plus the same outcome and served-stage metadata |