@garygentry/feature-forge 0.1.5 → 0.2.1

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 (182) hide show
  1. package/README.md +19 -1
  2. package/adapters/GENERATION-REPORT.md +12 -12
  3. package/adapters/claude/.feature-forge-bundle.json +6 -0
  4. package/adapters/claude/references/forge-config-schema.json +2 -2
  5. package/adapters/claude/references/portable-root.md +8 -5
  6. package/adapters/claude/references/process-overview.md +1 -1
  7. package/adapters/claude/references/shared-conventions.md +24 -5
  8. package/adapters/claude/references/stack-resolution.md +4 -1
  9. package/adapters/claude/references/stacks/go.md +1 -1
  10. package/adapters/claude/references/stacks/python.md +1 -1
  11. package/adapters/claude/references/stacks/rust.md +1 -1
  12. package/adapters/claude/references/stacks/typescript.md +1 -1
  13. package/adapters/claude/scripts/epic-manifest.py +1379 -0
  14. package/adapters/claude/scripts/forge-bootstrap.py +991 -0
  15. package/adapters/claude/scripts/forge-init.sh +44 -0
  16. package/adapters/claude/scripts/forge-root.sh +30 -8
  17. package/adapters/claude/scripts/validate-traceability.py +150 -0
  18. package/adapters/claude/skills/forge/SKILL.md +5 -5
  19. package/adapters/claude/skills/forge-0-epic/SKILL.md +6 -10
  20. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +2 -2
  21. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  22. package/adapters/claude/skills/forge-1-prd/SKILL.md +2 -2
  23. package/adapters/claude/skills/forge-2-tech/SKILL.md +8 -7
  24. package/adapters/claude/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  25. package/adapters/claude/skills/forge-3-specs/SKILL.md +1 -1
  26. package/adapters/claude/skills/forge-4-backlog/SKILL.md +2 -2
  27. package/adapters/claude/skills/forge-5-loop/SKILL.md +2 -2
  28. package/adapters/claude/skills/forge-6-docs/SKILL.md +2 -2
  29. package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
  30. package/adapters/claude/skills/forge-fix/SKILL.md +1 -1
  31. package/adapters/claude/skills/forge-init/SKILL.md +1 -1
  32. package/adapters/claude/skills/forge-verify/SKILL.md +7 -2
  33. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
  34. package/adapters/codex/.feature-forge-bundle.json +6 -0
  35. package/adapters/codex/agents/{forge-researcher.md → forge-researcher.toml} +4 -4
  36. package/adapters/codex/agents/{forge-spec-writer.md → forge-spec-writer.toml} +4 -4
  37. package/adapters/codex/agents/{forge-verifier.md → forge-verifier.toml} +4 -4
  38. package/adapters/codex/references/forge-config-schema.json +2 -2
  39. package/adapters/codex/references/portable-root.md +8 -5
  40. package/adapters/codex/references/process-overview.md +1 -1
  41. package/adapters/codex/references/shared-conventions.md +24 -5
  42. package/adapters/codex/references/stack-resolution.md +4 -1
  43. package/adapters/codex/references/stacks/go.md +1 -1
  44. package/adapters/codex/references/stacks/python.md +1 -1
  45. package/adapters/codex/references/stacks/rust.md +1 -1
  46. package/adapters/codex/references/stacks/typescript.md +1 -1
  47. package/adapters/codex/scripts/epic-manifest.py +1379 -0
  48. package/adapters/codex/scripts/forge-bootstrap.py +991 -0
  49. package/adapters/codex/scripts/forge-init.sh +44 -0
  50. package/adapters/codex/scripts/forge-root.sh +30 -8
  51. package/adapters/codex/scripts/validate-traceability.py +150 -0
  52. package/adapters/codex/skills/forge/{forge.md → SKILL.md} +16 -6
  53. package/adapters/codex/skills/forge-0-epic/{forge-0-epic.md → SKILL.md} +26 -20
  54. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +2 -2
  55. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  56. package/adapters/codex/skills/forge-1-prd/{forge-1-prd.md → SKILL.md} +18 -8
  57. package/adapters/codex/skills/forge-2-tech/{forge-2-tech.md → SKILL.md} +26 -15
  58. package/adapters/codex/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  59. package/adapters/codex/skills/forge-3-specs/{forge-3-specs.md → SKILL.md} +16 -6
  60. package/adapters/codex/skills/forge-4-backlog/{forge-4-backlog.md → SKILL.md} +15 -5
  61. package/adapters/codex/skills/forge-5-loop/{forge-5-loop.md → SKILL.md} +27 -17
  62. package/adapters/codex/skills/forge-6-docs/{forge-6-docs.md → SKILL.md} +17 -7
  63. package/adapters/codex/skills/forge-bootstrap/{forge-bootstrap.md → SKILL.md} +17 -7
  64. package/adapters/codex/skills/forge-fix/{forge-fix.md → SKILL.md} +12 -2
  65. package/adapters/codex/skills/forge-init/{forge-init.md → SKILL.md} +11 -1
  66. package/adapters/codex/skills/forge-verify/{forge-verify.md → SKILL.md} +24 -9
  67. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
  68. package/adapters/copilot/.feature-forge-bundle.json +6 -0
  69. package/adapters/copilot/references/forge-config-schema.json +2 -2
  70. package/adapters/copilot/references/portable-root.md +8 -5
  71. package/adapters/copilot/references/process-overview.md +1 -1
  72. package/adapters/copilot/references/shared-conventions.md +24 -5
  73. package/adapters/copilot/references/stack-resolution.md +4 -1
  74. package/adapters/copilot/references/stacks/go.md +1 -1
  75. package/adapters/copilot/references/stacks/python.md +1 -1
  76. package/adapters/copilot/references/stacks/rust.md +1 -1
  77. package/adapters/copilot/references/stacks/typescript.md +1 -1
  78. package/adapters/copilot/scripts/epic-manifest.py +1379 -0
  79. package/adapters/copilot/scripts/forge-bootstrap.py +991 -0
  80. package/adapters/copilot/scripts/forge-init.sh +44 -0
  81. package/adapters/copilot/scripts/forge-root.sh +30 -8
  82. package/adapters/copilot/scripts/validate-traceability.py +150 -0
  83. package/adapters/copilot/skills/forge/forge.md +16 -6
  84. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +26 -20
  85. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +2 -2
  86. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  87. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +18 -8
  88. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +26 -15
  89. package/adapters/copilot/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  90. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +16 -6
  91. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +15 -5
  92. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +27 -17
  93. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +17 -7
  94. package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +17 -7
  95. package/adapters/copilot/skills/forge-fix/forge-fix.md +12 -2
  96. package/adapters/copilot/skills/forge-init/forge-init.md +11 -1
  97. package/adapters/copilot/skills/forge-verify/forge-verify.md +24 -9
  98. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
  99. package/adapters/cursor/.feature-forge-bundle.json +6 -0
  100. package/adapters/cursor/references/forge-config-schema.json +2 -2
  101. package/adapters/cursor/references/portable-root.md +8 -5
  102. package/adapters/cursor/references/process-overview.md +1 -1
  103. package/adapters/cursor/references/shared-conventions.md +24 -5
  104. package/adapters/cursor/references/stack-resolution.md +4 -1
  105. package/adapters/cursor/references/stacks/go.md +1 -1
  106. package/adapters/cursor/references/stacks/python.md +1 -1
  107. package/adapters/cursor/references/stacks/rust.md +1 -1
  108. package/adapters/cursor/references/stacks/typescript.md +1 -1
  109. package/adapters/cursor/scripts/epic-manifest.py +1379 -0
  110. package/adapters/cursor/scripts/forge-bootstrap.py +991 -0
  111. package/adapters/cursor/scripts/forge-init.sh +44 -0
  112. package/adapters/cursor/scripts/forge-root.sh +30 -8
  113. package/adapters/cursor/scripts/validate-traceability.py +150 -0
  114. package/adapters/cursor/skills/forge/forge.mdc +16 -6
  115. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +26 -20
  116. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +2 -2
  117. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  118. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +18 -8
  119. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +26 -15
  120. package/adapters/cursor/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  121. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +16 -6
  122. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +15 -5
  123. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +27 -17
  124. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +17 -7
  125. package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +17 -7
  126. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +12 -2
  127. package/adapters/cursor/skills/forge-init/forge-init.mdc +11 -1
  128. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +24 -9
  129. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
  130. package/adapters/gemini/.feature-forge-bundle.json +6 -0
  131. package/adapters/gemini/gemini-extension.json +1 -1
  132. package/adapters/gemini/references/forge-config-schema.json +2 -2
  133. package/adapters/gemini/references/portable-root.md +8 -5
  134. package/adapters/gemini/references/process-overview.md +1 -1
  135. package/adapters/gemini/references/shared-conventions.md +24 -5
  136. package/adapters/gemini/references/stack-resolution.md +4 -1
  137. package/adapters/gemini/references/stacks/go.md +1 -1
  138. package/adapters/gemini/references/stacks/python.md +1 -1
  139. package/adapters/gemini/references/stacks/rust.md +1 -1
  140. package/adapters/gemini/references/stacks/typescript.md +1 -1
  141. package/adapters/gemini/scripts/epic-manifest.py +1379 -0
  142. package/adapters/gemini/scripts/forge-bootstrap.py +991 -0
  143. package/adapters/gemini/scripts/forge-init.sh +44 -0
  144. package/adapters/gemini/scripts/forge-root.sh +30 -8
  145. package/adapters/gemini/scripts/validate-traceability.py +150 -0
  146. package/adapters/gemini/skills/forge/forge.md +16 -6
  147. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +26 -20
  148. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +2 -2
  149. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  150. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +18 -8
  151. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +26 -15
  152. package/adapters/gemini/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  153. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +16 -6
  154. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +15 -5
  155. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +27 -17
  156. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +17 -7
  157. package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +17 -7
  158. package/adapters/gemini/skills/forge-fix/forge-fix.md +12 -2
  159. package/adapters/gemini/skills/forge-init/forge-init.md +11 -1
  160. package/adapters/gemini/skills/forge-verify/forge-verify.md +24 -9
  161. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
  162. package/dist/agent-targets.d.ts +20 -4
  163. package/dist/agent-targets.js +29 -4
  164. package/dist/apply.js +245 -18
  165. package/dist/cli.js +12 -6
  166. package/dist/hash.d.ts +5 -0
  167. package/dist/hash.js +7 -0
  168. package/dist/manifest.d.ts +4 -2
  169. package/dist/manifest.js +58 -2
  170. package/dist/placements.d.ts +69 -0
  171. package/dist/placements.js +116 -0
  172. package/dist/plan.d.ts +7 -0
  173. package/dist/plan.js +87 -1
  174. package/dist/rauf.d.ts +4 -4
  175. package/dist/rauf.js +3 -3
  176. package/dist/report.js +21 -0
  177. package/dist/source.d.ts +4 -3
  178. package/dist/source.js +4 -3
  179. package/dist/types.d.ts +163 -19
  180. package/dist/types.js +42 -11
  181. package/package.json +1 -1
  182. package/adapters/codex/agents/openai.yaml +0 -10
@@ -12,7 +12,7 @@ Apply fixes from the most recent forge-verify findings document, with step-level
12
12
 
13
13
  Read and follow `references/shared-conventions.md` for feature name validation, configuration reading, and force mode handling before proceeding.
14
14
 
15
- **Turn structure reminder:** Output analysis/context as text, then route ALL questions through `AskUserQuestion`. Never embed questions in text output — the user will not be prompted and the session will stall.
15
+ **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.
16
16
 
17
17
  ## Step 1: Locate Findings Document
18
18
 
@@ -29,7 +29,7 @@ Read and follow `references/shared-conventions.md` for feature name validation,
29
29
  ## Step 3: Handle User Decisions
30
30
 
31
31
  If the "User Decisions Required" section has unresolved items:
32
- 1. Present each decision to the user with the context from the findings, using `AskUserQuestion` for each decision point. Only recommend a specific option if the findings provide clear evidence for it; otherwise present options neutrally.
32
+ 1. Present each decision to the user with the context from the findings, using the host's question mechanism for each decision point. Follow the **Decision Support** protocol in `references/shared-conventions.md`: lead with a recommended option and put the trade-off in each option's description. When the findings provide clear evidence, recommend with confidence and cite it. When they don't, still offer a sensible default with the trade-offs, but flag it plainly as a judgment call rather than going neutral — a defaulted recommendation beats an unguided option dump.
33
33
  2. Wait for answers before proceeding
34
34
  3. Record decisions in the findings document under the "User Decisions Required" section (mark each as resolved)
35
35
 
@@ -62,3 +62,13 @@ Tell the user:
62
62
  "Fixes applied. Next steps:
63
63
  - Run `/feature-forge:forge-verify {feature}` again to confirm all issues are resolved
64
64
  - Or `/feature-forge:forge {feature}` to see pipeline status"
65
+
66
+ ---
67
+
68
+ ## Host execution notes
69
+
70
+ 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:
71
+
72
+ - **User input:** ask the question directly and wait for the answer before proceeding. Do not skip a required question or assume an answer.
73
+ - **Subagents:** if your host cannot dispatch the named custom agent, run that step inline yourself.
74
+ - **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; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done)"
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)"
13
13
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
14
14
  bash "$R/scripts/forge-init.sh"
15
15
  ```
@@ -27,3 +27,13 @@ After initialization, the config file will contain defaults for:
27
27
  If `forge.config.json` already exists, the script will not overwrite it.
28
28
 
29
29
  After initialization, start the pipeline with `/feature-forge:forge-1-prd <feature-name>`.
30
+
31
+ ---
32
+
33
+ ## Host execution notes
34
+
35
+ 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:
36
+
37
+ - **User input:** ask the question directly and wait for the answer before proceeding. Do not skip a required question or assume an answer.
38
+ - **Subagents:** if your host cannot dispatch the named custom agent, run that step inline yourself.
39
+ - **Background / monitoring:** run long-lived commands in the foreground (or your host's background facility) and report progress as it arrives.
@@ -10,7 +10,7 @@ Analyze feature artifacts for completeness, consistency, and quality. Produce st
10
10
 
11
11
  ## Subagent Delegation
12
12
 
13
- This skill is delegated to the `forge-verifier` subagent via the Agent tool. The verifier subagent has:
13
+ This skill is delegated to the `forge-verifier` subagent via the host's subagent mechanism. The verifier subagent has:
14
14
  - **Read-only tools** (Read, Glob, Grep, Bash) — it cannot accidentally modify specs
15
15
  - **Persistent memory** — it accumulates knowledge about this project's recurring issues and patterns across sessions
16
16
  - **The forge-verify skill pre-loaded** — so it has all verification checklists and guidance at startup
@@ -19,12 +19,12 @@ This skill is delegated to the `forge-verifier` subagent via the Agent tool. The
19
19
 
20
20
  Pick based on how many checks the mode carries (see the per-mode totals in Step 3):
21
21
 
22
- - **Small modes (prd ~15, tech ~15): single verifier.** Use the Agent tool once with
23
- `subagent_type="forge-verifier"`, passing the feature name and mode. It runs all
22
+ - **Small modes (prd ~15, tech ~15): single verifier.** Use the host's subagent mechanism once with
23
+ `the forge-verifier custom agent`, passing the feature name and mode. It runs all
24
24
  checks and returns findings.
25
25
  - **Large modes (specs ~38, backlog ~25, impl ~20): parallel dimensioned fan-out.**
26
26
  Split the mode's checklist into **dimension groups** and dispatch **one
27
- `forge-verifier` per group, in parallel — a single message with multiple Agent
27
+ `forge-verifier` per group, in parallel — a single message with multiple subagent
28
28
  calls** (the `superpowers:dispatching-parallel-agents` pattern). Each instance owns a
29
29
  disjoint slice of CHECK-IDs, so it verifies deeper over a narrower scope and they all
30
30
  run concurrently. Suggested groups (map to the category clusters in
@@ -76,7 +76,7 @@ without subagents), fall back to running verification inline in the current sess
76
76
 
77
77
  Read and follow `references/shared-conventions.md` for feature name validation, configuration reading, and force mode handling before proceeding.
78
78
 
79
- **Turn structure reminder:** Output analysis/context as text, then route ALL questions through `AskUserQuestion`. Never embed questions in text output — the user will not be prompted and the session will stall.
79
+ **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
80
 
81
81
  ## Step 1: Read Configuration and Determine Mode
82
82
 
@@ -93,7 +93,7 @@ If a stage is specified as a second argument (e.g., `/feature-forge:forge-verify
93
93
  - **backlog mode**: If `forge-4-backlog` is complete but `forge-verify-backlog` is not `passed` or `findings-applied`
94
94
  - **impl mode**: If user explicitly requests or if implementation code exists for this feature
95
95
 
96
- If ambiguous, use `AskUserQuestion` to ask which stage to verify.
96
+ If ambiguous, use the host's question mechanism to ask which stage to verify.
97
97
 
98
98
  ## Step 2: Load All Relevant Artifacts
99
99
 
@@ -132,7 +132,7 @@ Read `references/verification-checklists.md` for the detailed checklists per mod
132
132
 
133
133
  Each check in `verification-checklists.md` has a unique ID (CHECK-P01, CHECK-T01, CHECK-S01, CHECK-B01, etc.). As you execute each check, record its ID and result (pass/fail/not-applicable). After completing all checks, report the total: "Executed N of M checks. Results: X pass, Y fail, Z not-applicable." If your count is significantly below the expected total for the mode (prd: ~15 checks, tech: ~15 checks, specs: ~38 checks, backlog: ~25 checks, impl: ~20 checks, epic: ~8 checks), you likely skipped checks — go back and complete them.
134
134
 
135
- **Epic mode dispatch.** Epic mode is a small (~8-check) checklist, so per the single-vs-parallel rule above, dispatch a **single `forge-verifier`** via the Agent tool, passing the epic name and `mode=epic`. The verifier runs CHECK-E01..E08 from the `## Epic Mode Checklist` in `references/verification-checklists.md` (E01/E02/E03/E08 are delegated to `epic-manifest.py validate`/`check-name`; E04–E07 are verifier judgment) and returns its findings.
135
+ **Epic mode dispatch.** Epic mode is a small (~8-check) checklist, so per the single-vs-parallel rule above, dispatch a **single `forge-verifier`** via the host's subagent mechanism, passing the epic name and `mode=epic`. The verifier runs CHECK-E01..E08 from the `## Epic Mode Checklist` in `references/verification-checklists.md` (E01/E02/E03/E08 are delegated to `epic-manifest.py validate`/`check-name`; E04–E07 are verifier judgment) and returns its findings.
136
136
 
137
137
  ### Important: Be Specific, Not General
138
138
 
@@ -175,7 +175,12 @@ When building the Fix Execution Plan:
175
175
  **If not in plan mode:** Output the following as text:
176
176
  "Findings and fix plan written to `{findings-file}`."
177
177
 
178
- Then use `AskUserQuestion` to ask: "Would you like to: (a) Review the findings first, (b) Run `/feature-forge:forge-fix {feature}` to apply fixes now, or (c) Enter plan mode and re-run `/feature-forge:forge-verify {feature}` for plan-mode workflow?" Do NOT embed this question in your text output.
178
+ Then use the host's question mechanism to ask how to proceed. Follow the **Decision Support** protocol in `references/shared-conventions.md`: recommend a path based on the findings and give each option a one-line trade-off. Let the severity and volume of findings drive the recommendation — e.g. recommend (b) **Apply fixes now** when findings are clear-cut and mechanical; recommend (a) **Review first** when findings involve design judgment or you flagged low-confidence items; recommend (c) **plan-mode workflow** when the fixes are large or interdependent enough to warrant a reviewed plan. Present:
179
+ - **(a) Review the findings first** — read `{findings-file}` and decide per-finding; safest, but you act on nothing until you return.
180
+ - **(b) Run `/feature-forge:forge-fix {feature}` now** — applies the fix plan immediately; fastest, best when findings are unambiguous.
181
+ - **(c) Enter plan mode and re-run `/feature-forge:forge-verify {feature}`** — produces a reviewable plan before any edits; best for large or risky fix sets.
182
+
183
+ Do NOT embed this question in your text output.
179
184
 
180
185
  ## Step 6: Update Pipeline State
181
186
 
@@ -212,7 +217,17 @@ Do NOT mark as `findings-applied` — that happens after the fix pass.
212
217
  - For specs verification, also run the deterministic traceability validator to supplement agent-driven traceability checks. Include any uncovered requirements or orphaned references as findings:
213
218
 
214
219
  ```bash
215
- R="$(for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/*/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done)"
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)"
216
221
  [ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
217
222
  python3 "$R/scripts/validate-traceability.py" {specsDir}/{feature}/PRD.md {specsDir}/{feature}/ --json
218
223
  ```
224
+
225
+ ---
226
+
227
+ ## Host execution notes
228
+
229
+ 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:
230
+
231
+ - **User input:** ask the question directly and wait for the answer before proceeding. Do not skip a required question or assume an answer.
232
+ - **Subagents:** if your host cannot dispatch the named custom agent, run that step inline yourself.
233
+ - **Background / monitoring:** run long-lived commands in the foreground (or your host's background facility) and report progress as it arrives.
@@ -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; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done)"
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)"
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
  ```
@@ -10,7 +10,7 @@
10
10
  * one table row (REQ-SCALE-01) with no edit here. Read-only and total: nothing writes, spawns
11
11
  * an agent, or creates a directory. Zero runtime dependencies; only `node:` built-ins.
12
12
  */
13
- import { type AgentId, type AgentTarget, type DetectionResult, type ResolveOpts, type Scope } from "./types.js";
13
+ import { type AgentId, type AgentTarget, type Confidence, type DetectionResult, type ResolveOpts, type Scope } from "./types.js";
14
14
  export { AGENT_TARGETS } from "./types.js";
15
15
  /**
16
16
  * Resolve the two filesystem roots all destinations derive from, applying defaults. This is
@@ -30,15 +30,31 @@ export declare function resolveRoots(opts?: ResolveOpts): {
30
30
  * Derive the absolute install destination for one agent under a given scope (REQ-DET-01,
31
31
  * REQ-FLAG-02):
32
32
  *
33
- * <scopeRoot>/<configDirName>/<installSubdir>/<FEATURE_FORGE_NS>/
33
+ * <scopeRoot>/<installBaseDir>/<installSubpath>/<FEATURE_FORGE_NS>/
34
34
  *
35
- * where `scopeRoot` is the home dir for `"global"` and the cwd for `"project"`. The path is
36
- * derived, never stored, so a new agent is one `AGENT_TARGETS` row (REQ-SCALE-01). Pure.
35
+ * where `scopeRoot` is the home dir for `"global"` and the cwd for `"project"`, and an empty
36
+ * `installSubpath` is skipped (the namespace dir sits directly under `installBaseDir`). The path
37
+ * is derived, never stored, so a new agent is one `AGENT_TARGETS` row (REQ-SCALE-01). Pure.
37
38
  *
38
39
  * @example destinationFor(AGENT_TARGETS.claude, "global", { home: "/h" })
39
40
  * // → "/h/.claude/skills/feature-forge"
41
+ * @example destinationFor(AGENT_TARGETS.codex, "project", { cwd: "/p" })
42
+ * // → "/p/.agents/skills/feature-forge"
40
43
  */
41
44
  export declare function destinationFor(target: AgentTarget, scope: Scope, opts?: ResolveOpts): string;
45
+ /**
46
+ * The filesystem containment boundary every write for `target` is checked against (REQ-SEC-02):
47
+ * <scopeRoot>/<installBaseDir>
48
+ * Decoupled from the detection dir so codex (installs under `.agents`) and copilot (`.github`)
49
+ * contain correctly even though they detect on `.codex`/`.copilot`. Pure.
50
+ */
51
+ export declare function agentRootFor(target: AgentTarget, scope: Scope, opts?: ResolveOpts): string;
52
+ /**
53
+ * The effective confidence for `target` under `scope` (A4): the per-row `projectConfidence`
54
+ * override applies only to project scope (e.g. gemini project = best-known); otherwise the
55
+ * row's `confidence`. Pure — surfaced in the report so install honesty is scope-accurate.
56
+ */
57
+ export declare function confidenceFor(target: AgentTarget, scope: Scope): Confidence;
42
58
  /**
43
59
  * Detect a single agent on the host (REQ-DET-02). Detection is decided solely by the presence
44
60
  * of the agent's config dir under the active scope root (a `stat`, never an agent subprocess).
@@ -44,18 +44,41 @@ function scopeRootFor(scope, roots) {
44
44
  * Derive the absolute install destination for one agent under a given scope (REQ-DET-01,
45
45
  * REQ-FLAG-02):
46
46
  *
47
- * <scopeRoot>/<configDirName>/<installSubdir>/<FEATURE_FORGE_NS>/
47
+ * <scopeRoot>/<installBaseDir>/<installSubpath>/<FEATURE_FORGE_NS>/
48
48
  *
49
- * where `scopeRoot` is the home dir for `"global"` and the cwd for `"project"`. The path is
50
- * derived, never stored, so a new agent is one `AGENT_TARGETS` row (REQ-SCALE-01). Pure.
49
+ * where `scopeRoot` is the home dir for `"global"` and the cwd for `"project"`, and an empty
50
+ * `installSubpath` is skipped (the namespace dir sits directly under `installBaseDir`). The path
51
+ * is derived, never stored, so a new agent is one `AGENT_TARGETS` row (REQ-SCALE-01). Pure.
51
52
  *
52
53
  * @example destinationFor(AGENT_TARGETS.claude, "global", { home: "/h" })
53
54
  * // → "/h/.claude/skills/feature-forge"
55
+ * @example destinationFor(AGENT_TARGETS.codex, "project", { cwd: "/p" })
56
+ * // → "/p/.agents/skills/feature-forge"
54
57
  */
55
58
  export function destinationFor(target, scope, opts) {
56
59
  const roots = resolveRoots(opts);
57
60
  const root = scopeRootFor(scope, roots);
58
- return path.resolve(root, target.configDirName, target.installSubdir, FEATURE_FORGE_NS);
61
+ return path.resolve(root, target.installBaseDir, ...(target.installSubpath ? [target.installSubpath] : []), FEATURE_FORGE_NS);
62
+ }
63
+ /**
64
+ * The filesystem containment boundary every write for `target` is checked against (REQ-SEC-02):
65
+ * <scopeRoot>/<installBaseDir>
66
+ * Decoupled from the detection dir so codex (installs under `.agents`) and copilot (`.github`)
67
+ * contain correctly even though they detect on `.codex`/`.copilot`. Pure.
68
+ */
69
+ export function agentRootFor(target, scope, opts) {
70
+ const roots = resolveRoots(opts);
71
+ return path.resolve(scopeRootFor(scope, roots), target.installBaseDir);
72
+ }
73
+ /**
74
+ * The effective confidence for `target` under `scope` (A4): the per-row `projectConfidence`
75
+ * override applies only to project scope (e.g. gemini project = best-known); otherwise the
76
+ * row's `confidence`. Pure — surfaced in the report so install honesty is scope-accurate.
77
+ */
78
+ export function confidenceFor(target, scope) {
79
+ return scope === "project" && target.projectConfidence
80
+ ? target.projectConfidence
81
+ : target.confidence;
59
82
  }
60
83
  /**
61
84
  * Detect a single agent on the host (REQ-DET-02). Detection is decided solely by the presence
@@ -79,6 +102,8 @@ export function detectAgent(id, opts) {
79
102
  detected,
80
103
  configDirsProbed: [configDir],
81
104
  destination: destinationFor(target, scope, opts),
105
+ confidence: confidenceFor(target, scope),
106
+ docsUrl: target.docsUrl,
82
107
  cliOnPath: cliOnPath(id), // advisory only; never gates `detected`
83
108
  };
84
109
  }
package/dist/apply.js CHANGED
@@ -12,11 +12,13 @@
12
12
  * Zero runtime dependencies; only `node:` built-ins.
13
13
  */
14
14
  import * as fsp from "node:fs/promises";
15
+ import * as fs from "node:fs";
15
16
  import * as path from "node:path";
16
17
  import { ok, err } from "./types.js";
17
- import { sha256File } from "./hash.js";
18
+ import { sha256File, sha256String } from "./hash.js";
18
19
  import { resolveWithin, symlinkDir, removePath, removeEmptyDirsWithin, } from "./fsutil.js";
19
20
  import { buildManifest, writeManifest } from "./manifest.js";
21
+ import { wrapBlock, upsertBlock, removeBlock } from "./placements.js";
20
22
  /**
21
23
  * Execute one agent's `PlannedAction` against the filesystem, then write/delete the manifest.
22
24
  * Returns an `AgentReport` instead of throwing (REQ-OBS-03). See spec 04 §5.
@@ -84,8 +86,13 @@ async function applyCopyInstall(planned, ctx) {
84
86
  }
85
87
  }
86
88
  }
87
- // No-op short-circuit (REQ-IDEM-01): every action unchanged zero writes, manifest untouched.
88
- if (planned.files.every((f) => f.action === "unchanged")) {
89
+ // Secondary placements (A4b) execute regardless of the primary mode; collect their inventory.
90
+ const placementResult = await applyPlacements(planned.placements ?? [], ctx, source);
91
+ if (!placementResult.ok)
92
+ return fail(ctx, planned, placementResult.error);
93
+ // No-op short-circuit (REQ-IDEM-01): every action — primary AND placement — unchanged ⇒ zero
94
+ // writes, manifest untouched.
95
+ if (allUnchanged(planned)) {
89
96
  return success(ctx, planned);
90
97
  }
91
98
  const manifest = buildManifest({
@@ -97,6 +104,7 @@ async function applyCopyInstall(planned, ctx) {
97
104
  skills: source.skills,
98
105
  sourceHash: source.sourceHash,
99
106
  raufPin: ctx.raufPin,
107
+ placements: placementResult.value,
100
108
  previous: ctx.priorManifest,
101
109
  now: () => new Date(ctx.now),
102
110
  });
@@ -105,6 +113,11 @@ async function applyCopyInstall(planned, ctx) {
105
113
  return fail(ctx, planned, wrote.error);
106
114
  return success(ctx, planned);
107
115
  }
116
+ /** True iff every planned action — primary files and all placement files — is "unchanged". */
117
+ function allUnchanged(planned) {
118
+ return (planned.files.every((f) => f.action === "unchanged") &&
119
+ (planned.placements ?? []).every((p) => p.files.every((f) => f.action === "unchanged")));
120
+ }
108
121
  /** §5.3 copy-mode uninstall: remove recorded files, prune empty dirs, delete the manifest LAST. */
109
122
  async function applyCopyUninstall(planned, ctx) {
110
123
  for (const fa of planned.files) {
@@ -118,6 +131,9 @@ async function applyCopyUninstall(planned, ctx) {
118
131
  const pruned = await removeEmptyDirsWithin(ctx.destination, ctx.agentRoot);
119
132
  if (!pruned.ok)
120
133
  return fail(ctx, planned, pruned.error);
134
+ const placementsRemoved = await removePlacements(planned.placements ?? []);
135
+ if (!placementsRemoved.ok)
136
+ return fail(ctx, planned, placementsRemoved.error);
121
137
  const deleted = await deleteManifest(ctx);
122
138
  if (!deleted.ok)
123
139
  return fail(ctx, planned, deleted.error);
@@ -140,23 +156,35 @@ async function applySymlinkInstall(planned, ctx) {
140
156
  if (!resolved.ok)
141
157
  return fail(ctx, planned, resolved.error);
142
158
  const linkPath = resolved.value;
143
- // Unchanged (live link already points at the same target) zero writes, manifest untouched.
144
- if (planned.files.every((f) => f.action === "unchanged")) {
159
+ // Secondary placements (A4b) apply even in symlink mode they live under a different root than the
160
+ // symlinked namespace dir. Run them first so a placement-only change still rewrites the manifest.
161
+ const placementResult = await applyPlacements(planned.placements ?? [], ctx, source);
162
+ if (!placementResult.ok)
163
+ return fail(ctx, planned, placementResult.error);
164
+ const primary = planned.files;
165
+ const primaryUntouched = primary.every((f) => f.action === "unchanged") ||
166
+ primary.every((f) => f.action === "skip-modified");
167
+ // Nothing changed anywhere ⇒ zero writes, manifest untouched.
168
+ if (allUnchanged(planned)) {
145
169
  return success(ctx, planned);
146
170
  }
147
- // skip-modified (prior exists, no --force) leave it; report, write nothing.
148
- if (planned.files.every((f) => f.action === "skip-modified")) {
149
- return success(ctx, planned);
171
+ // The recorded mode/link reflect the primary namespace dir: only (re)link when it actually changed
172
+ // (a placement-only change leaves a live link and its prior manifest mode/link intact).
173
+ let effectiveMode = ctx.priorManifest?.mode ?? "symlink";
174
+ let linkTarget = ctx.priorManifest?.link?.target ?? (effectiveMode === "symlink" ? source.root : undefined);
175
+ let files = ctx.priorManifest?.files ?? source.files.map((f) => ({ path: f.relpath }));
176
+ if (!primaryUntouched) {
177
+ const removed = await removePath(linkPath);
178
+ if (!removed.ok)
179
+ return fail(ctx, planned, removed.error);
180
+ const linked = await symlinkDir(source.root, linkPath);
181
+ if (!linked.ok)
182
+ return fail(ctx, planned, linked.error);
183
+ effectiveMode = linked.value.mode;
184
+ linkTarget = effectiveMode === "symlink" ? source.root : undefined;
185
+ // files[] lists the bundle-relative paths with sha256 OMITTED (no per-file copy exists, 00 §3).
186
+ files = source.files.map((f) => ({ path: f.relpath }));
150
187
  }
151
- const removed = await removePath(linkPath);
152
- if (!removed.ok)
153
- return fail(ctx, planned, removed.error);
154
- const linked = await symlinkDir(source.root, linkPath);
155
- if (!linked.ok)
156
- return fail(ctx, planned, linked.error);
157
- const effectiveMode = linked.value.mode;
158
- // files[] lists the bundle-relative paths with sha256 OMITTED (no per-file copy exists, 00 §3).
159
- const files = source.files.map((f) => ({ path: f.relpath }));
160
188
  const manifest = buildManifest({
161
189
  agent: ctx.agent,
162
190
  scope: ctx.scope,
@@ -166,8 +194,9 @@ async function applySymlinkInstall(planned, ctx) {
166
194
  skills: source.skills,
167
195
  sourceHash: source.sourceHash,
168
196
  raufPin: ctx.raufPin,
197
+ placements: placementResult.value,
169
198
  // Truthful record: a copy fallback must NOT carry link (copy-mode manifest invariant, 05).
170
- ...(effectiveMode === "symlink" ? { link: { target: source.root } } : {}),
199
+ ...(linkTarget !== undefined ? { link: { target: linkTarget } } : {}),
171
200
  previous: ctx.priorManifest,
172
201
  now: () => new Date(ctx.now),
173
202
  });
@@ -184,12 +213,210 @@ async function applySymlinkUninstall(planned, ctx) {
184
213
  const removed = await removePath(resolved.value);
185
214
  if (!removed.ok)
186
215
  return fail(ctx, planned, removed.error);
216
+ const placementsRemoved = await removePlacements(planned.placements ?? []);
217
+ if (!placementsRemoved.ok)
218
+ return fail(ctx, planned, placementsRemoved.error);
187
219
  const deleted = await deleteManifest(ctx);
188
220
  if (!deleted.ok)
189
221
  return fail(ctx, planned, deleted.error);
190
222
  return success(ctx, planned);
191
223
  }
192
224
  // ---------------------------------------------------------------------------
225
+ // Secondary placements (A4b)
226
+ // ---------------------------------------------------------------------------
227
+ /**
228
+ * Execute every secondary placement for an install/update and return the inventory to record in the
229
+ * manifest. Each placement is contained to ITS OWN root (`resolveWithin(placement.root, …)`), so a
230
+ * mirror under `.codex` and a managed block under `.github` never escape their boundary (REQ-SEC-02).
231
+ * Unchanged/skip-modified entries carry their prior recorded hash forward so the manifest stays
232
+ * faithful. Never throws for expected errors.
233
+ */
234
+ async function applyPlacements(placements, ctx, source) {
235
+ const priorByDest = new Map();
236
+ for (const p of ctx.priorManifest?.placements ?? [])
237
+ priorByDest.set(p.destination, p);
238
+ const out = [];
239
+ for (const pl of placements) {
240
+ const prior = priorByDest.get(pl.destination) ?? null;
241
+ const res = pl.kind === "mirror"
242
+ ? await applyMirror(pl, ctx, source, prior)
243
+ : await applyManagedBlock(pl, prior);
244
+ if (!res.ok)
245
+ return res;
246
+ out.push(res.value);
247
+ }
248
+ return ok(out);
249
+ }
250
+ /** §A4b mirror: copy/refresh/remove flat files under the second root; record per-file sha256. */
251
+ async function applyMirror(pl, ctx, source, prior) {
252
+ const priorByPath = new Map();
253
+ for (const f of prior?.files ?? [])
254
+ priorByPath.set(f.path, f);
255
+ const writeFile = ctx.writeFileSeam ?? defaultCopyFile;
256
+ const inventory = [];
257
+ for (const fa of pl.files) {
258
+ const resolved = resolveWithin(pl.root, pl.destination, fa.relpath);
259
+ if (!resolved.ok)
260
+ return resolved;
261
+ const destAbs = resolved.value;
262
+ switch (fa.action) {
263
+ case "create":
264
+ case "overwrite": {
265
+ if (fa.srcRelpath === undefined) {
266
+ return err({
267
+ code: "UNEXPECTED",
268
+ agent: ctx.agent,
269
+ message: `mirror action for "${fa.relpath}" is missing its source path`,
270
+ });
271
+ }
272
+ const wrote = await writeFile(path.join(source.root, fa.srcRelpath), destAbs);
273
+ if (!wrote.ok)
274
+ return wrote;
275
+ inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
276
+ break;
277
+ }
278
+ case "remove": {
279
+ const removed = await removePath(destAbs);
280
+ if (!removed.ok)
281
+ return removed;
282
+ break;
283
+ }
284
+ case "unchanged":
285
+ case "skip-modified": {
286
+ // Carry the prior record forward; if none exists (e.g. a v1→v2 manifest migration where the
287
+ // 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.
289
+ const p = priorByPath.get(fa.relpath);
290
+ if (p !== undefined)
291
+ inventory.push(p);
292
+ else
293
+ inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
294
+ break;
295
+ }
296
+ }
297
+ }
298
+ return ok({ kind: "mirror", root: pl.root, destination: pl.destination, files: inventory });
299
+ }
300
+ /**
301
+ * §A4b managed-block: merge/refresh the sentinel block into the (possibly user-owned) target file,
302
+ * preserving everything outside the sentinels. Records a single inventory entry whose sha256 is the
303
+ * written region's hash. skip-modified/unchanged carry the prior record forward (no write).
304
+ */
305
+ async function applyManagedBlock(pl, prior) {
306
+ const fa = pl.files[0];
307
+ const basename = fa?.relpath ?? path.basename(pl.destination);
308
+ const resolved = resolveWithin(pl.root, pl.destination);
309
+ if (!resolved.ok)
310
+ return resolved;
311
+ const fileAbs = resolved.value;
312
+ const carry = () => {
313
+ const p = prior?.files.find((f) => f.path === basename);
314
+ return {
315
+ kind: "managed-block",
316
+ root: pl.root,
317
+ destination: pl.destination,
318
+ files: p !== undefined ? [p] : [],
319
+ };
320
+ };
321
+ if (fa === undefined || fa.action === "unchanged" || fa.action === "skip-modified") {
322
+ return ok(carry());
323
+ }
324
+ if (fa.action === "remove") {
325
+ // Uninstall is handled by removePlacements; an install-plan never yields "remove" here.
326
+ return ok(carry());
327
+ }
328
+ // create | overwrite — read existing (or treat as empty), upsert the block, write back.
329
+ const body = pl.blockContent ?? "";
330
+ let existing = "";
331
+ try {
332
+ existing = fs.readFileSync(fileAbs, "utf8");
333
+ }
334
+ catch {
335
+ existing = "";
336
+ }
337
+ const next = upsertBlock(existing, body);
338
+ const wrote = await writeText(fileAbs, next);
339
+ if (!wrote.ok)
340
+ return wrote;
341
+ return ok({
342
+ kind: "managed-block",
343
+ root: pl.root,
344
+ destination: pl.destination,
345
+ files: [{ path: basename, sha256: sha256String(wrapBlock(body)) }],
346
+ });
347
+ }
348
+ /**
349
+ * Remove every secondary placement during uninstall (A4b): a "mirror" deletes each recorded file
350
+ * (and prunes its now-empty dir); a "managed-block" strips ONLY the sentinel region, deleting the
351
+ * file only if nothing else remains. User content outside the block is always preserved.
352
+ */
353
+ async function removePlacements(placements) {
354
+ for (const pl of placements) {
355
+ if (pl.kind === "managed-block") {
356
+ const resolved = resolveWithin(pl.root, pl.destination);
357
+ if (!resolved.ok)
358
+ return resolved;
359
+ const fileAbs = resolved.value;
360
+ let existing;
361
+ try {
362
+ existing = fs.readFileSync(fileAbs, "utf8");
363
+ }
364
+ catch {
365
+ continue; // already gone — nothing to strip
366
+ }
367
+ const stripped = removeBlock(existing);
368
+ if (stripped === "") {
369
+ const removed = await removePath(fileAbs);
370
+ if (!removed.ok)
371
+ return removed;
372
+ }
373
+ else {
374
+ const wrote = await writeText(fileAbs, stripped);
375
+ if (!wrote.ok)
376
+ return wrote;
377
+ }
378
+ continue;
379
+ }
380
+ // mirror: remove each recorded file, then prune the now-empty destination dir.
381
+ for (const fa of pl.files) {
382
+ const resolved = resolveWithin(pl.root, pl.destination, fa.relpath);
383
+ if (!resolved.ok)
384
+ return resolved;
385
+ const removed = await removePath(resolved.value);
386
+ if (!removed.ok)
387
+ return removed;
388
+ }
389
+ const pruned = await removeEmptyDirsWithin(pl.destination, pl.root);
390
+ if (!pruned.ok)
391
+ return pruned;
392
+ }
393
+ return ok(undefined);
394
+ }
395
+ /** Write text content to `destAbs`, creating the parent dir; maps EACCES/EPERM to WRITE_DENIED. */
396
+ async function writeText(destAbs, content) {
397
+ try {
398
+ await fsp.mkdir(path.dirname(destAbs), { recursive: true });
399
+ await fsp.writeFile(destAbs, content, "utf8");
400
+ return ok(undefined);
401
+ }
402
+ catch (e) {
403
+ const code = e?.code;
404
+ if (code === "EACCES" || code === "EPERM") {
405
+ return err({
406
+ code: "WRITE_DENIED",
407
+ message: `no write permission to ${destAbs}`,
408
+ path: destAbs,
409
+ remedy: "check directory permissions, or choose a different scope (--global vs project)",
410
+ });
411
+ }
412
+ return err({
413
+ code: "UNEXPECTED",
414
+ message: `filesystem error at ${destAbs}: ${e?.message ?? String(e)}`,
415
+ path: destAbs,
416
+ });
417
+ }
418
+ }
419
+ // ---------------------------------------------------------------------------
193
420
  // Internal helpers
194
421
  // ---------------------------------------------------------------------------
195
422
  /** Default per-file write seam: ensure the parent dir, then copy the source bytes. */
package/dist/cli.js CHANGED
@@ -16,7 +16,8 @@ import { readFileSync, realpathSync } from "node:fs";
16
16
  import { pathToFileURL } from "node:url";
17
17
  import * as path from "node:path";
18
18
  import { AGENT_IDS, AGENT_TARGETS, EXIT, err, ok, } from "./types.js";
19
- import { detectAgent, detectAgents, resolveRoots } from "./agent-targets.js"; // 02
19
+ import { detectAgent, detectAgents, agentRootFor } from "./agent-targets.js"; // 02
20
+ import { resolvePlacements } from "./placements.js"; // 02 (A4b second-root placements)
20
21
  import { locateSource } from "./source.js"; // 03
21
22
  import { plan, resolveMode } from "./plan.js"; // 04
22
23
  import { apply } from "./apply.js"; // 04
@@ -254,7 +255,8 @@ async function runMutation(subcommand, flags, env) {
254
255
  for (const agent of targets) {
255
256
  const detection = detectAgent(agent, ropts);
256
257
  const r = await runOneAgent(subcommand, agent, detection, flags, scope, mode, raufPin, env);
257
- agentReports.push(r);
258
+ // Carry the scope-effective confidence + docs URL onto the report for honest labeling (A4).
259
+ agentReports.push({ ...r, confidence: detection.confidence, docsUrl: detection.docsUrl });
258
260
  }
259
261
  const anyAgentFailed = agentReports.some((r) => !r.ok);
260
262
  const exitCode = anyAgentFailed || raufError !== undefined ? EXIT.FAILURE : EXIT.SUCCESS;
@@ -274,9 +276,9 @@ async function runMutation(subcommand, flags, env) {
274
276
  /** Run the pipeline for a single agent, returning its AgentReport (catches every expected error). */
275
277
  async function runOneAgent(subcommand, agent, detection, flags, scope, mode, raufPin, env) {
276
278
  const mpath = manifestPath(agent, scope, { home: env.home, cwd: env.cwd });
277
- const roots = resolveRoots({ home: env.home, cwd: env.cwd, scope });
278
- const scopeRoot = scope === "global" ? roots.home : roots.cwd;
279
- const agentRoot = path.join(scopeRoot, AGENT_TARGETS[agent].configDirName);
279
+ // Containment boundary = the agent's install base dir (A4: decoupled from the detection dir,
280
+ // so codex contains under `.agents` and copilot under `.github`).
281
+ const agentRoot = agentRootFor(AGENT_TARGETS[agent], scope, { home: env.home, cwd: env.cwd });
280
282
  // uninstall path: manifest → planUninstall → apply.
281
283
  if (subcommand === "uninstall") {
282
284
  const m = readManifest(mpath);
@@ -319,6 +321,9 @@ async function runOneAgent(subcommand, agent, detection, flags, scope, mode, rau
319
321
  priorManifest: prior.value,
320
322
  force: flags.force,
321
323
  raufPin,
324
+ // A4b: resolve any second-root placements for this agent under the active scope (codex
325
+ // `.codex/agents`, copilot `.github/copilot-instructions.md`); empty for the rest.
326
+ placements: resolvePlacements(AGENT_TARGETS[agent], scope, { home: env.home, cwd: env.cwd }),
322
327
  };
323
328
  const planned = plan(subcommand, planCtx);
324
329
  if (!planned.ok)
@@ -367,7 +372,8 @@ async function runList(flags, env) {
367
372
  const agentReports = [];
368
373
  for (const agent of targets) {
369
374
  const detection = detectAgent(agent, ropts);
370
- agentReports.push(listOneAgent(agent, detection, flags, scope, env));
375
+ const base = listOneAgent(agent, detection, flags, scope, env);
376
+ agentReports.push({ ...base, confidence: detection.confidence, docsUrl: detection.docsUrl });
371
377
  }
372
378
  const anyFailed = agentReports.some((r) => !r.ok);
373
379
  return {