@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.
- package/adapters/claude/.feature-forge-bundle.json +1 -1
- package/adapters/claude/references/pipeline-state-schema.json +77 -1
- package/adapters/claude/references/stage-exit-protocol.md +50 -2
- package/adapters/claude/references/templates/specs-hygiene/AGENTS.md +9 -0
- package/adapters/claude/references/templates/specs-hygiene/CLAUDE.md +9 -0
- package/adapters/claude/scripts/epic-manifest.py +26 -0
- package/adapters/claude/scripts/forge-session.py +130 -9
- package/adapters/claude/skills/forge/SKILL.md +3 -1
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/claude/skills/forge-1-prd/SKILL.md +2 -0
- package/adapters/claude/skills/forge-2-tech/SKILL.md +2 -0
- package/adapters/claude/skills/forge-5-loop/SKILL.md +8 -6
- package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +22 -2
- package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
- package/adapters/claude/skills/forge-verify/SKILL.md +2 -2
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +12 -0
- package/adapters/codex/.feature-forge-bundle.json +1 -1
- package/adapters/codex/references/pipeline-state-schema.json +77 -1
- package/adapters/codex/references/stage-exit-protocol.md +50 -2
- package/adapters/codex/references/templates/specs-hygiene/AGENTS.md +9 -0
- package/adapters/codex/references/templates/specs-hygiene/CLAUDE.md +9 -0
- package/adapters/codex/scripts/epic-manifest.py +26 -0
- package/adapters/codex/scripts/forge-session.py +130 -9
- package/adapters/codex/skills/forge/SKILL.md +3 -1
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/codex/skills/forge-1-prd/SKILL.md +2 -0
- package/adapters/codex/skills/forge-2-tech/SKILL.md +2 -0
- package/adapters/codex/skills/forge-5-loop/SKILL.md +8 -6
- package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +22 -2
- package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
- package/adapters/codex/skills/forge-verify/SKILL.md +2 -2
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +12 -0
- package/adapters/copilot/.feature-forge-bundle.json +1 -1
- package/adapters/copilot/references/pipeline-state-schema.json +77 -1
- package/adapters/copilot/references/stage-exit-protocol.md +50 -2
- package/adapters/copilot/references/templates/specs-hygiene/AGENTS.md +9 -0
- package/adapters/copilot/references/templates/specs-hygiene/CLAUDE.md +9 -0
- package/adapters/copilot/scripts/epic-manifest.py +26 -0
- package/adapters/copilot/scripts/forge-session.py +130 -9
- package/adapters/copilot/skills/forge/forge.md +3 -1
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +2 -0
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +2 -0
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +8 -6
- package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +22 -2
- package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
- package/adapters/copilot/skills/forge-verify/forge-verify.md +2 -2
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +12 -0
- package/adapters/cursor/.feature-forge-bundle.json +1 -1
- package/adapters/cursor/references/pipeline-state-schema.json +77 -1
- package/adapters/cursor/references/stage-exit-protocol.md +50 -2
- package/adapters/cursor/references/templates/specs-hygiene/AGENTS.md +9 -0
- package/adapters/cursor/references/templates/specs-hygiene/CLAUDE.md +9 -0
- package/adapters/cursor/scripts/epic-manifest.py +26 -0
- package/adapters/cursor/scripts/forge-session.py +130 -9
- package/adapters/cursor/skills/forge/forge.mdc +3 -1
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +2 -0
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +2 -0
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +8 -6
- package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +22 -2
- package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +2 -2
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +12 -0
- package/adapters/gemini/.feature-forge-bundle.json +1 -1
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/pipeline-state-schema.json +77 -1
- package/adapters/gemini/references/stage-exit-protocol.md +50 -2
- package/adapters/gemini/references/templates/specs-hygiene/AGENTS.md +9 -0
- package/adapters/gemini/references/templates/specs-hygiene/CLAUDE.md +9 -0
- package/adapters/gemini/scripts/epic-manifest.py +26 -0
- package/adapters/gemini/scripts/forge-session.py +130 -9
- package/adapters/gemini/skills/forge/forge.md +3 -1
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +2 -0
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +2 -0
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +8 -6
- package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +22 -2
- package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +11 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +11 -0
- package/adapters/gemini/skills/forge-verify/forge-verify.md +2 -2
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +12 -0
- package/package.json +1 -1
|
@@ -101,6 +101,13 @@ default / `claude-cli` path skips this guard (the aliases are valid there).
|
|
|
101
101
|
> rauf `author-backlog` skill to keep `model` **provider-neutral** by default (or to
|
|
102
102
|
> document that writing a tier alias binds the backlog to Claude agents). That lives
|
|
103
103
|
> in the separate rauf plugin/repo, not feature-forge; tracked as a follow-up.
|
|
104
|
+
>
|
|
105
|
+
> **Follow-up (out of scope here — rauf repo).** The durable fix for the root/sandbox
|
|
106
|
+
> refusal (see "Root/sandbox env guard" under Step 3b) is for **rauf itself** to honor
|
|
107
|
+
> `IS_SANDBOX` when it launches `claude --dangerously-skip-permissions` as root (or to
|
|
108
|
+
> detect root+flag-refused and emit a clear error instead of an opaque circuit-break).
|
|
109
|
+
> feature-forge's launch-time export is the mitigation; the upstream fix lives in the
|
|
110
|
+
> rauf plugin/repo. Track as a follow-up.
|
|
104
111
|
|
|
105
112
|
## Optional flags catalog (Step 2d, rauf)
|
|
106
113
|
|
|
@@ -131,6 +138,19 @@ session, then supervise it live via the runner's structured event file.
|
|
|
131
138
|
> changes after that commit, surface it and let the user commit/stash or pass
|
|
132
139
|
> `--force`; never auto-pass `--force`.
|
|
133
140
|
|
|
141
|
+
> **Root/sandbox env guard.** On a hosted remote (e.g. Claude.ai) the loop often runs
|
|
142
|
+
> **as root**. rauf's default Claude launch is `claude -p --dangerously-skip-permissions
|
|
143
|
+
> …`, which the Claude CLI **refuses under root unless `IS_SANDBOX` is set** — the remote
|
|
144
|
+
> container is a legitimate ephemeral sandbox, but without the flag every spawn exits and
|
|
145
|
+
> rauf circuit-breaks (*"3 consecutive infra failures — halting"*) with no hint of the
|
|
146
|
+
> cause. So when — and only when — the launcher is root (`[ "$(id -u)" = 0 ]`), export
|
|
147
|
+
> `IS_SANDBOX="${IS_SANDBOX:-1}"` in front of the launch (an explicitly-set value is
|
|
148
|
+
> honored; the `:-1` only supplies a default). Non-root/local runs are unaffected — the
|
|
149
|
+
> guard is a no-op. **Surface a one-line note** when you set it — e.g. *"running as root →
|
|
150
|
+
> setting IS_SANDBOX=1 so the sandboxed runner can use --dangerously-skip-permissions"* —
|
|
151
|
+
> so the behavior is never silent. `forge-session.py doctor` also reports this condition.
|
|
152
|
+
> Both launch commands below already carry the guard.
|
|
153
|
+
|
|
134
154
|
**Do NOT redirect the run's stdout into `{loopRunner.stateDir}`.** rauf **persists
|
|
135
155
|
its own** `{stateDir}/events.ndjson` (structured) and `{stateDir}/{logFile}` (human)
|
|
136
156
|
natively, and **rotates** them at the start of every run (the prior run's files are
|
|
@@ -150,7 +170,7 @@ rotation timing. So:
|
|
|
150
170
|
(Step 3d). Guard the very first run with the state dir:
|
|
151
171
|
|
|
152
172
|
```
|
|
153
|
-
mkdir -p {backlogDir}/{loopRunner.stateDir} && {rendered runCommand}
|
|
173
|
+
mkdir -p {backlogDir}/{loopRunner.stateDir} && { [ "$(id -u)" = 0 ] && export IS_SANDBOX="${IS_SANDBOX:-1}" || true; } && {rendered runCommand}
|
|
154
174
|
```
|
|
155
175
|
|
|
156
176
|
(Note: the `--ndjson` stdout stream and `loopRunner.eventStreamCommand` are **not**
|
|
@@ -160,7 +180,7 @@ rotation timing. So:
|
|
|
160
180
|
collide with any native file or be swept into `archive/`, then Monitor that file:
|
|
161
181
|
|
|
162
182
|
```
|
|
163
|
-
mkdir -p {backlogDir}/{loopRunner.stateDir} && {rendered eventStreamCommand} > {backlogDir}/forge-events.ndjson 2>&1
|
|
183
|
+
mkdir -p {backlogDir}/{loopRunner.stateDir} && { [ "$(id -u)" = 0 ] && export IS_SANDBOX="${IS_SANDBOX:-1}" || true; } && {rendered eventStreamCommand} > {backlogDir}/forge-events.ndjson 2>&1
|
|
164
184
|
```
|
|
165
185
|
|
|
166
186
|
The background task's exit notification remains the single authoritative terminal
|
|
@@ -22,3 +22,14 @@ This project was scaffolded by forge-bootstrap. To continue development:
|
|
|
22
22
|
- Documents under `specs/` (PRDs, tech specs, numbered implementation specs) establish the backlog. They are **not** kept in sync with the code as it evolves.
|
|
23
23
|
- Do not flag or "fix" divergence between a finalized spec and the implementation — code is the source of truth for behavior.
|
|
24
24
|
- It's fine for `specs/` artifacts and `backlog.json` to reference specs for provenance, but implementation artifacts (source code, generated skills/agents, configs, docs) must not reference spec files, which may be archived or deleted after a feature ships.
|
|
25
|
+
|
|
26
|
+
## Tooling feedback (feature-forge / rauf)
|
|
27
|
+
|
|
28
|
+
This project is driven by the feature-forge pipeline and the rauf loop. Help improve those tools by capturing friction as you hit it — papercuts included, not just outright bugs.
|
|
29
|
+
|
|
30
|
+
- **When to flag:** any forge/rauf command, skill, agent, or prompt that is confusing, buggy, missing a capability, forces a workaround, or produces a surprising result.
|
|
31
|
+
- **Where to file** — route by which tool the friction is with:
|
|
32
|
+
- feature-forge (pipeline stages, forge skills/agents): https://github.com/garygentry/feature-forge/issues
|
|
33
|
+
- rauf (the autonomous loop runner, `rauf` CLI): https://github.com/garygentry/rauf/issues
|
|
34
|
+
- **How:** capture it while fresh — *what you ran / what you expected / what actually happened / a fix idea* — then propose a titled issue and file it with `gh issue create` **on the human's go-ahead, not silently.**
|
|
35
|
+
- **In an autonomous rauf iteration:** do **not** open issues mid-loop. Note the friction in `progress.md` for the human to triage later.
|
|
@@ -23,3 +23,14 @@ This project was scaffolded by forge-bootstrap. To continue development:
|
|
|
23
23
|
- Documents under `specs/` (PRDs, tech specs, numbered implementation specs) establish the backlog. They are **not** kept in sync with the code as it evolves.
|
|
24
24
|
- Don't flag or "fix" divergence between a finalized spec and the implementation — code is the source of truth for behavior.
|
|
25
25
|
- It's fine for `specs/` artifacts and `backlog.json` to reference specs for provenance, but implementation artifacts (source code, generated skills/agents, configs, docs) must not reference spec files, which may be archived or deleted after a feature ships.
|
|
26
|
+
|
|
27
|
+
## Tooling feedback (feature-forge / rauf)
|
|
28
|
+
|
|
29
|
+
I'm driving this project with the feature-forge pipeline and the rauf loop, and I want to keep improving them. When you hit friction with either tool, help me capture it — papercuts included, not just outright bugs.
|
|
30
|
+
|
|
31
|
+
- **When to flag:** any forge/rauf command, skill, agent, or prompt that is confusing, buggy, missing a capability, forces a workaround, or produces a surprising result.
|
|
32
|
+
- **Where to file** — route by which tool the friction is with:
|
|
33
|
+
- feature-forge (pipeline stages, `/feature-forge:*`, forge skills/agents): https://github.com/garygentry/feature-forge/issues
|
|
34
|
+
- rauf (the autonomous loop runner, `rauf` CLI): https://github.com/garygentry/rauf/issues
|
|
35
|
+
- **How:** capture it while fresh — *what you ran / what you expected / what actually happened / a fix idea* — then propose a titled issue and file it with `gh issue create` **once I give the go-ahead, not silently.**
|
|
36
|
+
- **In an autonomous rauf iteration:** don't open issues mid-loop. Note the friction in `progress.md` for me to triage later.
|
|
@@ -162,9 +162,9 @@ Load into context ALL artifacts for this feature based on mode:
|
|
|
162
162
|
|
|
163
163
|
Read `references/verification-checklists.md` for the detailed checklists per mode. Execute every check. Do not skip checks because things "look fine." That same reference also holds the relocated **Findings Document Template (Step 4)**, the worked **Example Findings (Step 4)**, and the **Epic Mode State Write Detail (Step 6)** sections used later in this skill.
|
|
164
164
|
|
|
165
|
-
Each check in `verification-checklists.md` has a unique ID (CHECK-P01, CHECK-T01, CHECK-S01, CHECK-B01, etc.). As you execute each check, record its ID and result (pass/fail/not-applicable). After completing all checks, report the total: "Executed N of M checks. Results: X pass, Y fail, Z not-applicable." If your count is significantly below the expected total for the mode (prd: ~15 checks, tech: ~15 checks, specs: ~38 checks, backlog: ~25 checks, impl: ~20 checks, epic: ~
|
|
165
|
+
Each check in `verification-checklists.md` has a unique ID (CHECK-P01, CHECK-T01, CHECK-S01, CHECK-B01, etc.). As you execute each check, record its ID and result (pass/fail/not-applicable). After completing all checks, report the total: "Executed N of M checks. Results: X pass, Y fail, Z not-applicable." If your count is significantly below the expected total for the mode (prd: ~15 checks, tech: ~15 checks, specs: ~38 checks, backlog: ~25 checks, impl: ~20 checks, epic: ~9 checks), you likely skipped checks — go back and complete them.
|
|
166
166
|
|
|
167
|
-
**Epic mode dispatch.** Epic mode is a small (~
|
|
167
|
+
**Epic mode dispatch.** Epic mode is a small (~9-check) checklist, so per the single-vs-parallel rule above, dispatch a **single `forge-verifier`** via the Agent tool, passing the epic name and `mode=epic`. The verifier runs CHECK-E01..E09 from the `## Epic Mode Checklist` in `references/verification-checklists.md` (E01/E02/E03/E08 are delegated to `epic-manifest.py validate`/`check-name`; E04–E07 and E09 are verifier judgment) and returns its findings.
|
|
168
168
|
|
|
169
169
|
### Important: Be Specific, Not General
|
|
170
170
|
|
|
@@ -223,6 +223,18 @@ python3 "$R/scripts/epic-manifest.py" validate "{epic}" --specs-dir "{specsDir}"
|
|
|
223
223
|
`.pipeline-state.json` `epic` value names this epic, and every `features[]` entry has a
|
|
224
224
|
matching member directory. On conflict the **manifest wins** (REQ-STATE-01); report, do
|
|
225
225
|
not auto-repair.
|
|
226
|
+
- [ ] **CHECK-E09**: **open epic change requests** — any member whose `.pipeline-state.json`
|
|
227
|
+
carries `epicChangeRequests[]` entries with `status: "open"` is surfaced as a **non-fatal**
|
|
228
|
+
finding (one per open request). Severity keys off `blocksCurrent`: a **blocking** request →
|
|
229
|
+
`inconsistency` (the epic decomposition and an in-flight member disagree; specs written now
|
|
230
|
+
would build on a soon-invalid premise), a **non-blocking** request → `improvement` (a
|
|
231
|
+
peer/downstream change to reconcile when convenient). Name the request's `kind`, `target`,
|
|
232
|
+
and `rationale`, and point at `/feature-forge:forge-0-epic {epic}` to reconcile. **Report, do
|
|
233
|
+
not repair** (same posture as CHECK-E07). Which members have open requests comes from the
|
|
234
|
+
same `render-status --json` counts the navigator uses (`features[].openEpicChangeRequests` /
|
|
235
|
+
`.blockingEpicChangeRequests`); the per-request `kind`/`target`/`rationale` detail is read
|
|
236
|
+
from the member `.pipeline-state.json` already loaded in Step 2. This is the pre-emptive
|
|
237
|
+
surface for the divergence class CHECK-E06/E07 otherwise catch only after the fact.
|
|
226
238
|
|
|
227
239
|
## Findings Document Template (Step 4)
|
|
228
240
|
|
|
@@ -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": "
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
1275
|
-
#
|
|
1276
|
-
#
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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(
|
|
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
|
|
|
@@ -142,6 +142,7 @@ and render from its output:
|
|
|
142
142
|
- **Epic header:** name + `status` (active | paused | abandoned | complete).
|
|
143
143
|
- **Dependency graph:** each feature with its `dependsOn`, as an arrow list or indented tree (the helper guarantees the graph is acyclic).
|
|
144
144
|
- **Per-feature rows:** reuse the **existing status indicators** below (✅/✅⚠️/🔄/⬜/❌/✅🔍/⏭️/⚠️), driven by each feature's derived `stage`/`status`. Mark `blocked` features and list their `unmetDeps`.
|
|
145
|
+
- **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.
|
|
145
146
|
- **Actionable vs blocked:** list the `actionable` set and the recommended `nextCommand`.
|
|
146
147
|
- **Rollup:** `{complete}/{total} features complete`.
|
|
147
148
|
|
|
@@ -157,12 +158,13 @@ Dependency graph:
|
|
|
157
158
|
audit-log (no deps)
|
|
158
159
|
|
|
159
160
|
✅ config-store complete
|
|
160
|
-
🔄 token-service forge-3-specs (in progress)
|
|
161
|
+
🔄 token-service forge-3-specs (in progress) — ⚠️ 1 pending epic change
|
|
161
162
|
⬜ api-gateway blocked — waiting on token-service
|
|
162
163
|
✅ audit-log complete
|
|
163
164
|
|
|
164
165
|
Actionable now: token-service
|
|
165
166
|
Next: /feature-forge:forge-3-specs token-service
|
|
167
|
+
⚠️ Pending epic changes: token-service (1). Run /feature-forge:forge-0-epic auth-overhaul to reconcile.
|
|
166
168
|
```
|
|
167
169
|
|
|
168
170
|
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).
|