@garygentry/feature-forge 0.2.5 → 0.2.7

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 (107) hide show
  1. package/adapters/claude/.feature-forge-bundle.json +1 -1
  2. package/adapters/claude/agents/forge-verifier.md +2 -0
  3. package/adapters/claude/references/portable-root.md +18 -9
  4. package/adapters/claude/references/shared-conventions.md +21 -4
  5. package/adapters/claude/references/stage-exit-protocol.md +11 -7
  6. package/adapters/claude/references/vendor-construct-inventory.md +1 -0
  7. package/adapters/claude/scripts/forge-session.py +248 -5
  8. package/adapters/claude/skills/forge/SKILL.md +9 -9
  9. package/adapters/claude/skills/forge-0-epic/SKILL.md +5 -5
  10. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +2 -2
  11. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  12. package/adapters/claude/skills/forge-1-prd/SKILL.md +1 -1
  13. package/adapters/claude/skills/forge-2-tech/SKILL.md +1 -1
  14. package/adapters/claude/skills/forge-3-specs/SKILL.md +1 -1
  15. package/adapters/claude/skills/forge-4-backlog/SKILL.md +1 -1
  16. package/adapters/claude/skills/forge-5-loop/SKILL.md +2 -2
  17. package/adapters/claude/skills/forge-6-docs/SKILL.md +1 -1
  18. package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
  19. package/adapters/claude/skills/forge-init/SKILL.md +1 -1
  20. package/adapters/claude/skills/forge-verify/SKILL.md +9 -2
  21. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
  22. package/adapters/codex/.feature-forge-bundle.json +1 -1
  23. package/adapters/codex/agents/forge-verifier.toml +2 -0
  24. package/adapters/codex/references/portable-root.md +18 -9
  25. package/adapters/codex/references/shared-conventions.md +21 -4
  26. package/adapters/codex/references/stage-exit-protocol.md +11 -7
  27. package/adapters/codex/references/vendor-construct-inventory.md +1 -0
  28. package/adapters/codex/scripts/forge-session.py +248 -5
  29. package/adapters/codex/skills/forge/SKILL.md +9 -9
  30. package/adapters/codex/skills/forge-0-epic/SKILL.md +5 -5
  31. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +2 -2
  32. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  33. package/adapters/codex/skills/forge-1-prd/SKILL.md +1 -1
  34. package/adapters/codex/skills/forge-2-tech/SKILL.md +1 -1
  35. package/adapters/codex/skills/forge-3-specs/SKILL.md +1 -1
  36. package/adapters/codex/skills/forge-4-backlog/SKILL.md +1 -1
  37. package/adapters/codex/skills/forge-5-loop/SKILL.md +2 -2
  38. package/adapters/codex/skills/forge-6-docs/SKILL.md +1 -1
  39. package/adapters/codex/skills/forge-bootstrap/SKILL.md +4 -4
  40. package/adapters/codex/skills/forge-init/SKILL.md +1 -1
  41. package/adapters/codex/skills/forge-verify/SKILL.md +9 -2
  42. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
  43. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  44. package/adapters/copilot/agents/forge-verifier.md +2 -0
  45. package/adapters/copilot/references/portable-root.md +18 -9
  46. package/adapters/copilot/references/shared-conventions.md +21 -4
  47. package/adapters/copilot/references/stage-exit-protocol.md +11 -7
  48. package/adapters/copilot/references/vendor-construct-inventory.md +1 -0
  49. package/adapters/copilot/scripts/forge-session.py +248 -5
  50. package/adapters/copilot/skills/forge/forge.md +9 -9
  51. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +5 -5
  52. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +2 -2
  53. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  54. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +1 -1
  55. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +1 -1
  56. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +1 -1
  57. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +1 -1
  58. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +2 -2
  59. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +1 -1
  60. package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +4 -4
  61. package/adapters/copilot/skills/forge-init/forge-init.md +1 -1
  62. package/adapters/copilot/skills/forge-verify/forge-verify.md +9 -2
  63. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
  64. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  65. package/adapters/cursor/agents/forge-verifier.mdc +2 -0
  66. package/adapters/cursor/references/portable-root.md +18 -9
  67. package/adapters/cursor/references/shared-conventions.md +21 -4
  68. package/adapters/cursor/references/stage-exit-protocol.md +11 -7
  69. package/adapters/cursor/references/vendor-construct-inventory.md +1 -0
  70. package/adapters/cursor/scripts/forge-session.py +248 -5
  71. package/adapters/cursor/skills/forge/forge.mdc +9 -9
  72. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +5 -5
  73. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +2 -2
  74. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  75. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +1 -1
  76. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +1 -1
  77. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +1 -1
  78. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +1 -1
  79. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +2 -2
  80. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +1 -1
  81. package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +4 -4
  82. package/adapters/cursor/skills/forge-init/forge-init.mdc +1 -1
  83. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +9 -2
  84. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
  85. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  86. package/adapters/gemini/agents/forge-verifier.md +2 -0
  87. package/adapters/gemini/gemini-extension.json +1 -1
  88. package/adapters/gemini/references/portable-root.md +18 -9
  89. package/adapters/gemini/references/shared-conventions.md +21 -4
  90. package/adapters/gemini/references/stage-exit-protocol.md +11 -7
  91. package/adapters/gemini/references/vendor-construct-inventory.md +1 -0
  92. package/adapters/gemini/scripts/forge-session.py +248 -5
  93. package/adapters/gemini/skills/forge/forge.md +9 -9
  94. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +5 -5
  95. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +2 -2
  96. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  97. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +1 -1
  98. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +1 -1
  99. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +1 -1
  100. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +1 -1
  101. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +2 -2
  102. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +1 -1
  103. package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +4 -4
  104. package/adapters/gemini/skills/forge-init/forge-init.md +1 -1
  105. package/adapters/gemini/skills/forge-verify/forge-verify.md +9 -2
  106. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
  107. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feature-forge",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "agent": "claude",
5
5
  "generatedBy": "python3 scripts/build-adapters.py"
6
6
  }
@@ -18,6 +18,8 @@ You are the "second set of eyes." You receive artifacts (PRDs, tech specs, imple
18
18
 
19
19
  You have READ-ONLY access. You cannot and should not modify any files. Your output is returned as your response — the parent agent handles writing the findings document to disk.
20
20
 
21
+ **You ARE the verifier — you never dispatch one.** You have no Agent/Task tool. Your pre-loaded `forge-verify` skill contains a "Subagent Delegation (parent orchestrator only)" section describing how a *parent* dispatches a `forge-verifier` — that guidance is for the parent, not for you. Ignore it: do not attempt to delegate, spawn a subagent, or return a "verification is running / will surface shortly" placeholder. Execute the verification checks yourself and return the findings block. Delegating from here is a self-referential loop that produces no work and no findings artifact.
22
+
21
23
  ## How You Work
22
24
 
23
25
  1. Read the pipeline state file to understand what stage the feature is at
@@ -12,7 +12,7 @@ against the fenced block here, byte-for-byte.
12
12
  ## Canonical bootstrap prelude
13
13
 
14
14
  ```bash
15
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
15
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
16
16
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
17
17
  ```
18
18
 
@@ -24,25 +24,34 @@ makes several calls, add the prelude once and reuse `$R` for each. A fresh block
24
24
  prelude (per-block re-resolution). Worked example:
25
25
 
26
26
  ```bash
27
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
27
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
28
28
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
29
29
  python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specsDir}" --json
30
30
  ```
31
31
 
32
32
  ## Invariants (do NOT "fix" these)
33
33
 
34
- 1. **Probes paths, not the env var.** The prelude's `for d in …` enumerates directory paths to
35
- locate an executable `forge-root.sh`; it contains no plugin-root environment variable. That is what lets
36
- a prelude occurrence satisfy the "zero residual var in canonical surfaces" rule while staying
37
- portable.
34
+ 1. **First hint is the Claude plugin-root env var; the rest are paths.** The prelude's
35
+ `for d in …` list leads with the `CLAUDE_PLUGIN_ROOT` env var (in default-empty `:-` form)
36
+ — the exact bundle dir Claude exports in every marketplace/plugin session — followed by the
37
+ directory-path candidates. The hint gives exact, glob-free resolution on any current/future
38
+ Claude layout (no version-skew window); when unset it expands to empty and is harmlessly
39
+ skipped, so other hosts fall through to the path candidates unchanged. This is the **one
40
+ sanctioned** appearance of that variable in canonical surfaces: spec-purity rule 3 allows it
41
+ by stripping the byte-pinned prelude before its residual-var scan (so a stray var anywhere
42
+ else — including the default-empty form — still fails), and `forge-agent-adapters-build`
43
+ translates it to `FEATURE_FORGE_ROOT` for non-Claude bundles (which `forge-root.sh` already
44
+ prefers). The exact literal is shown only in the fenced prelude above and audited in
45
+ `vendor-construct-inventory.md`.
38
46
  2. **First-discoverable-resolver-wins.** The `exec` inside the `$(…)` command substitution means
39
47
  the loop stops at the first directory holding an executable `forge-root.sh` and delegates ALL
40
48
  final root resolution to that script. The `for` list is a discovery order for `forge-root.sh`
41
49
  itself, not a fallback chain for the plugin root. Removing the `exec` to "keep looping" is a
42
50
  regression — once `exec`'d, the loop is replaced by the resolver process and never advances.
43
- 3. **Prelude candidate set is an agent-neutral bootstrap subset.** The prelude's `for d` list
44
- exists only to bootstrap-discover `forge-root.sh`; the authoritative multi-root probe lives in
45
- `forge-root.sh` step 2. The list enumerates install roots across agents — the Claude
51
+ 3. **Prelude candidate set is an agent-neutral bootstrap subset (after the env hint).** The
52
+ prelude's `for d` list exists only to bootstrap-discover `forge-root.sh`; the authoritative
53
+ multi-root probe lives in `forge-root.sh` step 2. After the leading env-var hint (invariant 1),
54
+ the list enumerates install roots across agents — the Claude
46
55
  skill/plugin dirs — including the marketplace-cache layout
47
56
  `~/.claude/plugins/cache/<marketplace>/feature-forge/<version>/`, listed before the
48
57
  single-star plugins glob so a versioned cache install always beats the marketplace clone —
@@ -81,7 +81,7 @@ Extract these config values (use defaults if not present):
81
81
  Before any file I/O against a feature's artifacts, resolve its directory through the deterministic helper rather than hardcoding `{specsDir}/{feature}/`. This makes flat (`{specsDir}/{feature}/`) and nested (`{specsDir}/{epic}/{feature}/`) layouts both resolve from a bare feature name (REQ-DIR-03), with standalone features behaving exactly as today.
82
82
 
83
83
  ```bash
84
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
84
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
85
85
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
86
86
  resolvedFeatureDir=$(python3 "$R/scripts/epic-manifest.py" \
87
87
  resolve "<feature>" --specs-dir "<specsDir>")
@@ -96,7 +96,7 @@ In both failure cases, do not fall back to a guessed path.
96
96
  **On `not-found`, check other branches before stopping.** With `branchPerFeature`, the feature's directory (and its `.pipeline-state.json`) may exist only on its topic branch — invisible from the default branch of a fresh clone. Before concluding the pipeline does not exist, run the read-only cross-branch discovery:
97
97
 
98
98
  ```bash
99
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
99
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
100
100
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
101
101
  python3 "$R/scripts/forge-session.py" discover-feature "<feature>" --specs-dir "<specsDir>" --json
102
102
  ```
@@ -124,7 +124,7 @@ Whenever a stage creates the specs tree for the first time (the first PRD or epi
124
124
  Run this after creating the feature/epic directory, before the stage's git commit:
125
125
 
126
126
  ```bash
127
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
127
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
128
128
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
129
129
  mkdir -p "<specsDir>"
130
130
  [ -f "<specsDir>/AGENTS.md" ] || cp "$R/references/templates/specs-hygiene/AGENTS.md" "<specsDir>/AGENTS.md"
@@ -151,7 +151,7 @@ After resolving the feature directory, check the feature's `.pipeline-state.json
151
151
  To obtain the manifest contracts and the live completion status of each dependency in one deterministic call, run `render-status` and read the per-feature `status` and the `consumes`/`exposes` arrays rather than re-deriving them:
152
152
 
153
153
  ```bash
154
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
154
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
155
155
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
156
156
  python3 "$R/scripts/epic-manifest.py" \
157
157
  render-status "<epic>" --specs-dir "<specsDir>" --json
@@ -194,6 +194,23 @@ Invoke this block at the **very start** of a pipeline entry point — `forge-1-p
194
194
 
195
195
  **Record the branch.** After this block resolves, write the resulting branch name to the feature's `.pipeline-state.json` top-level `branch` field (create/update it when the state file is first written for this stage). Downstream stages and `forge-5-loop` read it to detect drift back onto the default branch.
196
196
 
197
+ ## Branch Reconciliation
198
+
199
+ The recorded `branch` is a **self-healing hint, not gospel.** A hosted environment (Claude.ai remote, cloud agents) can impose an arbitrary session branch (e.g. `claude/<slug>`) that Branch Setup silently records; the user may then move the work to the intended topic branch, leaving the recorded field stale. Every branch-aware mechanism (the `forge-5-loop` guard, `discover-feature`) keys off that field, so a stale value actively misleads — the loop would offer to switch you *back* to the imposed branch. Invoke this block from `forge-5-loop`'s pre-flight (and any stage that acts on the recorded branch) to reconcile deterministically. Skip if not a git repo or `branchPerFeature` is false.
200
+
201
+ ```bash
202
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
203
+ [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
204
+ python3 "$R/scripts/forge-session.py" reconcile-branch --feature "{feature}" --specs-dir "{specsDir}" --json
205
+ ```
206
+
207
+ Act on the emitted `action` (source of truth is where the state actually resolves, not the recorded field):
208
+ - **`adopt-current`** — you are on a non-default topic branch where the state resolves, and the recorded `branch` differs (a stale/imposed value). Write `newBranch` into the state `branch` field with a **visible one-line note** ("recorded branch was `{stateBranch}`; work is on `{currentBranch}` — updating to match") — never silently, and **never push the user back** to the recorded branch (offer that only as a plain alternative).
209
+ - **`warn-drift`** — you are on the **default** branch and the state records a topic branch. Via `AskUserQuestion`, strongly recommend creating/switching to `{branchPrefix}{feature}` (then record it), still allowing **proceed on the default branch**. Never hard-stop.
210
+ - **`none`** / **`not-resolved`** — nothing to do; proceed.
211
+
212
+ If the helper is unavailable (non-Claude host without the resolver), fall back to the manual check: current branch differs from recorded → adopt the current branch unless it is the default, in which case recommend creating `{branchPrefix}{feature}`.
213
+
197
214
  ## Git Commit Protocol
198
215
 
199
216
  When `gitCommitAfterStage` is true, follow this exact order to avoid state inconsistency.
@@ -57,7 +57,7 @@ placeholders the skill resolves before running the command, exactly as elsewhere
57
57
  **Close this stage with the Scripted Stage Exit** (contract: `references/stage-exit-protocol.md`; do not improvise a "Next steps" list). Run:
58
58
 
59
59
  ```bash
60
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
60
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
61
61
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
62
62
  python3 "$R/scripts/forge-session.py" stage-exit {stage-exit-args} --specs-dir "{specsDir}" --host claude
63
63
  ```
@@ -85,12 +85,16 @@ navigator):
85
85
 
86
86
  1. **Clean-room verify (require-clean).** Dispatch the clean-room `forge-verifier`
87
87
  subagent from this session in require-clean mode — the same path the navigator uses
88
- (`skills/forge-verify/SKILL.md`). It inherits none of this session's context, so no
89
- `/clear` is needed and only a compact digest returns. **Clean-room unavailable** (no
90
- `Agent` tool, `forge-verifier` not dispatchable, or a sentinel returned): do **not**
91
- run inline — leave verify **pending** so the navigator catch-up fires on a later
92
- Claude-host `/feature-forge:forge`, print the `verifyCommand` for the user to run,
93
- and continue to the NEXT-STEPS block.
88
+ (`skills/forge-verify/SKILL.md`). Dispatch it **synchronously and await its digest
89
+ inline** — do **not** run it in the background or announce it as "still running";
90
+ the digest and any fix decision must land in this session. It inherits none of this
91
+ session's context, so no `/clear` is needed and only a compact digest returns.
92
+ **Clean-room unavailable** (no `Agent` tool, `forge-verifier` not dispatchable) **or
93
+ a non-answer returned** (the verifier returned a placeholder / "still running" /
94
+ delegation message instead of a findings block): do **not** run inline and do **not**
95
+ silently accept the non-answer as a pass — leave verify **pending** so the navigator
96
+ catch-up fires on a later Claude-host `/feature-forge:forge`, print the
97
+ `verifyCommand` for the user to run, and continue to the NEXT-STEPS block.
94
98
  2. **Verify passed / no findings** → the fresh verify state is recorded by the
95
99
  clean-room run; continue to the NEXT-STEPS block.
96
100
  3. **Verify found findings** →
@@ -25,6 +25,7 @@ vocabulary defined in `00-core-definitions.md` §8. No free-form values are perm
25
25
  | `argument-hint` (top-level frontmatter key) | 10 `skills/*/SKILL.md` (all skills **except** `forge-init`) | `relocated` | Claude-specific vendor key (constraint C-2). Moved verbatim to `metadata.argument-hint` per REQ-VND-01 (see `02-frontmatter-purity-and-inventory.md` §2). Value byte-identical; `description` untouched. |
26
26
  | `${CLAUDE_PLUGIN_ROOT}` — canonical invocations + prose | 23 occurrences across 9 canonical surfaces: `skills/forge-0-epic/SKILL.md` (12), `skills/forge/SKILL.md` (3), `skills/forge-5-loop/SKILL.md` (1), `skills/forge-6-docs/SKILL.md` (1), `skills/forge-init/SKILL.md` (1), `skills/forge-verify/SKILL.md` (1), `skills/forge-verify/references/verification-checklists.md` (1), `references/shared-conventions.md` (2), `agents/forge-verifier.md` (1) | `routed-through-resolver` | Claude-only env var. Routed through the byte-identical bootstrap prelude + `scripts/forge-root.sh` per REQ-RES-03. Mechanics owned by `03-portable-root-resolver.md`; recorded here for audit completeness. |
27
27
  | `${CLAUDE_PLUGIN_ROOT}` — sanctioned residual | 1 occurrence in `scripts/forge-root.sh` (env-fallback, REQ-RES-02 step 3) | `preserved-as-spec-allowed` | The single sanctioned residual: the resolver's documented Claude-compat fallback (REQ-RES-03 / REQ-RES-05). Exempt from the residual-var scan (`00-core-definitions.md` §6 `RESIDUAL_VAR_EXEMPT`). |
28
+ | `${CLAUDE_PLUGIN_ROOT:-}` — bootstrap-prelude first-hint (Chunk 2b) | 1 occurrence per prelude across every canonical stamp site (the byte-pinned `BOOTSTRAP_PRELUDE`) | `preserved-as-spec-allowed` | The prelude's first resolver candidate — exact, glob-free root resolution on any Claude layout; expands to empty and is skipped when unset. Rule 3 allows it by stripping the byte-pinned prelude before its scan (detection is by `${CLAUDE_PLUGIN_ROOT` prefix, so the `:-}` default form is not an escape hatch elsewhere). `forge-agent-adapters-build` translates it to `${FEATURE_FORGE_ROOT:-}` in non-Claude bundles. |
28
29
  | `${CLAUDE_PLUGIN_ROOT}` — in `hooks/hooks.json` | 1 occurrence in `hooks/hooks.json` | `out-of-canon` | Non-canonical Claude artifact (REQ-VND-04). Not a canonical surface; exempt from the REQ-RES-03 scan. Left in place. |
29
30
  | `hooks/hooks.json` SessionStart wiring | 1 file (`hooks/hooks.json`) — Claude `SessionStart` → `bash ${CLAUDE_PLUGIN_ROOT}/scripts/session-check.sh` | `out-of-canon` | Claude-specific plugin hook wiring (REQ-VND-04, decision D3). Preserved + documented so `forge-agent-adapters-build` treats it as a Claude artifact, not portable canon. |
30
31
  | (contingency) any other vendor invocation directive | none found in the audit | — | REQ-VND-02 contingency did not fire (see Notes). If one is later surfaced, add a row with `removed` or `out-of-canon` per `02-frontmatter-purity-and-inventory.md` §3. |
@@ -8,7 +8,9 @@ root navigator:
8
8
  python3 forge-session.py context-usage [--config FILE] [--window N] \
9
9
  [--threshold F] [--json]
10
10
  python3 forge-session.py doctor [--specs-dir DIR] [--config FILE] [--json]
11
- python3 forge-session.py discover-feature NAME [--specs-dir DIR] [--json]
11
+ python3 forge-session.py discover-feature [NAME | --all] [--specs-dir DIR] [--json]
12
+ python3 forge-session.py reconcile-branch --feature F [--specs-dir DIR] \
13
+ [--config FILE] [--epic E] [--json]
12
14
  python3 forge-session.py stage-exit --feature F --stage S [--specs-dir DIR] \
13
15
  [--config FILE] [--epic E] [--next-feature N] [--host claude|generic] [--json]
14
16
 
@@ -135,6 +137,7 @@ class FeatureRow(TypedDict):
135
137
  verifyState: str
136
138
  autoVerify: bool
137
139
  autoFix: bool
140
+ verifyGate: str
138
141
 
139
142
 
140
143
  class UsageError(Exception):
@@ -357,6 +360,16 @@ def build_rows(specs_dir: Path, config: dict | None = None) -> list[FeatureRow]:
357
360
  "verifyState": vlabel,
358
361
  "autoVerify": effective_auto_verify,
359
362
  "autoFix": global_auto_fix and effective_auto_verify,
363
+ # Single resolved verify-gate classification (5b — one exit computation,
364
+ # mirroring stage-exit's `verifyGate`): the navigator reads this instead of
365
+ # re-deriving from verifyPending + autoVerify in prose. `auto` = the §2b
366
+ # catch-up runs it unattended; `standard` = the §3 gate (degrades to
367
+ # manual-print on a non-Claude host); `none` = nothing outstanding.
368
+ "verifyGate": (
369
+ "none" if not verify_pending
370
+ else "auto" if effective_auto_verify
371
+ else "standard"
372
+ ),
360
373
  })
361
374
  # Sort by updatedAt desc; rows without a parseable timestamp sort last.
362
375
  rows.sort(
@@ -659,11 +672,18 @@ def doctor_report(specs_dir: Path, config_path: Path) -> dict:
659
672
  # --show-current (not rev-parse HEAD) so an unborn branch (fresh repo,
660
673
  # no commits yet) still reports its name instead of failing.
661
674
  current_branch = _git_output(["branch", "--show-current"])
675
+ default_branch = _default_branch()
662
676
  rows = build_rows(specs_dir, config)
663
677
  features = []
664
678
  for row in rows:
665
679
  backlog = _backlog_path(config, row["name"], row["epic"], specs_dir)
666
680
  state_branch = row["branch"]
681
+ mismatch = bool(state_branch and current_branch and state_branch != current_branch)
682
+ # Classify a mismatch: on a topic branch it is adoptable (imposed/session-branch
683
+ # drift, Chunk 6); on the default branch it is real drift-back, only a warning.
684
+ branch_reconcile = None
685
+ if mismatch:
686
+ branch_reconcile = "warn-drift" if current_branch == default_branch else "adopt-current"
667
687
  features.append({
668
688
  "name": row["name"],
669
689
  "epic": row["epic"],
@@ -676,6 +696,7 @@ def doctor_report(specs_dir: Path, config_path: Path) -> dict:
676
696
  if state_branch and current_branch
677
697
  else None
678
698
  ),
699
+ "branchReconcile": branch_reconcile,
679
700
  "backlogPath": str(backlog),
680
701
  "backlogExists": backlog.is_file(),
681
702
  })
@@ -720,7 +741,12 @@ def _print_doctor(report: dict) -> None:
720
741
  label = feat["name"] + (f" [{feat['epic']}]" if feat["epic"] else "")
721
742
  branch = feat["stateBranch"] or "?"
722
743
  if feat["branchMatchesState"] is False:
723
- branch += " (MISMATCH vs current)"
744
+ if feat.get("branchReconcile") == "adopt-current":
745
+ branch += " (MISMATCH — reconcile: adopt current branch)"
746
+ elif feat.get("branchReconcile") == "warn-drift":
747
+ branch += " (MISMATCH — on default branch; create a topic branch)"
748
+ else:
749
+ branch += " (MISMATCH vs current)"
724
750
  backlog = "exists" if feat["backlogExists"] else "MISSING"
725
751
  print(
726
752
  f" - {label}: stage={feat['currentStage']} "
@@ -937,6 +963,193 @@ def _print_discover(payload: dict) -> None:
937
963
  print(f" switch: {cand['switchCommand']}")
938
964
 
939
965
 
966
+ def _all_state_paths_in_ref(ref: str, specs_rel: str) -> list[tuple[str, str]]:
967
+ """Every feature-shaped ``.pipeline-state.json`` in one ref as ``(path, feature)``.
968
+
969
+ The ``--all`` counterpart to ``_state_paths_in_ref``: same flat/nested bound
970
+ (``{specsDir}/{name}/…`` or ``{specsDir}/{epic}/{name}/…``) but for every
971
+ feature, not one named one.
972
+ """
973
+ listing = _git_output(["ls-tree", "-r", "--name-only", ref, "--", specs_rel])
974
+ if not listing:
975
+ return []
976
+ hits: list[tuple[str, str]] = []
977
+ prefix = specs_rel + "/"
978
+ for path in listing.splitlines():
979
+ if not path.startswith(prefix) or not path.endswith("/" + PIPELINE_STATE_FILENAME):
980
+ continue
981
+ segments = path[len(prefix):].split("/")
982
+ if len(segments) == 2: # [name, state-file] (flat)
983
+ hits.append((path, segments[0]))
984
+ elif len(segments) == 3: # [epic, name, state-file] (nested)
985
+ hits.append((path, segments[1]))
986
+ return hits
987
+
988
+
989
+ def discover_all(specs_dir: str) -> dict:
990
+ """Discover EVERY feature's pipeline state across all branches (read-only, Chunk 5c).
991
+
992
+ The empty-dashboard counterpart to ``discover-feature <name>``: enumerates every
993
+ feature-shaped state across local heads + remote-tracking refs and groups the
994
+ candidates by feature, so a fresh clone / default-branch session can see the whole
995
+ branch-scattered pipeline set instead of nothing. Never mutates anything.
996
+ """
997
+ if _git_output(["rev-parse", "--git-dir"]) is None:
998
+ return {"gitRepo": False, "currentBranch": None, "features": []}
999
+ current_branch = _git_output(["branch", "--show-current"])
1000
+ specs_rel = _specs_rel(specs_dir)
1001
+ refs = [(ref, date, False) for ref, date in _list_refs("refs/heads")]
1002
+ refs += [(ref, date, True) for ref, date in _list_refs("refs/remotes")]
1003
+
1004
+ by_feature: dict[str, list[dict]] = {}
1005
+ for ref, commit_date, is_remote in refs:
1006
+ branch = ref.split("/", 1)[1] if is_remote else ref
1007
+ if is_remote and (not branch or branch == "HEAD"):
1008
+ continue
1009
+ for path, feature in _all_state_paths_in_ref(ref, specs_rel):
1010
+ seen = by_feature.setdefault(feature, [])
1011
+ if any(c["branch"] == branch for c in seen):
1012
+ continue # a local head already yielded this branch's state
1013
+ state = _read_state_at_ref(ref, path)
1014
+ state_branch = state.get("branch")
1015
+ state_branch = state_branch if isinstance(state_branch, str) else None
1016
+ seen.append({
1017
+ "branch": branch,
1018
+ "remoteTracking": is_remote,
1019
+ "path": path,
1020
+ "stateBranch": state_branch,
1021
+ "stateBranchMatches": state_branch == branch,
1022
+ "currentStage": state.get("currentStage"),
1023
+ "pipelineStatus": state.get("pipelineStatus", "active"),
1024
+ "commitDate": commit_date or None,
1025
+ "isCurrentBranch": branch == current_branch,
1026
+ "switchCommand": f"git switch {branch}",
1027
+ })
1028
+
1029
+ def _rank(cand: dict) -> tuple:
1030
+ ts = _parse_ts(cand["commitDate"]) or datetime.min.replace(tzinfo=timezone.utc)
1031
+ return (not cand["stateBranchMatches"], cand["remoteTracking"], -ts.timestamp())
1032
+
1033
+ features = []
1034
+ for feature in sorted(by_feature):
1035
+ cands = sorted(by_feature[feature], key=_rank)
1036
+ features.append({"feature": feature, "candidates": cands})
1037
+ return {"gitRepo": True, "currentBranch": current_branch, "features": features}
1038
+
1039
+
1040
+ def _print_discover_all(payload: dict) -> None:
1041
+ """Human-readable ``discover-feature --all`` report."""
1042
+ if not payload["gitRepo"]:
1043
+ print("discover-feature --all: not a git repository — nothing to scan")
1044
+ return
1045
+ if not payload["features"]:
1046
+ print("discover-feature --all: no pipeline state found on any local or "
1047
+ "remote-tracking branch")
1048
+ return
1049
+ for feat in payload["features"]:
1050
+ print(f"{feat['feature']}:")
1051
+ for cand in feat["candidates"]:
1052
+ marks = []
1053
+ if cand["isCurrentBranch"]:
1054
+ marks.append("current branch")
1055
+ if cand["remoteTracking"]:
1056
+ marks.append("remote-tracking")
1057
+ if not cand["stateBranchMatches"] and cand["stateBranch"]:
1058
+ marks.append(f"state records branch {cand['stateBranch']}")
1059
+ suffix = f" ({'; '.join(marks)})" if marks else ""
1060
+ print(f" {cand['branch']}: stage={cand['currentStage'] or '?'} "
1061
+ f"status={cand['pipelineStatus']}{suffix}")
1062
+ if not cand["isCurrentBranch"]:
1063
+ print(f" switch: {cand['switchCommand']}")
1064
+
1065
+
1066
+ # --------------------------------------------------------------------------- #
1067
+ # Branch reconciliation (Chunk 6) — imposed/session-branch drift
1068
+ # --------------------------------------------------------------------------- #
1069
+
1070
+
1071
+ def _default_branch() -> str | None:
1072
+ """The repo's default branch: origin/HEAD target, else `main`/`master` if present."""
1073
+ ref = _git_output(["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"])
1074
+ if ref:
1075
+ return ref.rsplit("/", 1)[-1]
1076
+ for cand in ("main", "master"):
1077
+ if _git_output(["rev-parse", "--verify", "--quiet", f"refs/heads/{cand}"]) is not None:
1078
+ return cand
1079
+ return None
1080
+
1081
+
1082
+ def reconcile_branch(
1083
+ name: str, specs_dir: Path, config_path: Path, epic: str | None = None
1084
+ ) -> dict:
1085
+ """Decide whether a feature's recorded ``branch`` should adopt the current branch.
1086
+
1087
+ Read-only: it emits a decision; the caller performs any state write. A hosted
1088
+ environment (Claude.ai remote, cloud agents) imposes an arbitrary session branch
1089
+ that Branch Setup silently records; when the user moves to the intended branch the
1090
+ recorded ``branch`` goes stale and every branch-aware mechanism keys off it. This
1091
+ reconciler treats *where the state actually resolves* as the source of truth, with a
1092
+ default-branch guardrail so genuine drift-back-to-default is still surfaced, not
1093
+ silently adopted.
1094
+ """
1095
+ if _git_output(["rev-parse", "--git-dir"]) is None:
1096
+ return {"feature": name, "gitRepo": False, "reconcile": False,
1097
+ "action": "none", "reason": "not a git repository"}
1098
+ current = _git_output(["branch", "--show-current"])
1099
+ default = _default_branch()
1100
+ config = _load_config(config_path)
1101
+ row = next(
1102
+ (r for r in build_rows(specs_dir, config)
1103
+ if r["name"] == name and (epic is None or r["epic"] == epic)),
1104
+ None,
1105
+ )
1106
+ state_path = None
1107
+ if row is not None:
1108
+ parent = specs_dir / row["epic"] / name if row["epic"] else specs_dir / name
1109
+ state_path = str(parent / PIPELINE_STATE_FILENAME)
1110
+ base = {
1111
+ "feature": name,
1112
+ "gitRepo": True,
1113
+ "currentBranch": current,
1114
+ "defaultBranch": default,
1115
+ "stateBranch": row["branch"] if row else None,
1116
+ "resolvesOnCurrentBranch": row is not None,
1117
+ "statePath": state_path,
1118
+ "newBranch": None,
1119
+ }
1120
+ if current is None:
1121
+ return {**base, "reconcile": False, "action": "none",
1122
+ "reason": "no current branch (detached HEAD or unborn branch)"}
1123
+ if row is None:
1124
+ return {**base, "reconcile": False, "action": "not-resolved",
1125
+ "reason": "feature state does not resolve on the current branch — "
1126
+ "use discover-feature to locate it"}
1127
+ state_branch = base["stateBranch"]
1128
+ if state_branch == current:
1129
+ return {**base, "reconcile": False, "action": "none",
1130
+ "reason": "recorded branch already matches the current branch"}
1131
+ if current == default:
1132
+ return {**base, "reconcile": False, "action": "warn-drift",
1133
+ "reason": f"on the default branch ({default}); recording it would commit "
1134
+ "here — create/switch to a topic branch instead of reconciling"}
1135
+ detail = (f"recorded branch {state_branch!r} differs from the current topic branch"
1136
+ if state_branch else "no branch recorded")
1137
+ return {**base, "reconcile": True, "action": "adopt-current", "newBranch": current,
1138
+ "reason": f"{detail}; the feature state resolves here, so adopt the current branch"}
1139
+
1140
+
1141
+ def _print_reconcile(payload: dict) -> None:
1142
+ """Human-readable reconcile-branch report."""
1143
+ if not payload["gitRepo"]:
1144
+ print(f"reconcile-branch {payload['feature']}: not a git repository")
1145
+ return
1146
+ print(f"reconcile-branch {payload['feature']}: {payload['action']} — {payload['reason']}")
1147
+ print(f" current={payload['currentBranch']} recorded={payload['stateBranch'] or '(none)'} "
1148
+ f"default={payload['defaultBranch']}")
1149
+ if payload["reconcile"]:
1150
+ print(f" → write state branch := {payload['newBranch']} ({payload['statePath']})")
1151
+
1152
+
940
1153
  # --------------------------------------------------------------------------- #
941
1154
  # Scripted Stage Exit
942
1155
  # --------------------------------------------------------------------------- #
@@ -1230,10 +1443,23 @@ def main() -> int:
1230
1443
  p_disc = sub.add_parser(
1231
1444
  "discover-feature", help="Find a feature's pipeline state across all branches"
1232
1445
  )
1233
- p_disc.add_argument("name", help="Feature name to discover")
1446
+ p_disc.add_argument("name", nargs="?", default=None,
1447
+ help="Feature name to discover (omit with --all)")
1448
+ p_disc.add_argument("--all", action="store_true", dest="discover_all",
1449
+ help="Discover every feature across all branches (empty-dashboard)")
1234
1450
  p_disc.add_argument("--specs-dir", default="./specs", help="Specs directory")
1235
1451
  p_disc.add_argument("--json", action="store_true", dest="json_output")
1236
1452
 
1453
+ p_recon = sub.add_parser(
1454
+ "reconcile-branch",
1455
+ help="Decide whether a feature's recorded branch should adopt the current branch",
1456
+ )
1457
+ p_recon.add_argument("--feature", required=True, help="Feature name")
1458
+ p_recon.add_argument("--specs-dir", default="./specs", help="Specs directory")
1459
+ p_recon.add_argument("--config", default="./forge.config.json", help="forge.config.json path")
1460
+ p_recon.add_argument("--epic", default=None, help="Epic name for a nested member")
1461
+ p_recon.add_argument("--json", action="store_true", dest="json_output")
1462
+
1237
1463
  p_exit = sub.add_parser(
1238
1464
  "stage-exit", help="Emit the Scripted Stage Exit directives + NEXT-STEPS block"
1239
1465
  )
@@ -1290,11 +1516,28 @@ def main() -> int:
1290
1516
  return 0
1291
1517
 
1292
1518
  if args.cmd == "discover-feature":
1293
- payload = discover_feature(args.name, args.specs_dir)
1519
+ if args.discover_all:
1520
+ payload = discover_all(args.specs_dir)
1521
+ printer = _print_discover_all
1522
+ elif args.name:
1523
+ payload = discover_feature(args.name, args.specs_dir)
1524
+ printer = _print_discover
1525
+ else:
1526
+ parser.error("discover-feature requires a NAME or --all")
1527
+ if args.json_output:
1528
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
1529
+ else:
1530
+ printer(payload)
1531
+ return 0
1532
+
1533
+ if args.cmd == "reconcile-branch":
1534
+ payload = reconcile_branch(
1535
+ args.feature, Path(args.specs_dir), Path(args.config), args.epic
1536
+ )
1294
1537
  if args.json_output:
1295
1538
  print(json.dumps(payload, indent=2, ensure_ascii=False))
1296
1539
  else:
1297
- _print_discover(payload)
1540
+ _print_reconcile(payload)
1298
1541
  return 0
1299
1542
 
1300
1543
  if args.cmd == "stage-exit":
@@ -29,7 +29,7 @@ For pipeline architecture details, read `references/process-overview.md`.
29
29
 
30
30
  1. **Epics first.** Identify epic directories as any `{specsDir}/*/` that directly contains an `epic-manifest.json` **and no `.pipeline-state.json` of its own** (an epic root is never itself a feature). For each epic, run:
31
31
  ```bash
32
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
32
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
33
33
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
34
34
  python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specsDir}" --json
35
35
  ```
@@ -37,14 +37,14 @@ python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specs
37
37
  2. **Standalone features below.** Scan the remaining `{specsDir}/*/` that directly contain a `.pipeline-state.json` **without** an `epic` back-pointer. A nested member's `.pipeline-state.json` is **attributed to its epic (Tier 1), never listed as a standalone feature**.
38
38
  - **Rank by recency.** Run the recency ranker so the most-recently-touched active feature is the default — the user rarely has to type a name (especially on mobile after a `/clear`):
39
39
  ```bash
40
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
40
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
41
41
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
42
42
  python3 "$R/scripts/forge-session.py" rank-features --specs-dir "{specsDir}" --json
43
43
  ```
44
- This returns `{active: [...], counts: {...}}` with active features sorted by `updatedAt` **descending** (row 0 is the most recent). Each row carries `currentStage`, `nextStage`, `nextCommand`, `verifyPending`, `verifyCommand`, `verifyStage`, `verifyState` (`fresh`/`stale`/`failing`/`never`/`none`), `autoVerify` (the effective per-stage setting), and `autoFix` (the single source of stage order). A top-level `invalidAutoVerifyKeys` array appears when `forge.config.json` has `autoVerifyStages` keys outside the five verify-capable stages — surface it as a one-line warning. The `active` list excludes nested epic members surfaced in Tier 1 — but the ranker scans them too, so ignore rows whose `epic` is non-null here (they belong to the epic rollup).
44
+ This returns `{active: [...], counts: {...}}` with active features sorted by `updatedAt` **descending** (row 0 is the most recent). Each row carries `currentStage`, `nextStage`, `nextCommand`, `verifyPending`, `verifyCommand`, `verifyStage`, `verifyState` (`fresh`/`stale`/`failing`/`never`/`none`), `autoVerify` (the effective per-stage setting), `autoFix` (the single source of stage order), and `verifyGate` (`none`/`auto`/`standard` — the single resolved verify-gate, computed once by the ranker; read it rather than re-deriving from `verifyPending`+`autoVerify`). A top-level `invalidAutoVerifyKeys` array appears when `forge.config.json` has `autoVerifyStages` keys outside the five verify-capable stages — surface it as a one-line warning. The `active` list excludes nested epic members surfaced in Tier 1 — but the ranker scans them too, so ignore rows whose `epic` is non-null here (they belong to the epic rollup).
45
45
  - **Pick the feature:** if exactly one active standalone pipeline exists, show its dashboard. If multiple exist, use `AskUserQuestion` — **list the most-recently-updated first, labeled `(recommended)`**, each option's description showing its `currentStage` and a relative age ("updated 2h ago"). Always include a free-form escape ("A different feature / something else") so the user is never boxed in. Then render the chosen feature's dashboard.
46
46
 
47
- If no epics and no standalone features exist, say: "No active feature pipelines found. Start one with `/feature-forge:forge-1-prd <feature-name>` or group several with `/feature-forge:forge-0-epic <epic-name>`."
47
+ If no epics and no standalone features exist **on the current branch**, do not conclude the pipeline is empty yet — the state may live on topic branches (a fresh clone / default-branch session sees nothing on disk). Run `discover-feature --all` (via the standard prelude: `python3 "$R/scripts/forge-session.py" discover-feature --all --specs-dir "{specsDir}" --json`). If it surfaces features, list them with their branches and offer to switch (explicit accept + clean tree only, per the Feature Directory Resolution block), then render that feature's dashboard. Only when `--all` also returns nothing, say: "No active feature pipelines found. Start one with `/feature-forge:forge-1-prd <feature-name>` or group several with `/feature-forge:forge-0-epic <epic-name>`." Never fabricate a dashboard from memory of earlier sessions.
48
48
 
49
49
  The feature name must be a single kebab-case token. If the user provides multiple words (e.g., "user auth flow"), convert to kebab-case: `user-auth-flow`.
50
50
 
@@ -89,11 +89,11 @@ Use these status indicators:
89
89
 
90
90
  After rendering a **per-feature** dashboard for an **active** pipeline (skip this for paused/abandoned pipelines and for the Epic Dashboard), don't stop at a text suggestion — actively offer to start the next stage. This removes the copy-paste-after-`/clear` chore that makes long, multi-stage runs painful (especially on mobile).
91
91
 
92
- **1. Read the next step.** From the `rank-features --json` output (above), find this feature's row and read its `nextStage`, `nextCommand`, `verifyPending`, `verifyCommand`, `verifyStage`, `verifyState`, `autoVerify`, and `autoFix`. If the feature is not in the `active` list (paused/abandoned), or `nextStage` is `null` (every production stage complete), skip the drive prompt — instead congratulate the user and, if `forge-6-docs` has not run, offer it; otherwise note the pipeline is complete. If the payload has a non-empty `invalidAutoVerifyKeys`, print a one-line warning first (e.g. "⚠️ forge.config.json `autoVerifyStages` has unknown keys: … — they are ignored; fix the typo").
92
+ **1. Read the next step.** From the `rank-features --json` output (above), find this feature's row and read its `nextStage`, `nextCommand`, `verifyPending`, `verifyCommand`, `verifyStage`, `verifyState`, `autoVerify`, `autoFix`, and `verifyGate` (the resolved gate: `none` → no verify action; `auto` → the §2b catch-up; `standard` → the §3 gate). If the feature is not in the `active` list (paused/abandoned), or `nextStage` is `null` (every production stage complete), skip the drive prompt — instead congratulate the user and, if `forge-6-docs` has not run, offer it; otherwise note the pipeline is complete. If the payload has a non-empty `invalidAutoVerifyKeys`, print a one-line warning first (e.g. "⚠️ forge.config.json `autoVerifyStages` has unknown keys: … — they are ignored; fix the typo").
93
93
 
94
94
  **2. Check the context window.** Run the context-usage helper so you can advise whether to continue here or start the next stage in a fresh session:
95
95
  ```bash
96
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
96
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
97
97
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
98
98
  python3 "$R/scripts/forge-session.py" context-usage --json
99
99
  ```
@@ -110,7 +110,7 @@ python3 "$R/scripts/forge-session.py" context-usage --json
110
110
  - **`autoFix` is false (default), or preconditions failed** → present a **compact findings digest** as text, then `AskUserQuestion`: *Apply fixes now (forge-fix)* / *Review findings* / *Skip & advance*.
111
111
  - **`autoVerify` is false/unconfigured** → do not auto-run; use the advance gate in step 3, which gains the folded opt-in verify options ("Verify `{stage}` now" and "Verify `{stage}` now + enable auto-verify going forward").
112
112
 
113
- **3. Offer the next step via `AskUserQuestion`.** (Reached when auto-verify did not fully resolve the step, or `verifyPending` is false.) Output the dashboard + a one-line context note as text, then ask (per the Decision Support protocol in `references/shared-conventions.md`). This is the same **Stage Exit Protocol** the stage skills stamp (`references/stage-exit-protocol.md`) — a clean session at the boundary is recommended on its own merits, and `overThreshold` only modulates *how emphatically*, never *whether*. Options, in this order:
113
+ **3. Offer the next step via `AskUserQuestion`.** (Reached when auto-verify did not fully resolve the step, or `verifyPending` is false.) Output the dashboard + a one-line context note as text, then ask (per the Decision Support protocol in `references/shared-conventions.md`). **Present this gate exactly once and act only on the option the user chooses — never also narrate the not-taken branch.** Concretely: do not print the "start in a clean session" recommendation as prose and *then* auto-invoke the next stage in the same session (or the reverse) — the `AskUserQuestion` answer is the single decision, and only the chosen path's action follows. This is the same **Stage Exit Protocol** the stage skills stamp (`references/stage-exit-protocol.md`) — a clean session at the boundary is recommended on its own merits, and `overThreshold` only modulates *how emphatically*, never *whether*. Options, in this order:
114
114
  - **Start `{nextStage}` in a clean session** — **recommended, unconditionally**, at every boundary. A clean session is the right default for the next stage on its own merits, independent of window size. The work survives a clear because all state is on disk: instruct the user to `/clear`, then re-run `/feature-forge:forge {feature}` (or run `{nextCommand}` directly) in the fresh session. Note plainly that you cannot `/clear` for them. **When `overThreshold` is true**, strengthen this wording (the window is also genuinely full) and add mid-stage compaction advice; **when the window is healthy**, still recommend the clean session — `overThreshold` is a *secondary, additive* modifier here, not the clear on/off switch.
115
115
  - **Continue in this session** — the always-available alternative, reasonable when the window is nearly empty and the user would rather not clear. Not the default.
116
116
  - **Verify `{stage}` now** — include **only when `verifyPending` is true and `autoVerify` is false**, and the clean-room path is available. **Recommended**, since verify is clean-room and rarely worth skipping. Runs verify now (require-clean mode); **does not** change config.
@@ -133,7 +133,7 @@ This applies whether the feature was named explicitly (`/feature-forge:forge {fe
133
133
  When the named argument is an epic (`{specsDir}/{name}/epic-manifest.json` exists), render the epic dashboard instead of a per-feature one. Run:
134
134
 
135
135
  ```bash
136
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
136
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
137
137
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
138
138
  python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specsDir}" --json
139
139
  ```
@@ -204,7 +204,7 @@ Support these sub-commands for pipeline lifecycle management:
204
204
 
205
205
  - Set the manifest's top-level `status` (`paused` / `active` / `abandoned`) via the helper's `set-status` mutator — an atomic write that also bumps `updatedAt`:
206
206
  ```bash
207
- R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
207
+ R="$(bash -c 'for d in "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
208
208
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
209
209
  python3 "$R/scripts/epic-manifest.py" set-status "{epic}" --status paused --specs-dir "{specsDir}"
210
210
  ```