@garygentry/feature-forge 0.2.8 → 0.2.10

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 (92) hide show
  1. package/adapters/claude/.feature-forge-bundle.json +1 -1
  2. package/adapters/claude/references/pipeline-state-schema.json +77 -1
  3. package/adapters/claude/references/stage-exit-protocol.md +50 -2
  4. package/adapters/claude/references/templates/specs-hygiene/AGENTS.md +9 -0
  5. package/adapters/claude/references/templates/specs-hygiene/CLAUDE.md +9 -0
  6. package/adapters/claude/scripts/epic-manifest.py +26 -0
  7. package/adapters/claude/scripts/forge-session.py +130 -9
  8. package/adapters/claude/skills/forge/SKILL.md +3 -1
  9. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +42 -0
  10. package/adapters/claude/skills/forge-1-prd/SKILL.md +2 -0
  11. package/adapters/claude/skills/forge-2-tech/SKILL.md +2 -0
  12. package/adapters/claude/skills/forge-5-loop/SKILL.md +8 -6
  13. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +5 -1
  14. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +22 -2
  15. package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
  16. package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
  17. package/adapters/claude/skills/forge-verify/SKILL.md +2 -2
  18. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +12 -0
  19. package/adapters/codex/.feature-forge-bundle.json +1 -1
  20. package/adapters/codex/references/pipeline-state-schema.json +77 -1
  21. package/adapters/codex/references/stage-exit-protocol.md +50 -2
  22. package/adapters/codex/references/templates/specs-hygiene/AGENTS.md +9 -0
  23. package/adapters/codex/references/templates/specs-hygiene/CLAUDE.md +9 -0
  24. package/adapters/codex/scripts/epic-manifest.py +26 -0
  25. package/adapters/codex/scripts/forge-session.py +130 -9
  26. package/adapters/codex/skills/forge/SKILL.md +3 -1
  27. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +42 -0
  28. package/adapters/codex/skills/forge-1-prd/SKILL.md +2 -0
  29. package/adapters/codex/skills/forge-2-tech/SKILL.md +2 -0
  30. package/adapters/codex/skills/forge-5-loop/SKILL.md +8 -6
  31. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +5 -1
  32. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +22 -2
  33. package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
  34. package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
  35. package/adapters/codex/skills/forge-verify/SKILL.md +2 -2
  36. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +12 -0
  37. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  38. package/adapters/copilot/references/pipeline-state-schema.json +77 -1
  39. package/adapters/copilot/references/stage-exit-protocol.md +50 -2
  40. package/adapters/copilot/references/templates/specs-hygiene/AGENTS.md +9 -0
  41. package/adapters/copilot/references/templates/specs-hygiene/CLAUDE.md +9 -0
  42. package/adapters/copilot/scripts/epic-manifest.py +26 -0
  43. package/adapters/copilot/scripts/forge-session.py +130 -9
  44. package/adapters/copilot/skills/forge/forge.md +3 -1
  45. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +42 -0
  46. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +2 -0
  47. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +2 -0
  48. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +8 -6
  49. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +5 -1
  50. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +22 -2
  51. package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
  52. package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
  53. package/adapters/copilot/skills/forge-verify/forge-verify.md +2 -2
  54. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +12 -0
  55. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  56. package/adapters/cursor/references/pipeline-state-schema.json +77 -1
  57. package/adapters/cursor/references/stage-exit-protocol.md +50 -2
  58. package/adapters/cursor/references/templates/specs-hygiene/AGENTS.md +9 -0
  59. package/adapters/cursor/references/templates/specs-hygiene/CLAUDE.md +9 -0
  60. package/adapters/cursor/scripts/epic-manifest.py +26 -0
  61. package/adapters/cursor/scripts/forge-session.py +130 -9
  62. package/adapters/cursor/skills/forge/forge.mdc +3 -1
  63. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +42 -0
  64. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +2 -0
  65. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +2 -0
  66. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +8 -6
  67. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +5 -1
  68. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +22 -2
  69. package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
  70. package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
  71. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +2 -2
  72. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +12 -0
  73. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  74. package/adapters/gemini/gemini-extension.json +1 -1
  75. package/adapters/gemini/references/pipeline-state-schema.json +77 -1
  76. package/adapters/gemini/references/stage-exit-protocol.md +50 -2
  77. package/adapters/gemini/references/templates/specs-hygiene/AGENTS.md +9 -0
  78. package/adapters/gemini/references/templates/specs-hygiene/CLAUDE.md +9 -0
  79. package/adapters/gemini/scripts/epic-manifest.py +26 -0
  80. package/adapters/gemini/scripts/forge-session.py +130 -9
  81. package/adapters/gemini/skills/forge/forge.md +3 -1
  82. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +42 -0
  83. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +2 -0
  84. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +2 -0
  85. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +8 -6
  86. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +5 -1
  87. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +22 -2
  88. package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
  89. package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
  90. package/adapters/gemini/skills/forge-verify/forge-verify.md +2 -2
  91. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +12 -0
  92. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feature-forge",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "agent": "claude",
5
5
  "generatedBy": "python3 scripts/build-adapters.py"
6
6
  }
@@ -33,12 +33,88 @@
33
33
  "currentStage": {
34
34
  "type": "string",
35
35
  "enum": ["forge-1-prd", "forge-2-tech", "forge-3-specs", "forge-4-backlog", "forge-5-loop", "forge-6-docs", "complete", "forge-verify-prd", "forge-verify-tech", "forge-verify-specs", "forge-verify-backlog", "forge-verify-impl", "forge-0-epic", "forge-verify-epic"],
36
- "description": "The stage currently in progress or next to start"
36
+ "description": "Where the pipeline IS: the most recently started stage — its `stages[<currentStage>].status` is `in-progress` while that stage is being authored, then `complete` once its artifacts are committed. A stage skill sets this to its own id when it starts. This is deliberately NOT 'the next stage to run': the next stage is DERIVED, never stored — it is the first production stage whose `stages[].status` is not `complete` (see `next_stage()` in forge-session.py, surfaced as the navigator/doctor `nextStage`). Consumers that need 'what runs next' compute it from `stages[].status`, not from this field. `complete` here means the whole pipeline is done. (Legacy/absent value: tools fall back to the derived next stage for display only — `build_rows` in forge-session.py.)"
37
37
  },
38
38
  "notes": {
39
39
  "type": "string",
40
40
  "description": "Free-form notes persisted between sessions (user can add context before stepping away)"
41
41
  },
42
+ "epicChangeRequests": {
43
+ "type": "array",
44
+ "description": "Epic-level change requests raised by a member stage (forge-1-prd/forge-2-tech) when the epic DECOMPOSITION itself must change — distinct from the same-feature `notes` parking lot. Read by forge-0-epic edit mode (which applies them) and by forge-session stage-exit (which routes the exit on `blocksCurrent`). Absent/empty for standalone features. Additive/optional: legacy states without it validate unchanged.",
45
+ "items": {
46
+ "type": "object",
47
+ "required": ["kind", "target", "rationale", "blocksCurrent", "raisedBy", "raisedAt", "status"],
48
+ "additionalProperties": false,
49
+ "properties": {
50
+ "kind": {
51
+ "type": "string",
52
+ "enum": ["add-feature", "redep", "move-boundary", "split"],
53
+ "description": "The decomposition change. add-feature/redep map 1:1 onto edit-mode mutators; move-boundary/split are composite (applied guided-manual in v1)."
54
+ },
55
+ "target": {
56
+ "type": "string",
57
+ "description": "The sibling feature to add, or the feature/boundary affected."
58
+ },
59
+ "rationale": {
60
+ "type": "string",
61
+ "description": "Why the epic must change; seeds the edit-mode charter/prompt when applied."
62
+ },
63
+ "blocksCurrent": {
64
+ "type": "boolean",
65
+ "description": "true → the current feature's next stage would build on a soon-to-change decomposition (pause-now: reconcile before proceeding). false → a peer/downstream change (finish-then-edit). Drives stage-exit routing."
66
+ },
67
+ "raisedBy": {
68
+ "type": "string",
69
+ "enum": ["forge-1-prd", "forge-2-tech"],
70
+ "description": "The stage that detected the epic-level concern."
71
+ },
72
+ "raisedAt": { "type": "string", "format": "date-time" },
73
+ "status": {
74
+ "type": "string",
75
+ "enum": ["open", "applied", "dismissed"],
76
+ "default": "open",
77
+ "description": "Lifecycle: open → applied|dismissed. Only forge-0-epic edit mode flips open→applied/dismissed; recording stages only create open entries; navigator/verify only read."
78
+ }
79
+ }
80
+ }
81
+ },
82
+ "deferredDecisions": {
83
+ "type": "array",
84
+ "description": "Same-feature decisions deliberately postponed to a LATER stage of THIS feature — a structured alternative to burying them in the free-text `notes` string. Distinct from `notes` (unstructured scratch) and from `epicChangeRequests[]` (the epic DECOMPOSITION must change). Use this when a stage would otherwise be tempted to solicit a decision that properly belongs to a downstream stage (e.g. forge-1-prd deferring the concrete cache backend to forge-2-tech): record it here instead of asking now (see the deferred-decisions rule in `references/stage-exit-protocol.md`), and the target stage addresses it. Additive/optional: legacy states without it validate unchanged.",
85
+ "items": {
86
+ "type": "object",
87
+ "required": ["question", "raisedBy", "raisedAt", "status"],
88
+ "additionalProperties": false,
89
+ "properties": {
90
+ "question": {
91
+ "type": "string",
92
+ "description": "The decision being deferred, phrased as a concrete question for the target stage to answer."
93
+ },
94
+ "rationale": {
95
+ "type": "string",
96
+ "description": "Why it is deferred rather than decided now (e.g. depends on information the target stage produces)."
97
+ },
98
+ "targetStage": {
99
+ "type": "string",
100
+ "enum": ["forge-1-prd", "forge-2-tech", "forge-3-specs", "forge-4-backlog", "forge-5-loop", "forge-6-docs"],
101
+ "description": "The stage that should resolve this decision. Omit when unknown/any-later-stage."
102
+ },
103
+ "raisedBy": {
104
+ "type": "string",
105
+ "enum": ["forge-1-prd", "forge-2-tech", "forge-3-specs", "forge-4-backlog"],
106
+ "description": "The stage that deferred the decision."
107
+ },
108
+ "raisedAt": { "type": "string", "format": "date-time" },
109
+ "status": {
110
+ "type": "string",
111
+ "enum": ["open", "addressed", "dismissed"],
112
+ "default": "open",
113
+ "description": "Lifecycle: open → addressed|dismissed. The target stage flips open→addressed when it resolves the decision (or dismissed if it no longer applies); recording stages only create open entries."
114
+ }
115
+ }
116
+ }
117
+ },
42
118
  "stages": {
43
119
  "type": "object",
44
120
  "properties": {
@@ -151,6 +151,46 @@ outstanding, so the navigator catch-up can fire later.
151
151
  Verification is already resolved (fresh or explicitly skipped) or the in-stage run
152
152
  above covers it. Say so in one line and continue to the NEXT-STEPS block.
153
153
 
154
+ ### `epicReconcile` (epic backflow — present only when there are open requests)
155
+
156
+ Emitted only when the exiting member carries `open` `epicChangeRequests` (recorded by
157
+ `forge-1-prd`/`forge-2-tech` when the epic *decomposition* itself must change — see
158
+ `references/pipeline-state-schema.json`). Absent on the common path and for standalone
159
+ features. The script has already folded the routing into the NEXT-STEPS block, so this
160
+ directive is informational — you do **not** re-derive the wording:
161
+
162
+ - `required: true` (at least one `blocksCurrent: true` request) — the NEXT-STEPS block's
163
+ fenced **primary** command is the epic reconcile command
164
+ (`/feature-forge:forge-0-epic {epic}`), and the normal next stage is demoted to a
165
+ follow-up line ("After reconciling, continue with …"). This is *reconcile-before-specs*:
166
+ proceeding would author artifacts against a decomposition that is about to change. It is
167
+ strongest when exiting `forge-2-tech` (next is `forge-3-specs`, the point of no cheap
168
+ return).
169
+ - `reminder: true` (only `blocksCurrent: false` requests) — normal next-stage routing is
170
+ unchanged; the block appends a non-blocking reminder line ("You also flagged N epic
171
+ change(s) to reconcile when convenient …"). This is *finish-then-edit*.
172
+
173
+ Either way the added lines are host-neutral (no literal `/clear`) and sit **above** the
174
+ sentinel; just print the NEXT-STEPS block verbatim as always.
175
+
176
+ ### Deferred decisions — do not solicit next-stage decisions at this exit
177
+
178
+ Each stage owns its own decisions. At a stage exit, do **not** pull a *later* stage's
179
+ decision forward — do not ask the user (or decide unilaterally) something that properly
180
+ belongs to the next stage's interview (e.g. at `forge-1-prd` exit, don't settle the
181
+ concrete cache backend that `forge-2-tech` will design). Soliciting it here guesses ahead
182
+ of the stage that owns the context, and the answer has nowhere durable to live.
183
+
184
+ Instead, when you notice a decision that belongs downstream, **record it structurally** as
185
+ a `deferredDecisions[]` entry on this feature's `.pipeline-state.json` (schema in
186
+ `references/pipeline-state-schema.json`; same direct-edit path as `notes` /
187
+ `epicChangeRequests[]`): `question` (phrased for the target stage), optional `rationale`
188
+ and `targetStage`, `raisedBy` (this stage), `raisedAt` (ISO-8601 UTC), `status: "open"`.
189
+ This keeps the exit focused on *this* stage's next-step routing while carrying the open
190
+ question forward for the owning stage to resolve (it flips `status` to `addressed` when it
191
+ does). Prefer a `deferredDecisions[]` entry over stuffing the same thing into the free-text
192
+ `notes` string. This is a recording affordance, not a gate: never block the exit on it.
193
+
154
194
  ### The NEXT-STEPS block (always last)
155
195
 
156
196
  Print the script's NEXT-STEPS block **verbatim as your absolute last output**. Nothing
@@ -181,7 +221,11 @@ Slots: `{stage}` (a lowercase noun phrase), `{verify-command}`, `{next-command}`
181
221
 
182
222
  **Host / clean-room fallback (not a user-selectable option):** if the question mechanism, the `Agent` tool, or the `forge-verifier` subagent is unavailable, do **not** run clean-room — degrade to printing `{verify-command}` for the user to run inline/manually (mirroring `autoInvokeNextStage`), and offer the auto-verify enable as plain text only if a config write is possible.
183
223
  2. **Then `/clear`.** Recommended **unconditionally** at this boundary for a clean start — independent of how full the context window is. Every artifact is on disk, so the work survives the clear. **I can't `/clear` for you — you have to run it yourself.**
184
- 3. **Then run `{next-command}`** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk.
224
+ 3. **Then run the next command** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk:
225
+
226
+ ```
227
+ {next-command}
228
+ ```
185
229
  <!-- END: standard-exit-block -->
186
230
 
187
231
  ---
@@ -206,5 +250,9 @@ by the loop itself, so this block defers rather than re-presenting a gate.
206
250
 
207
251
  1. **Verify is already offered above.** Impl-verify is offered interactively right after this report (Step 5b for a standalone feature, Step 6.1 for an epic member) — run it there rather than as a second gate. It runs clean-room, so it needs no fresh session.
208
252
  2. **Clearing is optional here — warm is fine.** `forge-6-docs` benefits from the still-warm context of what the loop actually did, so continuing in this same session is the easy default. A cold start also works — every artifact is on disk — but there is no need to force it.
209
- 3. **Then run `{next-command}`** — in this warm session, or a fresh one if you prefer.
253
+ 3. **Then run the next command** — in this warm session, or a fresh one if you prefer:
254
+
255
+ ```
256
+ {next-command}
257
+ ```
210
258
  <!-- END: warm-exit-block -->
@@ -20,4 +20,13 @@ traceability matrices, and per-feature `backlog.json` files).
20
20
  reference files under `specs/`, which may be archived or deleted after a feature
21
21
  ships.
22
22
 
23
+ ## Tooling feedback
24
+
25
+ Friction with the feature-forge pipeline or the rauf loop — anything confusing, buggy, or
26
+ missing? Capture it while fresh and file it (feature-forge →
27
+ https://github.com/garygentry/feature-forge/issues, rauf →
28
+ https://github.com/garygentry/rauf/issues). See the project-root `AGENTS.md` "Tooling
29
+ feedback" section for the full flow. In an autonomous rauf iteration, note it in
30
+ `progress.md` instead of opening an issue mid-loop.
31
+
23
32
  This file was generated by feature-forge. Edit or remove it to suit your project.
@@ -19,4 +19,13 @@ traceability matrices, and per-feature `backlog.json` files).
19
19
  skills/agents, configs, docs) should be self-contained and should **not** reference
20
20
  files under `specs/`, which may be archived or deleted after a feature ships.
21
21
 
22
+ ## Tooling feedback
23
+
24
+ Friction with the feature-forge pipeline or the rauf loop — anything confusing, buggy, or
25
+ missing? Help me capture it while fresh and file it (feature-forge →
26
+ https://github.com/garygentry/feature-forge/issues, rauf →
27
+ https://github.com/garygentry/rauf/issues). See the project-root `CLAUDE.md` "Tooling
28
+ feedback" section for the full flow. In an autonomous rauf iteration, note it in
29
+ `progress.md` instead of opening an issue mid-loop.
30
+
22
31
  This file was generated by feature-forge. Edit or remove it to suit your project.
@@ -99,6 +99,13 @@ class FeatureStatus(TypedDict):
99
99
  blocked: True if any entry in unmetDeps is non-empty.
100
100
  unmetDeps: Names of this feature's direct dependencies that are not yet
101
101
  complete-for-orchestration (00 §7). Empty when actionable or complete.
102
+ openEpicChangeRequests: Count of this member's ``epicChangeRequests``
103
+ entries with ``status == "open"`` — epic-level change requests raised
104
+ by a member stage that forge-0-epic edit mode has not yet reconciled.
105
+ 0 for standalone features or members with no pending requests.
106
+ blockingEpicChangeRequests: The subset of ``openEpicChangeRequests`` with
107
+ ``blocksCurrent == true`` (pause-now, reconcile-before-specs). Always
108
+ ``<= openEpicChangeRequests``.
102
109
  """
103
110
 
104
111
  name: str
@@ -106,6 +113,8 @@ class FeatureStatus(TypedDict):
106
113
  status: DerivedStatus
107
114
  blocked: bool
108
115
  unmetDeps: list[str]
116
+ openEpicChangeRequests: int
117
+ blockingEpicChangeRequests: int
109
118
 
110
119
 
111
120
  class Rollup(TypedDict):
@@ -836,12 +845,26 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
836
845
  )
837
846
  derived = "in-progress" if started else "not-started"
838
847
 
848
+ # Epic-backflow surfacing (Phase 2): count open epicChangeRequests from the
849
+ # same state dict. A missing/torn state, a non-list value, or non-dict items
850
+ # count as 0 — a malformed request must never crash the dashboard, mirroring
851
+ # the torn-state -> not-started tolerance above.
852
+ requests = state.get("epicChangeRequests", [])
853
+ open_reqs = [
854
+ r for r in requests
855
+ if isinstance(r, dict) and r.get("status") == "open"
856
+ ] if isinstance(requests, list) else []
857
+ open_count = len(open_reqs)
858
+ blocking_count = sum(1 for r in open_reqs if r.get("blocksCurrent") is True)
859
+
839
860
  return {
840
861
  "name": name,
841
862
  "stage": stage,
842
863
  "status": derived,
843
864
  "blocked": False,
844
865
  "unmetDeps": [],
866
+ "openEpicChangeRequests": open_count,
867
+ "blockingEpicChangeRequests": blocking_count,
845
868
  }
846
869
 
847
870
 
@@ -1212,6 +1235,9 @@ def _print_status_table(status: RenderStatus) -> None:
1212
1235
  line = f" - {row['name']}: {row['status']} (stage {row['stage']})"
1213
1236
  if row["blocked"]:
1214
1237
  line += f" — blocked on {', '.join(row['unmetDeps'])}"
1238
+ if row["openEpicChangeRequests"]:
1239
+ marker = "⚠️ BLOCKING" if row["blockingEpicChangeRequests"] else "⚠️"
1240
+ line += f" — {marker} {row['openEpicChangeRequests']} pending epic change(s)"
1215
1241
  print(line)
1216
1242
  if status["actionable"]:
1217
1243
  print(f"Actionable: {', '.join(status['actionable'])}")
@@ -66,6 +66,7 @@ from __future__ import annotations
66
66
 
67
67
  import argparse
68
68
  import json
69
+ import os
69
70
  import subprocess
70
71
  import sys
71
72
  from datetime import datetime, timezone
@@ -210,6 +211,12 @@ def next_stage(state: dict) -> str | None:
210
211
  status is not ``complete`` (a missing/pending/in-progress/stale stage all
211
212
  count as "not done"). Returns ``None`` when every production stage is
212
213
  complete (nothing left to run).
214
+
215
+ This is the derived "what runs next" value — the single source of truth for
216
+ the next stage. It is intentionally distinct from the stored
217
+ ``currentStage`` field ("where the pipeline IS"; see the schema): the next
218
+ stage is computed from ``stages[].status`` here, never read from
219
+ ``currentStage``.
213
220
  """
214
221
  for stage in PRODUCTION_STAGES:
215
222
  if _stage_status(state, stage) != _DONE_STATUS:
@@ -348,6 +355,9 @@ def build_rows(specs_dir: Path, config: dict | None = None) -> list[FeatureRow]:
348
355
  rows.append({
349
356
  "name": name,
350
357
  "epic": epic,
358
+ # currentStage = "where the pipeline IS" (the recorded field). When a
359
+ # legacy/absent state omits it, fall back to the DERIVED next stage
360
+ # for display only — never conflate the two elsewhere (schema O1).
351
361
  "currentStage": state.get("currentStage") or (nxt or "complete"),
352
362
  "branch": branch if isinstance(branch, str) else None,
353
363
  "updatedAt": updated if isinstance(updated, str) else None,
@@ -710,6 +720,27 @@ def doctor_report(specs_dir: Path, config_path: Path) -> dict:
710
720
  "counts": _counts(specs_dir),
711
721
  "features": features,
712
722
  "invalidAutoVerifyKeys": invalid_auto_verify_keys(config),
723
+ "rootSandbox": _root_sandbox_status(),
724
+ }
725
+
726
+
727
+ def _root_sandbox_status() -> dict:
728
+ """Report the root/sandbox launch condition for forge-5-loop (issue #99).
729
+
730
+ On a hosted remote (e.g. Claude.ai) the loop runs as root, where rauf's
731
+ ``claude --dangerously-skip-permissions`` is refused unless ``IS_SANDBOX``
732
+ is set. forge-5-loop exports ``IS_SANDBOX=${IS_SANDBOX:-1}`` at launch when
733
+ root; this surfaces the same condition as a diagnosable check. ``geteuid``
734
+ is absent on Windows — treat that as non-root.
735
+ """
736
+ geteuid = getattr(os, "geteuid", None)
737
+ is_root = geteuid() == 0 if geteuid is not None else False
738
+ is_sandbox_set = os.environ.get("IS_SANDBOX") not in (None, "")
739
+ return {
740
+ "isRoot": is_root,
741
+ "isSandboxSet": is_sandbox_set,
742
+ # True only when the loop would need to supply the default at launch.
743
+ "loopWillSetSandbox": is_root and not is_sandbox_set,
713
744
  }
714
745
 
715
746
 
@@ -756,6 +787,16 @@ def _print_doctor(report: dict) -> None:
756
787
  invalid = report.get("invalidAutoVerifyKeys") or []
757
788
  if invalid:
758
789
  print(" ! invalid autoVerifyStages keys (ignored): " + ", ".join(invalid))
790
+ rs = report.get("rootSandbox") or {}
791
+ if rs.get("isRoot"):
792
+ if rs.get("isSandboxSet"):
793
+ print("root/sandbox: running as root; IS_SANDBOX already set — loop launch OK")
794
+ else:
795
+ print(
796
+ "root/sandbox: running as root; IS_SANDBOX not set — forge-5-loop will "
797
+ "export IS_SANDBOX=1 at launch so rauf's "
798
+ "--dangerously-skip-permissions is not refused"
799
+ )
759
800
 
760
801
 
761
802
  # --------------------------------------------------------------------------- #
@@ -1243,13 +1284,24 @@ def _resolve_feature_dir(specs_dir: Path, feature: str, epic: str | None) -> Pat
1243
1284
  return flat
1244
1285
 
1245
1286
 
1246
- def _next_steps_block(next_command: str, host: str) -> str:
1287
+ def _next_steps_block(
1288
+ next_command: str, host: str, reconcile: dict | None = None
1289
+ ) -> str:
1247
1290
  """Render the sentinel-terminated NEXT-STEPS block for the given host.
1248
1291
 
1249
1292
  The Claude wording uses the literal ``/clear`` slash-command; the generic
1250
1293
  wording is host-neutral (matching the adapter build's host-term table, so
1251
1294
  a non-Claude bundle invoking ``--host generic`` never instructs a fake
1252
1295
  slash-command).
1296
+
1297
+ ``reconcile`` carries the epic-backflow routing (§Epic backflow in
1298
+ ``references/stage-exit-protocol.md``). When it marks a **blocking** request
1299
+ (``required: true``), the fenced primary command becomes the epic reconcile
1300
+ command and the normal next stage is demoted to a follow-up line. When it
1301
+ marks only **non-blocking** requests (``reminder: true``), the fenced command
1302
+ stays the normal next stage and a reminder line is appended. Either way the
1303
+ added prose is host-neutral (no literal ``/clear``) so it survives verbatim
1304
+ into a generic bundle.
1253
1305
  """
1254
1306
  if host == "claude":
1255
1307
  clear_line = (
@@ -1271,13 +1323,40 @@ def _next_steps_block(next_command: str, host: str) -> str:
1271
1323
  "2. Then start a fresh session and run the next stage below — or "
1272
1324
  "re-run the forge navigator skill to resume from disk."
1273
1325
  )
1274
- # The next-stage command goes in a fenced block so mobile/remote hosts get a
1275
- # native copy button (inline code is not tap-to-copy). The fence sits before
1276
- # the sentinel, so the sentinel remains the absolute last line.
1277
- command_block = f"```\n{next_command}\n```"
1278
- return "\n".join(
1279
- ["**Next steps**", clear_line, next_line, "", command_block, NEXT_STEPS_SENTINEL]
1280
- )
1326
+ blocking = bool(reconcile and reconcile.get("required"))
1327
+ # The primary actionable command goes in a fenced block so mobile/remote hosts
1328
+ # get a native copy button (inline code is not tap-to-copy). For a blocking
1329
+ # epic-change request the primary is the reconcile command; otherwise it is the
1330
+ # normal next-stage command. The fence sits before the sentinel, so the
1331
+ # sentinel remains the absolute last line.
1332
+ fenced_command = reconcile["command"] if blocking else next_command
1333
+ lines = ["**Next steps**", clear_line]
1334
+ if blocking:
1335
+ count = reconcile["count"]
1336
+ plural = "s" if count != 1 else ""
1337
+ lines.append(
1338
+ f"2. Then reconcile the epic **before** the next stage — {count} "
1339
+ f"blocking epic change request{plural} flagged, and proceeding would "
1340
+ "build this feature's artifacts on a decomposition that is about to "
1341
+ "change. Run the reconcile command below first."
1342
+ )
1343
+ else:
1344
+ lines.append(next_line)
1345
+ lines.append("")
1346
+ lines.append(f"```\n{fenced_command}\n```")
1347
+ if blocking and reconcile.get("deferred"):
1348
+ lines.append(
1349
+ f"After reconciling, continue the pipeline with: `{reconcile['deferred']}`"
1350
+ )
1351
+ elif reconcile and reconcile.get("reminder"):
1352
+ count = reconcile["count"]
1353
+ plural = "s" if count != 1 else ""
1354
+ lines.append(
1355
+ f"You also flagged {count} epic change{plural} to reconcile when "
1356
+ f"convenient: `{reconcile['command']}`"
1357
+ )
1358
+ lines.append(NEXT_STEPS_SENTINEL)
1359
+ return "\n".join(lines)
1281
1360
 
1282
1361
 
1283
1362
  def stage_exit(
@@ -1310,6 +1389,13 @@ def stage_exit(
1310
1389
  the fixed successor. ``--next-feature`` names the first actionable
1311
1390
  feature for the epic handoff; without it the runtime placeholder
1312
1391
  ``{first-actionable-feature}`` passes through for the skill to resolve.
1392
+ - ``epicReconcile`` — present only when the exiting member carries
1393
+ ``open`` ``epicChangeRequests`` (epic-backflow). ``required: true`` (any
1394
+ ``blocksCurrent: true`` request) interposes a reconcile-first exit: the
1395
+ NEXT-STEPS primary command becomes ``/feature-forge:forge-0-epic {epic}``
1396
+ and the normal next stage is deferred. Only non-blocking requests set
1397
+ ``reminder: true`` and append a non-blocking reminder line. Absent when
1398
+ there are no open requests (common path) or the epic name is unresolvable.
1313
1399
 
1314
1400
  Read-only, deterministic, exit 0 — errors degrade to defaults, never
1315
1401
  crash a stage closing.
@@ -1355,6 +1441,37 @@ def stage_exit(
1355
1441
  )
1356
1442
  next_command = f"/feature-forge:{next_stage_id} {next_arg}" if next_stage_id else None
1357
1443
 
1444
+ # Epic backflow routing: an exiting member may carry epic-level change requests
1445
+ # (recorded by forge-1-prd/forge-2-tech). A `blocksCurrent: true` request means
1446
+ # the current feature's next stage would build on a soon-to-change decomposition,
1447
+ # so the exit interposes a reconcile-first step; only-`false` requests append a
1448
+ # non-blocking reminder. Read-only; the common path (no open requests) is a no-op.
1449
+ # The epic name comes from the `--epic` arg or the state's `epic` back-pointer.
1450
+ epic_reconcile: dict | None = None
1451
+ epic_name = epic or state.get("epic")
1452
+ open_requests = [
1453
+ r
1454
+ for r in state.get("epicChangeRequests", [])
1455
+ if isinstance(r, dict) and r.get("status") == "open"
1456
+ ]
1457
+ if open_requests and epic_name:
1458
+ reconcile_command = f"/feature-forge:forge-0-epic {epic_name}"
1459
+ blocking = [r for r in open_requests if r.get("blocksCurrent") is True]
1460
+ if blocking:
1461
+ epic_reconcile = {
1462
+ "required": True,
1463
+ "command": reconcile_command,
1464
+ "count": len(blocking),
1465
+ "deferred": next_command,
1466
+ }
1467
+ else:
1468
+ epic_reconcile = {
1469
+ "required": False,
1470
+ "reminder": True,
1471
+ "command": reconcile_command,
1472
+ "count": len(open_requests),
1473
+ }
1474
+
1358
1475
  directives = {
1359
1476
  "stage": stage,
1360
1477
  "stageNoun": STAGE_NOUN.get(stage, stage),
@@ -1372,9 +1489,13 @@ def stage_exit(
1372
1489
  "cleanTree": clean_tree,
1373
1490
  "host": host,
1374
1491
  }
1492
+ if epic_reconcile is not None:
1493
+ directives["epicReconcile"] = epic_reconcile
1375
1494
  return {
1376
1495
  "directives": directives,
1377
- "nextSteps": _next_steps_block(next_command or "/feature-forge:forge", host),
1496
+ "nextSteps": _next_steps_block(
1497
+ next_command or "/feature-forge:forge", host, epic_reconcile
1498
+ ),
1378
1499
  "sentinel": NEXT_STEPS_SENTINEL,
1379
1500
  }
1380
1501
 
@@ -143,6 +143,7 @@ and render from its output:
143
143
  - **Epic header:** name + `status` (active | paused | abandoned | complete).
144
144
  - **Dependency graph:** each feature with its `dependsOn`, as an arrow list or indented tree (the helper guarantees the graph is acyclic).
145
145
  - **Per-feature rows:** reuse the **existing status indicators** below (✅/✅⚠️/🔄/⬜/❌/✅🔍/⏭️/⚠️), driven by each feature's derived `stage`/`status`. Mark `blocked` features and list their `unmetDeps`.
146
+ - **Pending epic changes:** for any feature with `openEpicChangeRequests > 0`, append a ⚠️ marker and a hint: *"N pending epic change(s) — run `/feature-forge:forge-0-epic {epic}` to reconcile."* If `blockingEpicChangeRequests > 0`, use a stronger marker (⚠️ **blocking**) and word it *"reconcile the epic **before** writing specs"* — this mirrors the pause-now vs finish-then split that stage-exit already routes on. Take these counts **only** from `render-status --json` (`features[].openEpicChangeRequests` / `.blockingEpicChangeRequests`); do not read member `.pipeline-state.json` directly for them.
146
147
  - **Actionable vs blocked:** list the `actionable` set and the recommended `nextCommand`.
147
148
  - **Rollup:** `{complete}/{total} features complete`.
148
149
 
@@ -158,12 +159,13 @@ Dependency graph:
158
159
  audit-log (no deps)
159
160
 
160
161
  ✅ config-store complete
161
- 🔄 token-service forge-3-specs (in progress)
162
+ 🔄 token-service forge-3-specs (in progress) — ⚠️ 1 pending epic change
162
163
  ⬜ api-gateway blocked — waiting on token-service
163
164
  ✅ audit-log complete
164
165
 
165
166
  Actionable now: token-service
166
167
  Next: /feature-forge:forge-3-specs token-service
168
+ ⚠️ Pending epic changes: token-service (1). Run /feature-forge:forge-0-epic auth-overhaul to reconcile.
167
169
  ```
168
170
 
169
171
  All of this is reconstructed **purely from disk** — the manifest plus each member's `.pipeline-state.json`, with no in-memory state — so a fresh session renders the same dashboard. If `render-status` fails, do not render a partial dashboard; surface per the exit-1/exit-2 split in the **Feature Directory Resolution** block of `references/shared-conventions.md` (exit 1 → parse `{findings[]}` from stdout; exit 2 → surface the plain `Error:` stderr line verbatim).
@@ -28,6 +28,48 @@ python3 "$R/scripts/epic-manifest.py" validate "{epic}" --specs-dir "{specsDir}"
28
28
  manifest by hand. **Never auto-repair**, never offer an edit operation, and never proceed past
29
29
  this gate. Tell the user what is wrong and STOP.
30
30
 
31
+ ## Step E0-read — Surface Pending Epic Change Requests (backflow)
32
+
33
+ Immediately after E1's validate gate passes, and **before** offering the E2 operation menu,
34
+ scan for epic-level change requests that a member stage recorded during the pipeline (the
35
+ backflow path — see `references/pipeline-state-schema.json` `epicChangeRequests[]`, written by
36
+ `forge-1-prd`/`forge-2-tech`). Enumerate the manifest's feature list (already loaded in E1) and
37
+ read each member's `{specsDir}/{epic}/{member}/.pipeline-state.json`, collecting every
38
+ `epicChangeRequests[]` entry with `status: "open"`. If a member state is missing or unreadable,
39
+ report it and continue with the rest — do **not** abort edit mode over one bad member file (mirror
40
+ E1's "report, never silently repair" posture).
41
+
42
+ If any open requests exist, present them grouped (show `kind`, `target`, `rationale`,
43
+ `blocksCurrent`, `raisedBy`), then for **each** request use `AskUserQuestion` to offer **Apply**,
44
+ **Dismiss**, or **Skip for now**:
45
+
46
+ - **Apply — simple kinds (`add-feature`, `redep`):** pre-fill the matching E2 operation and flow
47
+ through E3→E4→E5→E6 unchanged. `add-feature` seeds the new feature's charter from the request's
48
+ `rationale` (the user still edits `exposes`/`consumes`/`dependsOn` before commit, exactly as
49
+ C3/C4/C5); `redep` pre-fills `set-dep {epic} {target} --depends-on "…"`. The existing E4 impact
50
+ warning fires normally — a `blocksCurrent` boundary change naturally trips it, which is correct.
51
+ - **Apply — composite kinds (`move-boundary`, `split`):** there is no single mutator (v1). Walk the
52
+ user through a **guided-manual** sequence — the relevant `set-dep` and/or direct `exposes`/
53
+ `consumes` edits on the composed manifest entries (per E3's "Contracts have no mutator" rule),
54
+ across **both** affected features — re-validating after each step. Confirm each mutation via
55
+ `AskUserQuestion`; never batch-apply.
56
+ - **Dismiss:** the user decides the epic is fine after all — flip the source request's `status` to
57
+ `"dismissed"` (no manifest mutation). Explicit only; there is **no auto-expiry**.
58
+ - **Skip for now:** leave the request `open`; it resurfaces on the next edit-mode entry and stays
59
+ visible in the navigator/verify (once Phase 2 surfacing lands).
60
+
61
+ **On a successful Apply** (mutator exit 0, or the guided-manual sequence confirmed), flip the
62
+ **source** request's `status` from `"open"` to `"applied"` in its member `.pipeline-state.json`,
63
+ using the same atomic temp-file + `os.replace` write the skill uses for any state edit. The E6 git
64
+ step already stages the whole `{specsDir}/{epic}/` subtree, so the flipped member state is captured
65
+ in the **same commit** as the manifest mutation — no new commit machinery. If a **Dismiss** is the
66
+ only action taken (no manifest mutation follows), still commit the member-state change under the
67
+ standard edit-mode message form, e.g. `forge({epic}): dismiss epic change request`.
68
+
69
+ E0-read is **read-then-offer** — it never auto-applies. Every apply still passes through E3/E4 with
70
+ explicit human confirmation, preserving the human-approves-every-mutation invariant. If **no** open
71
+ requests exist, say nothing and proceed straight to E2.
72
+
31
73
  ## Step E2 — Choose Operation
32
74
 
33
75
  Use `AskUserQuestion` to offer the edit operations, each mapping to one helper mutator:
@@ -81,6 +81,8 @@ Before moving to Step 4, summarize your coverage as text, then use `AskUserQuest
81
81
 
82
82
  **Parking lot:** If the user raises a concern that belongs to a different pipeline stage, acknowledge it and note it in the pipeline state's `notes` field: "Good point — I've noted that for the [tech spec/implementation specs]. Let's continue with [current stage]."
83
83
 
84
+ **Epic-level concern (backflow):** The parking lot above is for concerns about a *later stage of THIS feature*. If instead the interview reveals the **epic decomposition itself** is wrong — a **sibling feature must be added**, a **frozen boundary between features must move**, a feature must **split**, or a **dependency edge is wrong** — that is an *epic-level* concern and does **not** go in `notes`. It only applies when this feature is an epic member (its `.pipeline-state.json` has an `epic` back-pointer); for a standalone feature there is no epic to reconcile, so treat the concern as same-feature or out of scope. To record one, append an entry to the member state's `epicChangeRequests[]` array (same direct-edit path as `notes`; schema in `references/pipeline-state-schema.json`): `kind` (`add-feature`|`redep`|`move-boundary`|`split`), `target`, `rationale`, `raisedBy: "forge-1-prd"`, `raisedAt` (ISO-8601 UTC), `status: "open"`, and `blocksCurrent`. Set `blocksCurrent: true` when the change alters a contract (`exposes`/`consumes`) or dependency edge this feature relies on for its *next* stage (proceeding would build specs on a soon-to-change decomposition); `false` for a peer/downstream change this feature does not consume. When the change touches a contract/dep edge and the classification is genuinely ambiguous, confirm `blocksCurrent` with a single `AskUserQuestion`, defaulting to `true` (a false negative silently diverges two members' contracts). **Do not** edit `epic-manifest.json` here — recording is not applying; only `/feature-forge:forge-0-epic` edit mode mutates the epic. Then acknowledge without blocking: "That's an epic-level change — I've recorded it so `forge-0-epic` can reconcile it. [Blocking: We'll want to reconcile the epic before writing specs. | Non-blocking: We can finish this feature first and reconcile when convenient.]" and continue the interview.
85
+
84
86
  ## Step 4: Write the PRD
85
87
 
86
88
  Once the interview is thorough, write `{resolvedFeatureDir}/PRD.md` following the structure in `references/prd-template.md`.
@@ -92,6 +92,8 @@ Interview the user about technology decisions. Unlike the PRD interview, here yo
92
92
 
93
93
  **Parking lot:** If the user raises a concern that belongs to a different pipeline stage (e.g., backlog granularity, documentation format), acknowledge it and note it in the pipeline state's `notes` field: "Good point — I've noted that for the [specs/backlog/docs stage]. Let's continue with the tech spec."
94
94
 
95
+ **Epic-level concern (backflow):** The parking lot above is for concerns about a *later stage of THIS feature*. If instead the design reveals the **epic decomposition itself** is wrong — a **sibling feature must be added**, a **frozen boundary between features must move**, a feature must **split**, or a **dependency edge is wrong** — that is an *epic-level* concern and does **not** go in `notes`. It only applies when this feature is an epic member (its `.pipeline-state.json` has an `epic` back-pointer); for a standalone feature there is no epic to reconcile. To record one, append an entry to the member state's `epicChangeRequests[]` array (same direct-edit path as `notes`; schema in `references/pipeline-state-schema.json`): `kind` (`add-feature`|`redep`|`move-boundary`|`split`), `target`, `rationale`, `raisedBy: "forge-2-tech"`, `raisedAt` (ISO-8601 UTC), `status: "open"`, and `blocksCurrent`. Set `blocksCurrent: true` when the change alters a contract (`exposes`/`consumes`) or dependency edge this feature relies on for its *next* stage — proceeding to specs would build on a soon-to-change decomposition (this is the point of no cheap return, so bias toward `true`); `false` for a peer/downstream change this feature does not consume. When a contract/dep edge is touched and the classification is ambiguous, confirm `blocksCurrent` with a single `AskUserQuestion`, defaulting to `true`. **Do not** edit `epic-manifest.json` here — recording is not applying; only `/feature-forge:forge-0-epic` edit mode mutates the epic. Then acknowledge without blocking and continue the tech spec.
96
+
95
97
  ### Key Decision Areas to Cover
96
98
 
97
99
  Work through these areas across multiple turns, grouping related areas (1-2 per message):
@@ -197,7 +197,7 @@ Then commit this state write before launching (mandatory). The runner refuses to
197
197
 
198
198
  ### 3b. Launch Background Process
199
199
 
200
- Launch the loop **backgrounded** (`run_in_background: 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). Loop runs can take significant time (minutes to hours depending on backlog size). For the exact launch commands (incl. the `mkdir -p` state-dir guard) and the self-persisting vs. stdout-only detail, read `references/runner-contract.md`.
200
+ Launch the loop **backgrounded** (`run_in_background: 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). Loop runs can take significant time (minutes to hours depending on backlog size). 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`.
201
201
 
202
202
  ### 3c. Inform User
203
203
 
@@ -289,16 +289,18 @@ Update `{resolvedFeatureDir}/.pipeline-state.json`:
289
289
 
290
290
  **Host / clean-room fallback (not a user-selectable option):** if the question mechanism, the `Agent` tool, or the `forge-verifier` subagent is unavailable, do **not** run clean-room — degrade to printing `/feature-forge:forge-verify {feature} impl` for the user to run inline/manually (mirroring `autoInvokeNextStage`), and offer the auto-verify enable as plain text only if a config write is possible.
291
291
  2. **Then `/clear`.** Recommended **unconditionally** at this boundary for a clean start — independent of how full the context window is. Every artifact is on disk, so the work survives the clear. **I can't `/clear` for you — you have to run it yourself.**
292
- 3. **Then run `/feature-forge:forge-1-prd {chosen}`** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk.
292
+ 3. **Then run the next command** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk:
293
+
294
+ ```
295
+ /feature-forge:forge-1-prd {chosen}
296
+ ```
293
297
 
294
298
  ## Gotchas
295
299
 
296
300
  - **Plugin-root discovery (1b-epic helper) covers installed paths, not workspace-dev checkouts.** The `forge-root.sh` search in 1b-epic probes `~/.claude/skills/feature-forge`, `~/.claude/plugins/cache/*/feature-forge/*` (marketplace-cache installs), `~/.claude/plugins/*/feature-forge`, and `./.agents/skills/feature-forge` — the locations of an **installed** plugin. A feature-forge **source checkout** (e.g. `~/workspace/feature-forge`) is not on that list, so the helper exits "cannot locate plugin root." That is expected in a dev environment, not a bug; run the epic-manifest script from the checkout directly (`python3 <checkout>/scripts/epic-manifest.py …`). The bootstrap prelude wraps its candidate loop in `bash -c` so the `~/.claude/plugins/*/feature-forge` glob is zsh-safe: an empty expansion no longer aborts the loop under zsh's `nomatch`.
297
301
  - `{backlogDir}` is a **directory path**, not a file path. Pass `specs/auth`, not `specs/auth/backlog.json`.
298
- - rauf resolves `RAUF.md` with fallback: checks `{backlogDir}/.rauf/RAUF.md` first, then the project's `.rauf/RAUF.md`. As long as the runner is installed in the project, the prompt template will be found.
299
- - State files (state.json, {loopRunner.logFile}, etc.) are created at `{backlogDir}/{loopRunner.stateDir}/` — this is within the feature's spec directory and is expected. State is isolated per backlog dir, so concurrent features don't collide.
300
- - If the session disconnects during a long-running loop, the runner process continues independently. The user can check results later with the status / list commands.
302
+ - rauf resolves `RAUF.md` with fallback (`{backlogDir}/.rauf/RAUF.md` first, then the project's `.rauf/RAUF.md`) — found as long as the runner is installed in the project. State files (state.json, {loopRunner.logFile}, etc.) are created at `{backlogDir}/{loopRunner.stateDir}/`, within the feature's spec directory (expected) and isolated per backlog dir, so concurrent features don't collide.
303
+ - If the session disconnects during a long-running loop, the runner process continues independently — the user can check results later with the status / list commands. If a previous run left a stale lock, the user may need to pass `--force` to clear it (rauf reports this error clearly).
301
304
  - Never run the run command in the foreground (without `run_in_background`) — 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 `Monitor` tool (3d), never `sleep`/poll in the foreground. The `Monitor` must use `persistent: true` (not a bounded `timeout_ms`), watch the **structured** surface (`events.ndjson`), and never filter on raw `RAUF_*` tokens — they appear in agent prose and false-match. 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.
302
- - If a previous loop run left a stale lock, the user may need to pass `--force` to clear it. rauf will report this error clearly.
303
305
  - The version gate (1c) uses the `--json` form on purpose; never parse `rauf version`'s human output.
304
306
  - **Implementation artifacts must not cite specs.** The loop should **read** the specs and `backlog.json` freely — they are the source of truth for what to build, and the backlog rightly references specs for provenance. But the artifacts the loop **writes into the target repo** (source code, generated `SKILL.md`/agent files, configs, code comments) must be **self-contained**: they must NOT reference feature-forge spec files (no `See specs/{feature}/NN-*.md`, no "source spec" provenance notes in shipped output). Specs are pre-implementation inputs that may be archived or deleted once the feature ships; the implementation must stand on its own. This applies only to shipped implementation output — never to the backlog or spec documents, which should keep citing specs.
@@ -16,7 +16,11 @@ Loop completed for {feature}. All {N} items implemented successfully.
16
16
 
17
17
  1. **Verify is already offered above.** Impl-verify is offered interactively right after this report (Step 5b for a standalone feature, Step 6.1 for an epic member) — run it there rather than as a second gate. It runs clean-room, so it needs no fresh session.
18
18
  2. **Clearing is optional here — warm is fine.** `forge-6-docs` benefits from the still-warm context of what the loop actually did, so continuing in this same session is the easy default. A cold start also works — every artifact is on disk — but there is no need to force it.
19
- 3. **Then run `/feature-forge:forge-6-docs {feature}`** — in this warm session, or a fresh one if you prefer.
19
+ 3. **Then run the next command** — in this warm session, or a fresh one if you prefer:
20
+
21
+ ```
22
+ /feature-forge:forge-6-docs {feature}
23
+ ```
20
24
 
21
25
  **Runner review pass.** A review flag (e.g. rauf's `--review`) makes the runner run
22
26
  a post-loop review that **auto-creates and implements fix items** rather than handing