@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.
- package/README.md +19 -1
- package/adapters/GENERATION-REPORT.md +12 -12
- package/adapters/claude/.feature-forge-bundle.json +6 -0
- package/adapters/claude/references/forge-config-schema.json +2 -2
- package/adapters/claude/references/portable-root.md +8 -5
- package/adapters/claude/references/process-overview.md +1 -1
- package/adapters/claude/references/shared-conventions.md +24 -5
- package/adapters/claude/references/stack-resolution.md +4 -1
- package/adapters/claude/references/stacks/go.md +1 -1
- package/adapters/claude/references/stacks/python.md +1 -1
- package/adapters/claude/references/stacks/rust.md +1 -1
- package/adapters/claude/references/stacks/typescript.md +1 -1
- package/adapters/claude/scripts/epic-manifest.py +1379 -0
- package/adapters/claude/scripts/forge-bootstrap.py +991 -0
- package/adapters/claude/scripts/forge-init.sh +44 -0
- package/adapters/claude/scripts/forge-root.sh +30 -8
- package/adapters/claude/scripts/validate-traceability.py +150 -0
- package/adapters/claude/skills/forge/SKILL.md +5 -5
- package/adapters/claude/skills/forge-0-epic/SKILL.md +6 -10
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/claude/skills/forge-1-prd/SKILL.md +2 -2
- package/adapters/claude/skills/forge-2-tech/SKILL.md +8 -7
- package/adapters/claude/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/claude/skills/forge-3-specs/SKILL.md +1 -1
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +2 -2
- package/adapters/claude/skills/forge-5-loop/SKILL.md +2 -2
- package/adapters/claude/skills/forge-6-docs/SKILL.md +2 -2
- package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
- package/adapters/claude/skills/forge-fix/SKILL.md +1 -1
- package/adapters/claude/skills/forge-init/SKILL.md +1 -1
- package/adapters/claude/skills/forge-verify/SKILL.md +7 -2
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/codex/.feature-forge-bundle.json +6 -0
- package/adapters/codex/agents/{forge-researcher.md → forge-researcher.toml} +4 -4
- package/adapters/codex/agents/{forge-spec-writer.md → forge-spec-writer.toml} +4 -4
- package/adapters/codex/agents/{forge-verifier.md → forge-verifier.toml} +4 -4
- package/adapters/codex/references/forge-config-schema.json +2 -2
- package/adapters/codex/references/portable-root.md +8 -5
- package/adapters/codex/references/process-overview.md +1 -1
- package/adapters/codex/references/shared-conventions.md +24 -5
- package/adapters/codex/references/stack-resolution.md +4 -1
- package/adapters/codex/references/stacks/go.md +1 -1
- package/adapters/codex/references/stacks/python.md +1 -1
- package/adapters/codex/references/stacks/rust.md +1 -1
- package/adapters/codex/references/stacks/typescript.md +1 -1
- package/adapters/codex/scripts/epic-manifest.py +1379 -0
- package/adapters/codex/scripts/forge-bootstrap.py +991 -0
- package/adapters/codex/scripts/forge-init.sh +44 -0
- package/adapters/codex/scripts/forge-root.sh +30 -8
- package/adapters/codex/scripts/validate-traceability.py +150 -0
- package/adapters/codex/skills/forge/{forge.md → SKILL.md} +16 -6
- package/adapters/codex/skills/forge-0-epic/{forge-0-epic.md → SKILL.md} +26 -20
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/codex/skills/forge-1-prd/{forge-1-prd.md → SKILL.md} +18 -8
- package/adapters/codex/skills/forge-2-tech/{forge-2-tech.md → SKILL.md} +26 -15
- package/adapters/codex/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/codex/skills/forge-3-specs/{forge-3-specs.md → SKILL.md} +16 -6
- package/adapters/codex/skills/forge-4-backlog/{forge-4-backlog.md → SKILL.md} +15 -5
- package/adapters/codex/skills/forge-5-loop/{forge-5-loop.md → SKILL.md} +27 -17
- package/adapters/codex/skills/forge-6-docs/{forge-6-docs.md → SKILL.md} +17 -7
- package/adapters/codex/skills/forge-bootstrap/{forge-bootstrap.md → SKILL.md} +17 -7
- package/adapters/codex/skills/forge-fix/{forge-fix.md → SKILL.md} +12 -2
- package/adapters/codex/skills/forge-init/{forge-init.md → SKILL.md} +11 -1
- package/adapters/codex/skills/forge-verify/{forge-verify.md → SKILL.md} +24 -9
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/copilot/.feature-forge-bundle.json +6 -0
- package/adapters/copilot/references/forge-config-schema.json +2 -2
- package/adapters/copilot/references/portable-root.md +8 -5
- package/adapters/copilot/references/process-overview.md +1 -1
- package/adapters/copilot/references/shared-conventions.md +24 -5
- package/adapters/copilot/references/stack-resolution.md +4 -1
- package/adapters/copilot/references/stacks/go.md +1 -1
- package/adapters/copilot/references/stacks/python.md +1 -1
- package/adapters/copilot/references/stacks/rust.md +1 -1
- package/adapters/copilot/references/stacks/typescript.md +1 -1
- package/adapters/copilot/scripts/epic-manifest.py +1379 -0
- package/adapters/copilot/scripts/forge-bootstrap.py +991 -0
- package/adapters/copilot/scripts/forge-init.sh +44 -0
- package/adapters/copilot/scripts/forge-root.sh +30 -8
- package/adapters/copilot/scripts/validate-traceability.py +150 -0
- package/adapters/copilot/skills/forge/forge.md +16 -6
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +26 -20
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +18 -8
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +26 -15
- package/adapters/copilot/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +16 -6
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +15 -5
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +27 -17
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +17 -7
- package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +17 -7
- package/adapters/copilot/skills/forge-fix/forge-fix.md +12 -2
- package/adapters/copilot/skills/forge-init/forge-init.md +11 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +24 -9
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/cursor/.feature-forge-bundle.json +6 -0
- package/adapters/cursor/references/forge-config-schema.json +2 -2
- package/adapters/cursor/references/portable-root.md +8 -5
- package/adapters/cursor/references/process-overview.md +1 -1
- package/adapters/cursor/references/shared-conventions.md +24 -5
- package/adapters/cursor/references/stack-resolution.md +4 -1
- package/adapters/cursor/references/stacks/go.md +1 -1
- package/adapters/cursor/references/stacks/python.md +1 -1
- package/adapters/cursor/references/stacks/rust.md +1 -1
- package/adapters/cursor/references/stacks/typescript.md +1 -1
- package/adapters/cursor/scripts/epic-manifest.py +1379 -0
- package/adapters/cursor/scripts/forge-bootstrap.py +991 -0
- package/adapters/cursor/scripts/forge-init.sh +44 -0
- package/adapters/cursor/scripts/forge-root.sh +30 -8
- package/adapters/cursor/scripts/validate-traceability.py +150 -0
- package/adapters/cursor/skills/forge/forge.mdc +16 -6
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +26 -20
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +18 -8
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +26 -15
- package/adapters/cursor/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +16 -6
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +15 -5
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +27 -17
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +17 -7
- package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +17 -7
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +12 -2
- package/adapters/cursor/skills/forge-init/forge-init.mdc +11 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +24 -9
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/gemini/.feature-forge-bundle.json +6 -0
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/forge-config-schema.json +2 -2
- package/adapters/gemini/references/portable-root.md +8 -5
- package/adapters/gemini/references/process-overview.md +1 -1
- package/adapters/gemini/references/shared-conventions.md +24 -5
- package/adapters/gemini/references/stack-resolution.md +4 -1
- package/adapters/gemini/references/stacks/go.md +1 -1
- package/adapters/gemini/references/stacks/python.md +1 -1
- package/adapters/gemini/references/stacks/rust.md +1 -1
- package/adapters/gemini/references/stacks/typescript.md +1 -1
- package/adapters/gemini/scripts/epic-manifest.py +1379 -0
- package/adapters/gemini/scripts/forge-bootstrap.py +991 -0
- package/adapters/gemini/scripts/forge-init.sh +44 -0
- package/adapters/gemini/scripts/forge-root.sh +30 -8
- package/adapters/gemini/scripts/validate-traceability.py +150 -0
- package/adapters/gemini/skills/forge/forge.md +16 -6
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +26 -20
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +18 -8
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +26 -15
- package/adapters/gemini/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +16 -6
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +15 -5
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +27 -17
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +17 -7
- package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +17 -7
- package/adapters/gemini/skills/forge-fix/forge-fix.md +12 -2
- package/adapters/gemini/skills/forge-init/forge-init.md +11 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +24 -9
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
- package/dist/agent-targets.d.ts +20 -4
- package/dist/agent-targets.js +29 -4
- package/dist/apply.js +245 -18
- package/dist/cli.js +12 -6
- package/dist/hash.d.ts +5 -0
- package/dist/hash.js +7 -0
- package/dist/manifest.d.ts +4 -2
- package/dist/manifest.js +58 -2
- package/dist/placements.d.ts +69 -0
- package/dist/placements.js +116 -0
- package/dist/plan.d.ts +7 -0
- package/dist/plan.js +87 -1
- package/dist/rauf.d.ts +4 -4
- package/dist/rauf.js +3 -3
- package/dist/report.js +21 -0
- package/dist/source.d.ts +4 -3
- package/dist/source.js +4 -3
- package/dist/types.d.ts +163 -19
- package/dist/types.js +42 -11
- package/package.json +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
23
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
```
|
package/dist/agent-targets.d.ts
CHANGED
|
@@ -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>/<
|
|
33
|
+
* <scopeRoot>/<installBaseDir>/<installSubpath>/<FEATURE_FORGE_NS>/
|
|
34
34
|
*
|
|
35
|
-
* where `scopeRoot` is the home dir for `"global"` and the cwd for `"project"
|
|
36
|
-
*
|
|
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).
|
package/dist/agent-targets.js
CHANGED
|
@@ -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>/<
|
|
47
|
+
* <scopeRoot>/<installBaseDir>/<installSubpath>/<FEATURE_FORGE_NS>/
|
|
48
48
|
*
|
|
49
|
-
* where `scopeRoot` is the home dir for `"global"` and the cwd for `"project"
|
|
50
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
88
|
-
|
|
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
|
-
//
|
|
144
|
-
|
|
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
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
...(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
const agentRoot =
|
|
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
|
-
|
|
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 {
|