@garygentry/feature-forge 0.2.3 → 0.2.4

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 (149) hide show
  1. package/README.md +6 -1
  2. package/adapters/GENERATION-REPORT.md +5 -1
  3. package/adapters/claude/references/forge-config-schema.json +25 -3
  4. package/adapters/claude/references/pipeline-state-schema.json +3 -2
  5. package/adapters/claude/references/portable-root.md +2 -2
  6. package/adapters/claude/references/process-overview.md +10 -0
  7. package/adapters/claude/references/shared-conventions.md +14 -9
  8. package/adapters/claude/references/stage-exit-protocol.md +99 -0
  9. package/adapters/claude/scripts/epic-manifest.py +10 -0
  10. package/adapters/claude/scripts/forge-bootstrap.py +94 -16
  11. package/adapters/claude/scripts/forge-init.sh +7 -1
  12. package/adapters/claude/scripts/forge-session.py +175 -30
  13. package/adapters/claude/skills/forge/SKILL.md +28 -14
  14. package/adapters/claude/skills/forge-0-epic/SKILL.md +20 -15
  15. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +6 -4
  16. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  17. package/adapters/claude/skills/forge-1-prd/SKILL.md +14 -4
  18. package/adapters/claude/skills/forge-2-tech/SKILL.md +14 -3
  19. package/adapters/claude/skills/forge-3-specs/SKILL.md +14 -3
  20. package/adapters/claude/skills/forge-4-backlog/SKILL.md +16 -5
  21. package/adapters/claude/skills/forge-5-loop/SKILL.md +19 -21
  22. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +10 -5
  23. package/adapters/claude/skills/forge-6-docs/SKILL.md +6 -6
  24. package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
  25. package/adapters/claude/skills/forge-fix/SKILL.md +27 -6
  26. package/adapters/claude/skills/forge-guide/SKILL.md +179 -0
  27. package/adapters/claude/skills/forge-init/SKILL.md +28 -1
  28. package/adapters/claude/skills/forge-verify/SKILL.md +46 -15
  29. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
  30. package/adapters/codex/references/forge-config-schema.json +25 -3
  31. package/adapters/codex/references/pipeline-state-schema.json +3 -2
  32. package/adapters/codex/references/portable-root.md +2 -2
  33. package/adapters/codex/references/process-overview.md +10 -0
  34. package/adapters/codex/references/shared-conventions.md +14 -9
  35. package/adapters/codex/references/stage-exit-protocol.md +99 -0
  36. package/adapters/codex/scripts/epic-manifest.py +10 -0
  37. package/adapters/codex/scripts/forge-bootstrap.py +94 -16
  38. package/adapters/codex/scripts/forge-init.sh +7 -1
  39. package/adapters/codex/scripts/forge-session.py +175 -30
  40. package/adapters/codex/skills/forge/SKILL.md +33 -19
  41. package/adapters/codex/skills/forge-0-epic/SKILL.md +21 -16
  42. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +6 -4
  43. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  44. package/adapters/codex/skills/forge-1-prd/SKILL.md +14 -4
  45. package/adapters/codex/skills/forge-2-tech/SKILL.md +15 -4
  46. package/adapters/codex/skills/forge-3-specs/SKILL.md +14 -3
  47. package/adapters/codex/skills/forge-4-backlog/SKILL.md +16 -5
  48. package/adapters/codex/skills/forge-5-loop/SKILL.md +21 -23
  49. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +10 -5
  50. package/adapters/codex/skills/forge-6-docs/SKILL.md +6 -6
  51. package/adapters/codex/skills/forge-bootstrap/SKILL.md +4 -4
  52. package/adapters/codex/skills/forge-fix/SKILL.md +27 -6
  53. package/adapters/codex/skills/forge-guide/SKILL.md +188 -0
  54. package/adapters/codex/skills/forge-init/SKILL.md +28 -1
  55. package/adapters/codex/skills/forge-verify/SKILL.md +45 -14
  56. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
  57. package/adapters/copilot/references/forge-config-schema.json +25 -3
  58. package/adapters/copilot/references/pipeline-state-schema.json +3 -2
  59. package/adapters/copilot/references/portable-root.md +2 -2
  60. package/adapters/copilot/references/process-overview.md +10 -0
  61. package/adapters/copilot/references/shared-conventions.md +14 -9
  62. package/adapters/copilot/references/stage-exit-protocol.md +99 -0
  63. package/adapters/copilot/scripts/epic-manifest.py +10 -0
  64. package/adapters/copilot/scripts/forge-bootstrap.py +94 -16
  65. package/adapters/copilot/scripts/forge-init.sh +7 -1
  66. package/adapters/copilot/scripts/forge-session.py +175 -30
  67. package/adapters/copilot/skills/forge/forge.md +33 -19
  68. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +21 -16
  69. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +6 -4
  70. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  71. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +14 -4
  72. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +15 -4
  73. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +14 -3
  74. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +16 -5
  75. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +21 -23
  76. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +10 -5
  77. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +6 -6
  78. package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +4 -4
  79. package/adapters/copilot/skills/forge-fix/forge-fix.md +27 -6
  80. package/adapters/copilot/skills/forge-guide/forge-guide.md +188 -0
  81. package/adapters/copilot/skills/forge-init/forge-init.md +28 -1
  82. package/adapters/copilot/skills/forge-verify/forge-verify.md +45 -14
  83. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
  84. package/adapters/cursor/references/forge-config-schema.json +25 -3
  85. package/adapters/cursor/references/pipeline-state-schema.json +3 -2
  86. package/adapters/cursor/references/portable-root.md +2 -2
  87. package/adapters/cursor/references/process-overview.md +10 -0
  88. package/adapters/cursor/references/shared-conventions.md +14 -9
  89. package/adapters/cursor/references/stage-exit-protocol.md +99 -0
  90. package/adapters/cursor/scripts/epic-manifest.py +10 -0
  91. package/adapters/cursor/scripts/forge-bootstrap.py +94 -16
  92. package/adapters/cursor/scripts/forge-init.sh +7 -1
  93. package/adapters/cursor/scripts/forge-session.py +175 -30
  94. package/adapters/cursor/skills/forge/forge.mdc +33 -19
  95. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +21 -16
  96. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +6 -4
  97. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  98. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +14 -4
  99. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +15 -4
  100. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +14 -3
  101. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +16 -5
  102. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +21 -23
  103. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +10 -5
  104. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +6 -6
  105. package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +4 -4
  106. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +27 -6
  107. package/adapters/cursor/skills/forge-guide/forge-guide.mdc +189 -0
  108. package/adapters/cursor/skills/forge-init/forge-init.mdc +28 -1
  109. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +45 -14
  110. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
  111. package/adapters/gemini/gemini-extension.json +4 -0
  112. package/adapters/gemini/references/forge-config-schema.json +25 -3
  113. package/adapters/gemini/references/pipeline-state-schema.json +3 -2
  114. package/adapters/gemini/references/portable-root.md +2 -2
  115. package/adapters/gemini/references/process-overview.md +10 -0
  116. package/adapters/gemini/references/shared-conventions.md +14 -9
  117. package/adapters/gemini/references/stage-exit-protocol.md +99 -0
  118. package/adapters/gemini/scripts/epic-manifest.py +10 -0
  119. package/adapters/gemini/scripts/forge-bootstrap.py +94 -16
  120. package/adapters/gemini/scripts/forge-init.sh +7 -1
  121. package/adapters/gemini/scripts/forge-session.py +175 -30
  122. package/adapters/gemini/skills/forge/forge.md +33 -19
  123. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +21 -16
  124. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +6 -4
  125. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  126. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +14 -4
  127. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +15 -4
  128. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +14 -3
  129. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +16 -5
  130. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +21 -23
  131. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +10 -5
  132. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +6 -6
  133. package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +4 -4
  134. package/adapters/gemini/skills/forge-fix/forge-fix.md +27 -6
  135. package/adapters/gemini/skills/forge-guide/forge-guide.md +188 -0
  136. package/adapters/gemini/skills/forge-init/forge-init.md +28 -1
  137. package/adapters/gemini/skills/forge-verify/forge-verify.md +45 -14
  138. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
  139. package/dist/apply.js +34 -8
  140. package/dist/cli.js +40 -4
  141. package/dist/fsutil.d.ts +0 -12
  142. package/dist/fsutil.js +10 -1
  143. package/dist/manifest.d.ts +1 -1
  144. package/dist/plan.js +22 -2
  145. package/dist/rauf.d.ts +4 -4
  146. package/dist/rauf.js +3 -3
  147. package/dist/report.js +1 -1
  148. package/dist/types.d.ts +1 -1
  149. package/package.json +1 -1
@@ -0,0 +1,188 @@
1
+ ---
2
+ # GENERATED — DO NOT EDIT. Source: skills/forge-guide/SKILL.md. Regenerate: python3 scripts/build-adapters.py
3
+ name: forge-guide
4
+ description: Explain what feature-forge is, when to use it, how to configure it, and its best practices — advisory guidance, not stage execution. Use when the user or another agent asks what feature-forge is, whether/when to adopt it, how the pipeline works conceptually, how to set up or configure forge.config.json, or for usage tips and best practices. Do NOT trigger to RUN a pipeline stage (use forge-1-prd … forge-6-docs), to show a specific feature's status (use forge), or for general software questions unrelated to feature-forge.
5
+ ---
6
+
7
+ # Feature Forge — Usage & Best-Practices Guide
8
+
9
+ You are the **guide** for feature-forge: an advisor, not an operator. Your job is to
10
+ explain *what* forge is, *when* to use it, *how* to configure it, and *what the best
11
+ practices are* — in plain language, grounded in the repo's own docs. You do **not**
12
+ run pipeline stages; when the user is ready to act, point them at the right skill.
13
+
14
+ ## How to answer
15
+
16
+ 1. Identify the topic (the argument, or infer from the question).
17
+ 2. **Ground yourself in the canonical source before answering** — read the mapped
18
+ reference file(s) below rather than answering from memory. These are the source
19
+ of truth and stay current as the pipeline evolves.
20
+ 3. Answer concisely, then end with the concrete next command (`/feature-forge:forge-*`)
21
+ or doc pointer the user should go to.
22
+
23
+ | Topic | Read first |
24
+ |-------|-----------|
25
+ | Pipeline architecture, stage-by-stage flow | `references/process-overview.md` |
26
+ | Cross-stage conventions (naming, state, git, branch, epic injection) | `references/shared-conventions.md` |
27
+ | Config keys + defaults | `references/forge-config-schema.json` |
28
+ | Stack detection / language profiles | `references/stack-resolution.md`, `references/stacks/*.md` |
29
+ | Loop runner interface, signals & version gate | `references/ralph-loop-contract.md` |
30
+ | Deep dives, glossary, troubleshooting | `references/process-overview.md`, `references/shared-conventions.md` |
31
+
32
+ Those `references/` files are the guaranteed grounding path — they ship in every install.
33
+ The project's `README.md`, `COMPATIBILITY.md`, and the hosted docs site are richer but are
34
+ **not** part of an installed bundle, so treat them as optional enrichment (offer the docs-site
35
+ URL to a human) and never block an answer on opening them — fall back to the `references/`
36
+ files and your own knowledge.
37
+
38
+ Do NOT actually invoke stage skills or write files — this skill only explains and directs.
39
+
40
+ ## What feature-forge is
41
+
42
+ A **feature development pipeline** that refines a vague idea into shipped code through
43
+ discrete, auditable stages — like a compiler for features. Each stage narrows scope and
44
+ adds structure, reading the previous stage's artifacts as standalone contracts:
45
+
46
+ - **PRD** — *what* to build (requirements only, no technology).
47
+ - **Tech spec** — *how* to build it (design, grounded in the codebase).
48
+ - **Implementation specs** — build-ready detail (types, signatures, contracts, tests).
49
+ - **Backlog** — self-contained work items for autonomous execution.
50
+ - **Loop** — a fresh-context runner implements each item, tests, and commits.
51
+ - **Docs** — architecture reference generated from the real implementation.
52
+
53
+ `REQ-XXX-NN` requirement IDs form a traceability spine from PRD through implementation.
54
+ **Verification gates** are available after any stage and run clean-room in a fresh subagent.
55
+
56
+ ## When to use it — and when not
57
+
58
+ **Use forge when:** you have a well-defined feature or small epic to ship; you want
59
+ requirements captured cleanly before coding; you value traceability, thorough spec
60
+ verification, and autonomous loop execution with fresh context per item; you want
61
+ reproducible, auditable artifacts.
62
+
63
+ **Skip forge when:** the work is an exploratory spike with still-fluid requirements
64
+ (though Stage 1's interview can help clarify them); it's a one-line bug fix or trivial
65
+ patch where pipeline overhead isn't justified; or you're only extending mature code
66
+ along established patterns, where new specs become noise.
67
+
68
+ **Anti-patterns to warn against:** skipping the PRD and jumping to tech spec (the value
69
+ comes from separating *what* from *how*); treating specs as living contracts (they're
70
+ pre-implementation — drop an `AGENTS.md`/`CLAUDE.md` in the specs dir telling agents to
71
+ ignore drift); forcing an epic for a single feature; relying on conversation memory to
72
+ carry context across stages instead of reading upstream artifacts.
73
+
74
+ ## The pipeline at a glance
75
+
76
+ | Stage | Skill | Produces |
77
+ |-------|-------|----------|
78
+ | 0 (optional) | `forge-0-epic` | `epic-manifest.json` + `EPIC.md` (members, deps, `exposes`/`consumes` contracts) |
79
+ | 1 | `forge-1-prd` | `PRD.md` with `REQ-*` IDs |
80
+ | 2 | `forge-2-tech` | `tech-spec.md`; detects stack + test/typecheck commands |
81
+ | 3 | `forge-3-specs` | numbered spec suite + `TRACEABILITY.md` |
82
+ | 4 | `forge-4-backlog` | `backlog.json` (self-contained items) |
83
+ | 5 | `forge-5-loop` | implemented code, tested + committed per item |
84
+ | 6 | `forge-6-docs` | architecture docs from the real code |
85
+ | any | `forge-verify` → `forge-fix` | findings report → applied fixes |
86
+ | any | `forge` | status dashboard / navigator |
87
+
88
+ Drive the whole thing with the **navigator**: `/feature-forge:forge <feature>` shows the
89
+ current stage and offers the next; with `autoInvokeNextStage` it launches it directly.
90
+
91
+ ## Setup & configuration
92
+
93
+ **First-time setup:** `/feature-forge:forge-init` (existing repo) creates `forge.config.json`
94
+ with defaults. `/feature-forge:forge-bootstrap` scaffolds a *greenfield* (empty) repo to a
95
+ green baseline. On non-Claude agents, install via `npx @garygentry/feature-forge install`.
96
+
97
+ **Key `forge.config.json` knobs** (authoritative list: `references/forge-config-schema.json`):
98
+
99
+ - **Paths** — `specsDir` (`./specs`), `docsDir` (`./docs/architecture`), `backlogDir`.
100
+ - **Git** — `gitCommitAfterStage` (true), `commitPrefix` (`forge`); commits use a two-commit
101
+ protocol so the stage's commit hash is recorded in state without `--amend`.
102
+ - **Branch** — `branchPerFeature` (true), `branchPrefix` (`forge/`): isolate each feature.
103
+ - **Stack** — `stack`, `typeCheckCommand`, `testCommand`: null until Stage 2 auto-detects them.
104
+ - **Context** — `contextWindowTokens`, `contextWarnThreshold` (0.7): the navigator warns to
105
+ clear your session / start a fresh session past this fullness. On 1M-context models set `contextWindowTokens` explicitly.
106
+ - **Verification** — `autoVerify` (false), `autoVerifyStages`, `autoFix` (false).
107
+ - **Stage flow** — `autoInvokeNextStage` (true on Claude, print-only elsewhere).
108
+ - **Loop** — `loopRunner` block (binary, command templates, version gate, agent selection);
109
+ defaults to **rauf** when absent. `workspaces` supports monorepos.
110
+
111
+ ## Verification gates
112
+
113
+ `forge-verify <feature>` dispatches the read-only `forge-verifier` subagent to find gaps,
114
+ inconsistencies, and quality issues; it writes a findings doc, and `forge-fix` applies them.
115
+ Because verification runs in a **fresh subagent**, it's clean-room by construction — it never
116
+ needs a clear your session / start a fresh session, and it's safe to automate with `autoVerify: true` (fixing stays human-gated
117
+ unless `autoFix: true`). **Always verify before Stage 5 (the loop)** — catching errors in
118
+ specs/backlog is far cheaper than mid-loop. Verifying after PRD and after backlog is also
119
+ recommended. A findings pass is fresh only while the artifact `version` matches what was
120
+ verified; revise upstream and downstream re-verifies.
121
+
122
+ ## Context management
123
+
124
+ Each stage reads upstream artifacts as standalone contracts, so you can (and usually should)
125
+ clear your session / start a fresh session between them:
126
+
127
+ - **Clear** between PRD → tech, tech → specs, specs → backlog, backlog → loop.
128
+ - **Stay warm** mid-interview (PRD, tech spec) — the interview needs a continuous thread.
129
+ - **No clear needed** for any → verify (runs in a fresh subagent).
130
+
131
+ The navigator warns when the session passes `contextWarnThreshold` (default 70% full).
132
+
133
+ ## Epics (large changes)
134
+
135
+ Use Stage 0 only when a change naturally splits into **several interdependent features** that
136
+ must agree on interfaces. `forge-0-epic` produces `epic-manifest.json` + `EPIC.md` with a
137
+ per-member charter, `exposes`/`consumes` contracts, and `dependsOn` edges. Each member then
138
+ runs the normal pipeline with epic context injected. At Stage 5 a **dependency gate** warns if
139
+ a member's dependencies are unmet. Epic support is purely additive — single-feature flows are
140
+ unchanged. Re-run `forge-0-epic` on an existing epic to enter edit mode.
141
+
142
+ ## The loop
143
+
144
+ Stage 5 runs a configurable runner (**rauf** by default) that gives each backlog item a fresh
145
+ agent session — implement → run the verification command → commit on pass. This is why items
146
+ must be truly **self-contained**; context bleed breaks the model. Per-item signals:
147
+
148
+ - `RAUF_DONE` — item passed; loop continues.
149
+ - `RAUF_BLOCKED` — missing dependency / unclear requirement; set aside, loop continues others.
150
+ - `RAUF_NEEDS_HUMAN` — decision or secret needed; set aside, loop continues.
151
+
152
+ The loop doesn't pause on blocked items. Supply what's missing, then `rauf resume <path>` to
153
+ retry set-aside items. The runner refuses to start with a **dirty working tree** and enforces
154
+ a minimum runner version (the version gate is described in `references/ralph-loop-contract.md`).
155
+
156
+ ## Best practices & gotchas
157
+
158
+ - Feature name is **required** for every stage command — never guess or infer it.
159
+ - Verify **before the loop**; a bad spec is cheap to fix now, expensive mid-loop.
160
+ - Keep backlog items self-contained — the loop has zero memory across items.
161
+ - Let stages commit for you; don't hand-edit `.pipeline-state.json` or backlog status.
162
+ - Re-running an upstream stage marks downstream stages **stale** — re-run them rather than
163
+ reaching for `--force`, which skips prerequisite checks and should be rare.
164
+ - Specs are pre-implementation artifacts, not living docs — don't cite them from generated code.
165
+ - Use the navigator (`/feature-forge:forge <feature>`) to orient; use `forge-verify` to inspect.
166
+
167
+ ## Troubleshooting starters
168
+
169
+ - **Stage 5 won't start:** backlog exists and is verified? runner installed and ≥ min version?
170
+ working tree clean? See `references/ralph-loop-contract.md` for the runner contract and version gate.
171
+ - **Loop stopped mid-run:** check the signal — `BLOCKED`/`NEEDS_HUMAN` items are set aside, not
172
+ failures; the loop keeps going.
173
+ - **Downstream flagged stale:** an upstream stage was revised; re-run the downstream stage.
174
+ - **Where am I?** `/feature-forge:forge <feature>` renders the full pipeline status.
175
+
176
+ For anything deeper, ground yourself in `references/process-overview.md` and
177
+ `references/shared-conventions.md`, and point the *user* at the hosted docs site —
178
+ <https://garygentry.github.io/feature-forge/> — for the full guides and glossary.
179
+
180
+ ---
181
+
182
+ ## Host execution notes
183
+
184
+ This skill was authored Claude-first; the body above refers to "the host's question mechanism", "the host's subagent mechanism", and "the host's background-execution mechanism". Use your runtime's equivalent for each — and if your runtime has no such tool:
185
+
186
+ - **User input:** ask the question directly and wait for the answer before proceeding. Do not skip a required question or assume an answer.
187
+ - **Subagents:** if your host cannot dispatch the named custom agent, run that step inline yourself.
188
+ - **Background / monitoring:** run long-lived commands in the foreground (or your host's background facility) and report progress as it arrives.
@@ -9,7 +9,7 @@ description: Initialize feature-forge configuration in the current project. Use
9
9
  Run the initialization script to create `forge.config.json` with default settings:
10
10
 
11
11
  ```bash
12
- R="$(for d in "$HOME"/.claude/skills/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)"
12
+ R="$(bash -c 'for d in "$HOME"/.claude/skills/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')"
13
13
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
14
14
  bash "$R/scripts/forge-init.sh"
15
15
  ```
@@ -26,9 +26,36 @@ After initialization, the config file will contain defaults for:
26
26
  - `autoInvokeNextStage`: `true` (the navigator auto-starts the next stage after you confirm; set `false` to only print the command)
27
27
  - `contextWindowTokens`: `null` (the navigator infers the context window; set to your model's window, e.g. `1000000` for a 1M-context model, for accurate context-usage advice)
28
28
  - `contextWarnThreshold`: `0.7` (fraction of the window past which the navigator suggests a clean session)
29
+ - `autoVerify`: `false` (set `true` to run `forge-verify` automatically after each stage completes)
30
+ - `autoVerifyStages`: `{}` (per-stage overrides for `autoVerify`)
31
+ - `autoFix`: `false` (set `true` to chain `forge-fix` after an auto-verify finds issues)
29
32
 
30
33
  If `forge.config.json` already exists, the script will not overwrite it.
31
34
 
35
+ ## Offer auto-verify
36
+
37
+ The template writes `autoVerify: false`. After the config is created (and only when the script
38
+ actually created it — skip this if it reported the file already exists), offer to turn
39
+ auto-verify on, then write the choice back into `forge.config.json`.
40
+
41
+ If the host's question mechanism is available, ask exactly one question:
42
+
43
+ > **Enable auto-verify?** Verification runs in a clean-room subagent after each stage
44
+ > completes — it never needs a clear your session / start a fresh session and only returns a compact digest to your session.
45
+ > **Recommended: on.** (Change later by editing `autoVerify` in `forge.config.json`.)
46
+
47
+ Options: **Enable (recommended)** / **Leave off**.
48
+
49
+ - On **Enable**: patch `"autoVerify": false` → `"autoVerify": true` in the generated
50
+ `forge.config.json` in place, preserving formatting and every other key.
51
+ - On **Leave off**: leave the config as written (`autoVerify: false`).
52
+
53
+ If the host lacks a structured question tool but can still prompt the user (e.g. Codex asks in
54
+ plain text), use that — ask the one question directly and wait for the reply; it is the same
55
+ choice, just rendered differently. Only when the host has **no** way to ask at all (a fully
56
+ non-interactive / headless run) do you skip the prompt: leave `autoVerify: false` and print the
57
+ one-line note `Set "autoVerify": true in forge.config.json to verify automatically after each stage.`
58
+
32
59
  After initialization, start the pipeline with `/feature-forge:forge-1-prd <feature-name>`.
33
60
 
34
61
  ---
@@ -47,7 +47,7 @@ Pick based on how many checks the mode carries (see the per-mode totals in Step
47
47
 
48
48
  The verifier(s) are read-only — they return findings as their response; **you** (the
49
49
  parent) assemble and write the single document to
50
- `{specsDir}/{feature}/.verification/VERIFY-{mode}-{YYYY-MM-DD}.md`. When you fanned out:
50
+ `{resolvedFeatureDir}/.verification/VERIFY-{mode}-{YYYY-MM-DD}.md`. When you fanned out:
51
51
  1. Concatenate all instances' findings and **renumber `V-NNN` IDs uniquely** across the
52
52
  merged set.
53
53
  2. **Dedup** overlaps — when two instances flag the same file+location+issue (e.g. a
@@ -72,15 +72,39 @@ without subagents), fall back to running verification inline in the current sess
72
72
 
73
73
  **Inline execution guidance:** If running inline (not as subagent), process verification checklists one category at a time to manage context pressure. Load only the artifacts needed for each category, verify, summarize findings, then move to the next category.
74
74
 
75
+ ### Require-clean (`auto`) mode — unattended auto-verify
76
+
77
+ When the navigator auto-invokes this skill (its `autoVerify` path), it passes a
78
+ **require-clean** signal (e.g. args include `--require-clean`, or the invocation is
79
+ described as auto-verify). In this mode the clean-room guarantee is load-bearing: the
80
+ whole reason auto-verify is safe to run without a clear your session / start a fresh session is that the `forge-verifier`
81
+ subagent inherits none of the dispatching session's context. Running inline would break
82
+ that — it would consume the dispatching session's context and invalidate the no-clear
83
+ justification.
84
+
85
+ So in require-clean mode, **do NOT fall back to inline execution**. If the host's subagent mechanism
86
+ or `forge-verifier` subagent is not dispatchable, return a **sentinel** instead of doing
87
+ any work:
88
+
89
+ > `CLEAN_ROOM_UNAVAILABLE: forge-verifier subagent not dispatchable — verify not run.`
90
+
91
+ Do not analyze artifacts, do not write a findings document, and do not touch pipeline
92
+ state. The navigator detects this sentinel and degrades to its manual verify gate (Tier
93
+ 2/3), so verify state stays outstanding and the stage is never marked verified on false
94
+ assurance. **Manual / interactive invocation** (the normal `/feature-forge:forge-verify`
95
+ path, no require-clean signal) keeps the inline fallback above unchanged.
96
+
75
97
  ## Prerequisites
76
98
 
77
99
  Read and follow `references/shared-conventions.md` for feature name validation, configuration reading, and force mode handling before proceeding.
78
100
 
101
+ Resolve the feature directory via the **Feature Directory Resolution** block in `references/shared-conventions.md` (so a standalone feature resolves to its flat `{specsDir}/{feature}/` path exactly as today, and an epic member resolves to its nested `{specsDir}/{epic}/{feature}/` path). Use the resulting `{resolvedFeatureDir}` everywhere this skill reads or writes a per-feature artifact or state file — the `{specsDir}/{feature}/…` forms below are shorthand for the resolved path, not a literal flat layout. This does not apply to **epic mode**, whose paths are epic-scoped (`{specsDir}/{epic}/…`) by design.
102
+
79
103
  **Turn structure reminder:** Output analysis/context as text, then route ALL questions through the host's question mechanism. Never embed questions in text output — the user will not be prompted and the session will stall.
80
104
 
81
105
  ## Step 1: Read Configuration and Determine Mode
82
106
 
83
- Read `{specsDir}/{feature}/.pipeline-state.json` to understand current pipeline state.
107
+ Read `{resolvedFeatureDir}/.pipeline-state.json` to understand current pipeline state.
84
108
 
85
109
  ### Mode Selection
86
110
 
@@ -100,16 +124,16 @@ If ambiguous, use the host's question mechanism to ask which stage to verify.
100
124
  Load into context ALL artifacts for this feature based on mode:
101
125
 
102
126
  **For prd mode:**
103
- - `{specsDir}/{feature}/PRD.md`
127
+ - `{resolvedFeatureDir}/PRD.md`
104
128
 
105
129
  **For tech mode:**
106
- - `{specsDir}/{feature}/PRD.md`
107
- - `{specsDir}/{feature}/tech-spec.md`
130
+ - `{resolvedFeatureDir}/PRD.md`
131
+ - `{resolvedFeatureDir}/tech-spec.md`
108
132
 
109
133
  **For specs mode:**
110
- - `{specsDir}/{feature}/PRD.md`
111
- - `{specsDir}/{feature}/tech-spec.md`
112
- - `{specsDir}/{feature}/##-*.md` (all implementation specs)
134
+ - `{resolvedFeatureDir}/PRD.md`
135
+ - `{resolvedFeatureDir}/tech-spec.md`
136
+ - `{resolvedFeatureDir}/##-*.md` (all implementation specs)
113
137
 
114
138
  **For backlog mode:**
115
139
  - All of the above, PLUS
@@ -150,7 +174,7 @@ Every finding must include:
150
174
 
151
175
  ## Step 4: Write Findings Document
152
176
 
153
- Ensure the `.verification/` subdirectory exists, then write findings to `{specsDir}/{feature}/.verification/VERIFY-{mode}-{YYYY-MM-DD}.md`.
177
+ Ensure the `.verification/` subdirectory exists, then write findings to `{resolvedFeatureDir}/.verification/VERIFY-{mode}-{YYYY-MM-DD}.md`.
154
178
 
155
179
  **For epic mode**, the target is `{specsDir}/{epic}/.verification/VERIFY-epic-{YYYY-MM-DD}.md` (the same format, with `{mode}=epic`).
156
180
 
@@ -186,9 +210,16 @@ Do NOT embed this question in your text output.
186
210
 
187
211
  Write pipeline state conforming to `references/pipeline-state-schema.json`.
188
212
 
189
- Update `{specsDir}/{feature}/.pipeline-state.json`:
190
- - Set the relevant verify entry status to `findings-reported`
213
+ Update `{resolvedFeatureDir}/.pipeline-state.json`:
214
+ - Set the relevant verify entry status to `findings-reported` (or `passed` when there
215
+ are zero findings)
191
216
  - Record `findingsFile`, `findingsCount`, `verifiedAt`
217
+ - Record `verifiedStageVersion` = the current `version` of the production stage entry
218
+ this verify covers (e.g. verifying `tech` → `stages["forge-2-tech"].version`). This
219
+ feeds the navigator's freshness ledger: a later revision to that artifact bumps its
220
+ `version`, so the recorded value no longer matches and auto-verify re-fires. Omitting
221
+ this leaves the verify looking stale (safe: the navigator re-verifies rather than
222
+ skips).
192
223
 
193
224
  Do NOT mark as `findings-applied` — that happens after the fix pass.
194
225
 
@@ -213,13 +244,13 @@ Do NOT mark as `findings-applied` — that happens after the fix pass.
213
244
  - Don't verify things that are intentionally left open (check the PRD's "Open Questions" section).
214
245
  - If you find zero issues, say so honestly. Don't manufacture findings to seem thorough. But zero findings on a complex feature is suspicious — double-check.
215
246
  - The findings document must be self-contained. A fresh agent reading it should be able to apply every fix without needing conversational context from this session.
216
- - For backlog verification, also run the loop runner's validate command (resolve `loopRunner` from `forge.config.json`, default rauf: `rauf backlog validate . --backlog {backlogDir} --specs-dir {specsDir}/{feature} --json`). Include any findings it reports (exit 1) as verification findings; if the runner isn't installed yet (command missing), note that backlog validation was skipped rather than failing.
247
+ - For backlog verification, also run the loop runner's validate command (resolve `loopRunner` from `forge.config.json`, default rauf: `rauf backlog validate . --backlog {backlogDir} --specs-dir {resolvedFeatureDir} --json`). Include any findings it reports (exit 1) as verification findings; if the runner isn't installed yet (command missing), note that backlog validation was skipped rather than failing.
217
248
  - For specs verification, also run the deterministic traceability validator to supplement agent-driven traceability checks. Include any uncovered requirements or orphaned references as findings:
218
249
 
219
250
  ```bash
220
- R="$(for d in "$HOME"/.claude/skills/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)"
251
+ R="$(bash -c 'for d in "$HOME"/.claude/skills/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')"
221
252
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
222
- python3 "$R/scripts/validate-traceability.py" {specsDir}/{feature}/PRD.md {specsDir}/{feature}/ --json
253
+ python3 "$R/scripts/validate-traceability.py" {resolvedFeatureDir}/PRD.md {resolvedFeatureDir}/ --json
223
254
  ```
224
255
 
225
256
  ---
@@ -192,7 +192,7 @@ findings to E01/E02/E03/E08. Then perform the judgment checks E04–E07 by readi
192
192
  manifest, EPIC.md, and completed members' specs.
193
193
 
194
194
  ```bash
195
- R="$(for d in "$HOME"/.claude/skills/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)"
195
+ R="$(bash -c 'for d in "$HOME"/.claude/skills/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')"
196
196
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
197
197
  python3 "$R/scripts/epic-manifest.py" validate "{epic}" --specs-dir "{specsDir}" --json
198
198
  ```
package/dist/apply.js CHANGED
@@ -91,8 +91,9 @@ async function applyCopyInstall(planned, ctx) {
91
91
  if (!placementResult.ok)
92
92
  return fail(ctx, planned, placementResult.error);
93
93
  // No-op short-circuit (REQ-IDEM-01): every action — primary AND placement — unchanged ⇒ zero
94
- // writes, manifest untouched.
95
- if (allUnchanged(planned)) {
94
+ // writes, manifest untouched — UNLESS manifest metadata (raufPin) drifted, which must persist
95
+ // even with no file change (F1) so report / on-disk manifest / a later `list` agree.
96
+ if (allUnchanged(planned) && !manifestNeedsRewrite(ctx)) {
96
97
  return success(ctx, planned);
97
98
  }
98
99
  const manifest = buildManifest({
@@ -113,6 +114,18 @@ async function applyCopyInstall(planned, ctx) {
113
114
  return fail(ctx, planned, wrote.error);
114
115
  return success(ctx, planned);
115
116
  }
117
+ /**
118
+ * True when manifest metadata diverges from the prior manifest, so the manifest must be rewritten
119
+ * even when every file action is "unchanged" (F1). Today the only such field is `raufPin`; a null
120
+ * prior manifest also counts (there is nothing recorded yet). `updatedAt` is deliberately NOT a
121
+ * trigger — it is a consequence of writing, so keying on it would defeat the idempotency short-circuit.
122
+ */
123
+ function manifestNeedsRewrite(ctx) {
124
+ const prior = ctx.priorManifest;
125
+ if (prior === null)
126
+ return true;
127
+ return prior.raufPin !== ctx.raufPin;
128
+ }
116
129
  /** True iff every planned action — primary files and all placement files — is "unchanged". */
117
130
  function allUnchanged(planned) {
118
131
  return (planned.files.every((f) => f.action === "unchanged") &&
@@ -164,8 +177,8 @@ async function applySymlinkInstall(planned, ctx) {
164
177
  const primary = planned.files;
165
178
  const primaryUntouched = primary.every((f) => f.action === "unchanged") ||
166
179
  primary.every((f) => f.action === "skip-modified");
167
- // Nothing changed anywhere ⇒ zero writes, manifest untouched.
168
- if (allUnchanged(planned)) {
180
+ // Nothing changed anywhere ⇒ zero writes, manifest untouched — unless raufPin drifted (F1).
181
+ if (allUnchanged(planned) && !manifestNeedsRewrite(ctx)) {
169
182
  return success(ctx, planned);
170
183
  }
171
184
  // The recorded mode/link reflect the primary namespace dir: only (re)link when it actually changed
@@ -285,12 +298,25 @@ async function applyMirror(pl, ctx, source, prior) {
285
298
  case "skip-modified": {
286
299
  // Carry the prior record forward; if none exists (e.g. a v1→v2 manifest migration where the
287
300
  // file is already on disk), reconstruct it by hashing the destination so the inventory stays
288
- // faithful rather than silently dropping an unrecorded-but-present file.
301
+ // faithful rather than silently dropping an unrecorded-but-present file. Guard the hash: a
302
+ // TOCTOU vanish (file removed after planning) must yield an err Result, never throw ENOENT
303
+ // out of apply (REQ-OBS-03 — a throw would abort every sibling agent at the CLI boundary).
289
304
  const p = priorByPath.get(fa.relpath);
290
- if (p !== undefined)
305
+ if (p !== undefined) {
291
306
  inventory.push(p);
292
- else
293
- inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
307
+ }
308
+ else {
309
+ try {
310
+ inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
311
+ }
312
+ catch {
313
+ return err({
314
+ code: "UNEXPECTED",
315
+ agent: ctx.agent,
316
+ message: `mirror file "${fa.relpath}" vanished before it could be recorded (${destAbs})`,
317
+ });
318
+ }
319
+ }
294
320
  break;
295
321
  }
296
322
  }
package/dist/cli.js CHANGED
@@ -23,7 +23,8 @@ import { plan, resolveMode } from "./plan.js"; // 04
23
23
  import { apply } from "./apply.js"; // 04
24
24
  import { manifestPath, readManifest, planUninstall } from "./manifest.js"; // 05
25
25
  import { preflightRauf, RAUF_PIN } from "./rauf.js"; // 06
26
- import { sha256File } from "./hash.js"; // 03 (list destination-drift hashing)
26
+ import { sha256File, sha256String } from "./hash.js"; // 03 (list destination-drift hashing)
27
+ import { extractManagedRegion } from "./placements.js"; // A4b managed-block drift
27
28
  import { renderReport } from "./report.js"; // 09
28
29
  /** Canonical subcommand table (REQ-DIST-03, §1.2). */
29
30
  export const SUBCOMMANDS = [
@@ -424,12 +425,18 @@ function listOneAgent(agent, detection, flags, scope, env) {
424
425
  /**
425
426
  * Return `"true"` if any manifest-recorded file's on-disk bytes differ from its recorded sha256
426
427
  * (a locally-modified destination, REQ-SAFE-03), `"false"` if every recorded file matches, or
427
- * `"n/a(symlink)"` for a symlink-mode install (no per-file sha256 to compare). A missing recorded
428
- * file also counts as drift. Pure read of the manifest's per-file sha256 against a fresh local
429
- * hash — no network, no source bundle needed (REQ-PERF-01). Hash errors are swallowed as drift
428
+ * `"n/a(symlink)"` for a symlink-mode primary install (no per-file sha256 to compare). A missing
429
+ * recorded file also counts as drift. Pure read of the manifest's per-file sha256 against a fresh
430
+ * local hash — no network, no source bundle needed (REQ-PERF-01). Hash errors are swallowed as drift
430
431
  * (an unreadable recorded file is itself a deviation from the clean install).
432
+ *
433
+ * Secondary placements (A4b) are checked in EVERY mode — they are always copy-written with a recorded
434
+ * hash even when the primary namespace is a symlink, so ignoring them (the prior behavior) left codex
435
+ * `.toml` mirrors and copilot managed blocks unwatched for drift.
431
436
  */
432
437
  function detectDestinationDrift(manifest) {
438
+ if (detectPlacementDrift(manifest))
439
+ return "true";
433
440
  if (manifest.mode === "symlink")
434
441
  return "n/a(symlink)";
435
442
  for (const f of manifest.files) {
@@ -445,6 +452,35 @@ function detectDestinationDrift(manifest) {
445
452
  }
446
453
  return "false";
447
454
  }
455
+ /**
456
+ * True iff any secondary placement diverges from its recorded hash. A "mirror" file drifts when its
457
+ * on-disk bytes differ from the recorded sha256 (destination is a DIR, paths relative to it). A
458
+ * "managed-block" drifts when the sentinel region extracted from the (user-owned) target FILE no
459
+ * longer hashes to the recorded region hash — or the region is gone entirely. Read errors count as
460
+ * drift, matching the primary check.
461
+ */
462
+ function detectPlacementDrift(manifest) {
463
+ for (const p of manifest.placements ?? []) {
464
+ for (const f of p.files) {
465
+ if (f.sha256 === undefined)
466
+ continue;
467
+ try {
468
+ if (p.kind === "managed-block") {
469
+ const region = extractManagedRegion(readFileSync(p.destination, "utf8"));
470
+ if (region === null || sha256String(region) !== f.sha256)
471
+ return true;
472
+ }
473
+ else if (sha256File(path.join(p.destination, f.path)) !== f.sha256) {
474
+ return true;
475
+ }
476
+ }
477
+ catch {
478
+ return true; // unreadable/absent recorded placement ⇒ drift
479
+ }
480
+ }
481
+ }
482
+ return false;
483
+ }
448
484
  // ---------------------------------------------------------------------------
449
485
  // 7. helpText and --version (§3.4)
450
486
  // ---------------------------------------------------------------------------
package/dist/fsutil.d.ts CHANGED
@@ -1,16 +1,4 @@
1
1
  import type { Result } from "./types.js";
2
- /**
3
- * Resolve `segs` against `root` and assert the result lies WITHIN `root` (REQ-SEC-02). Returns the
4
- * resolved absolute path on success, or a PATH_ESCAPE InstallerError if a `..` segment or a
5
- * malformed agent id would escape the agent config root. MUST be called before ANY write/delete.
6
- *
7
- * Uses `path.relative` + the `..` prefix test (the robust boundary check — a bare string startsWith
8
- * would false-pass `/root-evil`).
9
- *
10
- * @param root - the containment boundary (the agent config root, e.g. <home>/.claude)
11
- * @param segs - path segments to join under root (destination, then a bundle-relative path)
12
- * @returns ok(absolutePath) if inside; err(PATH_ESCAPE) otherwise.
13
- */
14
2
  export declare function resolveWithin(root: string, ...segs: string[]): Result<string>;
15
3
  /**
16
4
  * Recursively copy `src` → `dest` (copy mode). The CALLER is responsible for having
package/dist/fsutil.js CHANGED
@@ -19,6 +19,12 @@ import { ok, err } from "./types.js";
19
19
  * @param segs - path segments to join under root (destination, then a bundle-relative path)
20
20
  * @returns ok(absolutePath) if inside; err(PATH_ESCAPE) otherwise.
21
21
  */
22
+ /** True iff `child` is strictly within `parent` (path.relative + `..`-prefix test, never a bare
23
+ * `startsWith`). Shares the boundary logic of {@link resolveWithin} for a plain boolean check. */
24
+ function isWithin(parent, child) {
25
+ const rel = path.relative(parent, child);
26
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
27
+ }
22
28
  export function resolveWithin(root, ...segs) {
23
29
  const base = path.resolve(root);
24
30
  const target = path.resolve(base, ...segs);
@@ -125,7 +131,10 @@ export async function removeEmptyDirsWithin(startDir, stopRoot) {
125
131
  return contained;
126
132
  const stop = path.resolve(stopRoot);
127
133
  let cur = contained.value;
128
- while (cur !== stop && cur.startsWith(stop)) {
134
+ // Robust boundary test (path.relative + `..` prefix), not a bare `cur.startsWith(stop)` — the
135
+ // exact anti-pattern this module warns against at the top: `startsWith` false-passes a sibling
136
+ // like `<stop>-evil`. Safe here by the ancestor invariant today, but don't rely on it.
137
+ while (cur !== stop && isWithin(stop, cur)) {
129
138
  let entries;
130
139
  try {
131
140
  entries = await fsp.readdir(cur);
@@ -28,7 +28,7 @@ export interface BuildManifestArgs {
28
28
  readonly skills: readonly string[];
29
29
  /** SHA-256 over the source bundle's canonical (sorted-path) file set — drift anchor (spec 03). */
30
30
  readonly sourceHash: string;
31
- /** Recorded pinned rauf coordinate (e.g. "@garygentry/rauf@0.11.0"); `null` when `--skip-rauf` (spec 06). */
31
+ /** Recorded pinned rauf coordinate (e.g. "@garygentry/rauf@0.12.0"); `null` when `--skip-rauf` (spec 06). */
32
32
  readonly raufPin: string | null;
33
33
  /** Symlink mode only: the source bundle the namespace dir links to (REQ-SAFE-02). */
34
34
  readonly link?: {
package/dist/plan.js CHANGED
@@ -220,8 +220,16 @@ function planSymlink(ctx) {
220
220
  const source = ctx.source;
221
221
  const prior = ctx.priorManifest;
222
222
  const priorExists = prior !== null;
223
- const priorIsLiveSymlinkToSameTarget = priorExists && prior.link?.target === source.root;
224
- const action = priorIsLiveSymlinkToSameTarget
223
+ const manifestSaysLive = priorExists && prior.link?.target === source.root;
224
+ // The manifest can claim a live symlink that no longer exists on disk (deleted, or clobbered by a
225
+ // regular file/dir). Copy mode self-heals via per-file hashing; symlink mode had no on-disk check
226
+ // and would classify a broken link "unchanged" forever. lstat the link and force a relink when the
227
+ // manifest says live but the link is absent/not-a-symlink (F2) — never "skip-modified", which apply
228
+ // treats as untouched and would leave the install broken.
229
+ if (manifestSaysLive && !isLiveSymlink(ctx.destination)) {
230
+ return [{ relpath: ".", action: "create" }];
231
+ }
232
+ const action = manifestSaysLive
225
233
  ? "unchanged"
226
234
  : priorExists
227
235
  ? ctx.force
@@ -230,6 +238,18 @@ function planSymlink(ctx) {
230
238
  : "create";
231
239
  return [{ relpath: ".", action }];
232
240
  }
241
+ /**
242
+ * True iff `absPath` exists and is currently a symbolic link. `lstat`, not `readlink` — we only need
243
+ * the node type to decide whether a manifest-recorded symlink is still live (F2). Absent ⇒ false.
244
+ */
245
+ function isLiveSymlink(absPath) {
246
+ try {
247
+ return fs.lstatSync(absPath).isSymbolicLink();
248
+ }
249
+ catch {
250
+ return false;
251
+ }
252
+ }
233
253
  /** Paths in the prior manifest that the current source no longer contains → `remove` (row 6). */
234
254
  function orphanRemovals(priorFiles, sourceFiles) {
235
255
  const inSource = new Set(sourceFiles.map((f) => f.relpath));
package/dist/rauf.d.ts CHANGED
@@ -19,13 +19,13 @@ import { type Result } from "./types.js";
19
19
  *
20
20
  * Shape: `<name>@<version>` — the SCOPED package `@garygentry/rauf` (the unscoped `rauf` name is
21
21
  * blocked by npm's similarity filter). Advanced on each feature-forge release to a new
22
- * known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.11.0.
22
+ * known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.12.0.
23
23
  *
24
- * rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.11.0` resolves from the npm registry, so the
24
+ * rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.12.0` resolves from the npm registry, so the
25
25
  * preflight below passes by default. (Historically this pin pointed at an unpublished package and
26
26
  * the preflight was a designed-to-fail check — see the `--skip-rauf` escape hatch.)
27
27
  */
28
- export declare const RAUF_PIN = "@garygentry/rauf@0.11.0";
28
+ export declare const RAUF_PIN = "@garygentry/rauf@0.12.0";
29
29
  /**
30
30
  * An injectable, READ-ONLY registry query (D1). Given a coordinate `name@version`, returns the
31
31
  * resolved version string on success, or an `InstallerError` if it is not resolvable.
@@ -37,7 +37,7 @@ export declare const RAUF_PIN = "@garygentry/rauf@0.11.0";
37
37
  * Contract: the query MUST be read-only — it MUST NOT install, MUST NOT mutate global npm
38
38
  * state, and MUST NOT execute rauf. `npm view` satisfies this (it only reads registry metadata).
39
39
  *
40
- * @param coordinate - the `name@version` to resolve, e.g. "@garygentry/rauf@0.11.0"
40
+ * @param coordinate - the `name@version` to resolve, e.g. "@garygentry/rauf@0.12.0"
41
41
  * @returns Result<string> — the resolved version on success; RAUF_UNRESOLVABLE on failure.
42
42
  */
43
43
  export type RegistryQuery = (coordinate: string) => Result<string>;
package/dist/rauf.js CHANGED
@@ -20,13 +20,13 @@ import { err, ok } from "./types.js";
20
20
  *
21
21
  * Shape: `<name>@<version>` — the SCOPED package `@garygentry/rauf` (the unscoped `rauf` name is
22
22
  * blocked by npm's similarity filter). Advanced on each feature-forge release to a new
23
- * known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.11.0.
23
+ * known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.12.0.
24
24
  *
25
- * rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.11.0` resolves from the npm registry, so the
25
+ * rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.12.0` resolves from the npm registry, so the
26
26
  * preflight below passes by default. (Historically this pin pointed at an unpublished package and
27
27
  * the preflight was a designed-to-fail check — see the `--skip-rauf` escape hatch.)
28
28
  */
29
- export const RAUF_PIN = "@garygentry/rauf@0.11.0";
29
+ export const RAUF_PIN = "@garygentry/rauf@0.12.0";
30
30
  /**
31
31
  * Resolvability preflight for the pinned default loop runner (D1; REQ-RAUF-01/02/03, OQ-1).
32
32
  *