@garygentry/feature-forge 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/adapters/GENERATION-REPORT.md +5 -1
- package/adapters/claude/references/forge-config-schema.json +25 -3
- package/adapters/claude/references/pipeline-state-schema.json +3 -2
- package/adapters/claude/references/portable-root.md +2 -2
- package/adapters/claude/references/process-overview.md +10 -0
- package/adapters/claude/references/shared-conventions.md +14 -9
- package/adapters/claude/references/stage-exit-protocol.md +99 -0
- package/adapters/claude/scripts/epic-manifest.py +10 -0
- package/adapters/claude/scripts/forge-bootstrap.py +94 -16
- package/adapters/claude/scripts/forge-init.sh +7 -1
- package/adapters/claude/scripts/forge-session.py +175 -30
- package/adapters/claude/skills/forge/SKILL.md +28 -14
- package/adapters/claude/skills/forge-0-epic/SKILL.md +20 -15
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +6 -4
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/claude/skills/forge-1-prd/SKILL.md +14 -4
- package/adapters/claude/skills/forge-2-tech/SKILL.md +14 -3
- package/adapters/claude/skills/forge-3-specs/SKILL.md +14 -3
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +16 -5
- package/adapters/claude/skills/forge-5-loop/SKILL.md +19 -21
- package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/claude/skills/forge-6-docs/SKILL.md +6 -6
- package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
- package/adapters/claude/skills/forge-fix/SKILL.md +27 -6
- package/adapters/claude/skills/forge-guide/SKILL.md +179 -0
- package/adapters/claude/skills/forge-init/SKILL.md +28 -1
- package/adapters/claude/skills/forge-verify/SKILL.md +46 -15
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/codex/references/forge-config-schema.json +25 -3
- package/adapters/codex/references/pipeline-state-schema.json +3 -2
- package/adapters/codex/references/portable-root.md +2 -2
- package/adapters/codex/references/process-overview.md +10 -0
- package/adapters/codex/references/shared-conventions.md +14 -9
- package/adapters/codex/references/stage-exit-protocol.md +99 -0
- package/adapters/codex/scripts/epic-manifest.py +10 -0
- package/adapters/codex/scripts/forge-bootstrap.py +94 -16
- package/adapters/codex/scripts/forge-init.sh +7 -1
- package/adapters/codex/scripts/forge-session.py +175 -30
- package/adapters/codex/skills/forge/SKILL.md +33 -19
- package/adapters/codex/skills/forge-0-epic/SKILL.md +21 -16
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +6 -4
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/codex/skills/forge-1-prd/SKILL.md +14 -4
- package/adapters/codex/skills/forge-2-tech/SKILL.md +15 -4
- package/adapters/codex/skills/forge-3-specs/SKILL.md +14 -3
- package/adapters/codex/skills/forge-4-backlog/SKILL.md +16 -5
- package/adapters/codex/skills/forge-5-loop/SKILL.md +21 -23
- package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/codex/skills/forge-6-docs/SKILL.md +6 -6
- package/adapters/codex/skills/forge-bootstrap/SKILL.md +4 -4
- package/adapters/codex/skills/forge-fix/SKILL.md +27 -6
- package/adapters/codex/skills/forge-guide/SKILL.md +188 -0
- package/adapters/codex/skills/forge-init/SKILL.md +28 -1
- package/adapters/codex/skills/forge-verify/SKILL.md +45 -14
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/copilot/references/forge-config-schema.json +25 -3
- package/adapters/copilot/references/pipeline-state-schema.json +3 -2
- package/adapters/copilot/references/portable-root.md +2 -2
- package/adapters/copilot/references/process-overview.md +10 -0
- package/adapters/copilot/references/shared-conventions.md +14 -9
- package/adapters/copilot/references/stage-exit-protocol.md +99 -0
- package/adapters/copilot/scripts/epic-manifest.py +10 -0
- package/adapters/copilot/scripts/forge-bootstrap.py +94 -16
- package/adapters/copilot/scripts/forge-init.sh +7 -1
- package/adapters/copilot/scripts/forge-session.py +175 -30
- package/adapters/copilot/skills/forge/forge.md +33 -19
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +21 -16
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +6 -4
- 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 +14 -4
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +15 -4
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +14 -3
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +16 -5
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +21 -23
- package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +6 -6
- package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +4 -4
- package/adapters/copilot/skills/forge-fix/forge-fix.md +27 -6
- package/adapters/copilot/skills/forge-guide/forge-guide.md +188 -0
- package/adapters/copilot/skills/forge-init/forge-init.md +28 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +45 -14
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/cursor/references/forge-config-schema.json +25 -3
- package/adapters/cursor/references/pipeline-state-schema.json +3 -2
- package/adapters/cursor/references/portable-root.md +2 -2
- package/adapters/cursor/references/process-overview.md +10 -0
- package/adapters/cursor/references/shared-conventions.md +14 -9
- package/adapters/cursor/references/stage-exit-protocol.md +99 -0
- package/adapters/cursor/scripts/epic-manifest.py +10 -0
- package/adapters/cursor/scripts/forge-bootstrap.py +94 -16
- package/adapters/cursor/scripts/forge-init.sh +7 -1
- package/adapters/cursor/scripts/forge-session.py +175 -30
- package/adapters/cursor/skills/forge/forge.mdc +33 -19
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +21 -16
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +6 -4
- 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 +14 -4
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +15 -4
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +14 -3
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +16 -5
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +21 -23
- package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +6 -6
- package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +4 -4
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +27 -6
- package/adapters/cursor/skills/forge-guide/forge-guide.mdc +189 -0
- package/adapters/cursor/skills/forge-init/forge-init.mdc +28 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +45 -14
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/gemini/gemini-extension.json +4 -0
- package/adapters/gemini/references/forge-config-schema.json +25 -3
- package/adapters/gemini/references/pipeline-state-schema.json +3 -2
- package/adapters/gemini/references/portable-root.md +2 -2
- package/adapters/gemini/references/process-overview.md +10 -0
- package/adapters/gemini/references/shared-conventions.md +14 -9
- package/adapters/gemini/references/stage-exit-protocol.md +99 -0
- package/adapters/gemini/scripts/epic-manifest.py +10 -0
- package/adapters/gemini/scripts/forge-bootstrap.py +94 -16
- package/adapters/gemini/scripts/forge-init.sh +7 -1
- package/adapters/gemini/scripts/forge-session.py +175 -30
- package/adapters/gemini/skills/forge/forge.md +33 -19
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +21 -16
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +6 -4
- 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 +14 -4
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +15 -4
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +14 -3
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +16 -5
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +21 -23
- package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +6 -6
- package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +4 -4
- package/adapters/gemini/skills/forge-fix/forge-fix.md +27 -6
- package/adapters/gemini/skills/forge-guide/forge-guide.md +188 -0
- package/adapters/gemini/skills/forge-init/forge-init.md +28 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +45 -14
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
- package/dist/apply.js +34 -8
- package/dist/cli.js +40 -4
- package/dist/fsutil.d.ts +0 -12
- package/dist/fsutil.js +10 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/plan.js +22 -2
- package/dist/rauf.d.ts +4 -4
- package/dist/rauf.js +3 -3
- package/dist/report.js +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -71,6 +71,9 @@ Extract these config values (use defaults if not present):
|
|
|
71
71
|
- `autoInvokeNextStage` (default: `true` — the `/feature-forge:forge` navigator auto-invokes the next stage via the `Skill` tool after the user confirms; `false` keeps copy-paste behavior. Navigator-only.)
|
|
72
72
|
- `contextWindowTokens` (default: `null` — context window used by the navigator's context-usage check; `null` infers from the session model and falls back to 200000. Set to the model's window, e.g. `1000000` on a 1M model. Navigator-only.)
|
|
73
73
|
- `contextWarnThreshold` (default: `0.7` — fraction of the window past which the navigator recommends a clean session. Navigator-only.)
|
|
74
|
+
- `autoVerify` (default: `false` — when `true`, the navigator auto-runs `forge-verify` after a stage completes, no prompt; runs in a fresh clean-room subagent so it never needs a `/clear` and costs only a compact digest. Navigator-only.)
|
|
75
|
+
- `autoVerifyStages` (default: `{}` — per-stage overrides for `autoVerify`, e.g. `{"forge-1-prd": false}`. Effective value = `autoVerifyStages[stage]` if present, else `autoVerify`. Keys are constrained to the five verify-capable stages; a typo is a config error surfaced as `invalidAutoVerifyKeys`. Navigator-only.)
|
|
76
|
+
- `autoFix` (default: `false` — when `true`, the navigator chains `forge-fix` after an auto-verify that finds issues, but only when auto-verify is on for that stage AND preconditions hold (zero unresolved decisions, clean tree, passing re-verify); otherwise it surfaces a digest and prompts. Navigator-only.)
|
|
74
77
|
- `loopRunner` (optional object — the loop runner to drive; **defaults to rauf** when absent, with every command templated. See `references/forge-config-schema.json` and `references/ralph-loop-contract.md`.)
|
|
75
78
|
|
|
76
79
|
## Feature Directory Resolution
|
|
@@ -78,7 +81,7 @@ Extract these config values (use defaults if not present):
|
|
|
78
81
|
Before any file I/O against a feature's artifacts, resolve its directory through the deterministic helper rather than hardcoding `{specsDir}/{feature}/`. This makes flat (`{specsDir}/{feature}/`) and nested (`{specsDir}/{epic}/{feature}/`) layouts both resolve from a bare feature name (REQ-DIR-03), with standalone features behaving exactly as today.
|
|
79
82
|
|
|
80
83
|
```bash
|
|
81
|
-
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)"
|
|
84
|
+
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
|
|
82
85
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
83
86
|
resolvedFeatureDir=$(python3 "$R/scripts/epic-manifest.py" \
|
|
84
87
|
resolve "<feature>" --specs-dir "<specsDir>")
|
|
@@ -108,7 +111,7 @@ Whenever a stage creates the specs tree for the first time (the first PRD or epi
|
|
|
108
111
|
Run this after creating the feature/epic directory, before the stage's git commit:
|
|
109
112
|
|
|
110
113
|
```bash
|
|
111
|
-
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)"
|
|
114
|
+
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
|
|
112
115
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
113
116
|
mkdir -p "<specsDir>"
|
|
114
117
|
[ -f "<specsDir>/AGENTS.md" ] || cp "$R/references/templates/specs-hygiene/AGENTS.md" "<specsDir>/AGENTS.md"
|
|
@@ -135,7 +138,7 @@ After resolving the feature directory, check the feature's `.pipeline-state.json
|
|
|
135
138
|
To obtain the manifest contracts and the live completion status of each dependency in one deterministic call, run `render-status` and read the per-feature `status` and the `consumes`/`exposes` arrays rather than re-deriving them:
|
|
136
139
|
|
|
137
140
|
```bash
|
|
138
|
-
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)"
|
|
141
|
+
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
|
|
139
142
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
140
143
|
python3 "$R/scripts/epic-manifest.py" \
|
|
141
144
|
render-status "<epic>" --specs-dir "<specsDir>" --json
|
|
@@ -180,16 +183,18 @@ Invoke this block at the **very start** of a pipeline entry point — `forge-1-p
|
|
|
180
183
|
|
|
181
184
|
## Git Commit Protocol
|
|
182
185
|
|
|
183
|
-
When `gitCommitAfterStage` is true, follow this exact order to avoid state inconsistency
|
|
186
|
+
When `gitCommitAfterStage` is true, follow this exact order to avoid state inconsistency.
|
|
187
|
+
|
|
188
|
+
**Why two commits.** The stage's `.pipeline-state.json` is itself part of the staged commit, but the stage's `commitHash` cannot be known until *after* that commit is made. Recording it *inside* the same commit is a chicken-and-egg with no single-commit solution. Resolve it with a **deterministic two-commit sequence**, and **never** with `git commit --amend`: amending rewrites HEAD, so a hash captured before the amend points at an orphaned commit that is not in the final history (the exact defect this protocol exists to prevent).
|
|
184
189
|
|
|
185
190
|
1. **Stage specific files only:** `git add {specsDir}/{feature}/` — never use `git add -A` or `git add .`
|
|
186
|
-
2. **
|
|
187
|
-
3. **If
|
|
188
|
-
4. **If
|
|
191
|
+
2. **Commit 1 — artifacts + state, hash not yet known:** In `.pipeline-state.json`, set this stage's `status: "complete"` and `commitHash: null`, then `git commit -m "{commitPrefix}({feature}): <action>"`. This is the stage's **artifact commit**; its hash is the provenance hash callers rely on.
|
|
192
|
+
3. **If Commit 1 succeeds — Commit 2 records the hash:** Capture the hash of Commit 1 (`git rev-parse HEAD`). Write it into this stage's `commitHash` in `.pipeline-state.json`, then commit only that one-line change: `git add {specsDir}/{feature}/.pipeline-state.json && git commit -m "{commitPrefix}({feature}): record stage commit hash"`. The stored `commitHash` now points at the artifact commit (Commit 1) — never at Commit 2, and never at an orphaned amend. The working tree is clean afterward, so the next stage's dirty-tree check passes.
|
|
193
|
+
4. **If Commit 1 fails:** do NOT update pipeline state to complete. Report the error to the user and leave state as `in-progress` so the stage can be resumed. Common failure causes:
|
|
189
194
|
- **Pre-commit hook failure:** Report the hook output. Never use `--no-verify` to bypass. Help the user fix the underlying issue.
|
|
190
195
|
- **Merge conflicts:** Report conflicting files. Suggest resolution steps appropriate to the conflict.
|
|
191
|
-
- **Nothing to commit:** If all artifacts were already committed, this is fine —
|
|
192
|
-
5. **Never** use `git add -A`, `--no-verify`, or `--force` flags
|
|
196
|
+
- **Nothing to commit:** If all artifacts were already committed, this is fine — mark the stage `complete`, leave `commitHash` at its existing value (or `null` if there was never an artifact commit), and skip Commit 2. There is no new artifact commit to record.
|
|
197
|
+
5. **Never** use `git add -A`, `--amend`, `--no-verify`, or `--force` flags
|
|
193
198
|
|
|
194
199
|
## Crash Recovery
|
|
195
200
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Stage Exit Protocol
|
|
2
|
+
|
|
3
|
+
The single source of truth for how every forge **authoring** stage closes. It
|
|
4
|
+
replaces the old ad-hoc "Next steps:" bullet lists with one fixed, correctly-ordered
|
|
5
|
+
sequence: **verify (if missing or stale) → `/clear` → run the next command.**
|
|
6
|
+
|
|
7
|
+
Two principles this block encodes (do not relitigate — they are locked product
|
|
8
|
+
decisions):
|
|
9
|
+
|
|
10
|
+
1. **Clearing is recommended on its own merits at every stage boundary** — a clean
|
|
11
|
+
start for the next stage — *not* as a proxy for a full context window. Window
|
|
12
|
+
fullness only changes *how emphatically* the clear is recommended, never *whether*
|
|
13
|
+
it is.
|
|
14
|
+
2. **Verify happens before the clear, never after.** Verify's clean-room subagent is
|
|
15
|
+
dispatched from the *current* session, so the findings digest and any fix decision
|
|
16
|
+
land where the context to act on them still exists. Clearing first throws that
|
|
17
|
+
context away.
|
|
18
|
+
|
|
19
|
+
## How this file is used
|
|
20
|
+
|
|
21
|
+
The blocks below are **stamped verbatim** into each stage skill's closing (a runtime
|
|
22
|
+
`references/` include would not survive the adapter build, which flattens skills into
|
|
23
|
+
`adapters/<agent>/`). A drift-guard test (`tests/test_stage_exit_protocol.py`) asserts
|
|
24
|
+
each stamp site still contains its block, so an edit here must be mirrored into every
|
|
25
|
+
stamp site (and vice-versa).
|
|
26
|
+
|
|
27
|
+
Each stamp site fills three slots; everything else is identical across sites:
|
|
28
|
+
|
|
29
|
+
- `{stage}` — the just-completed stage as a lowercase noun phrase (e.g. `the PRD`,
|
|
30
|
+
`the tech spec`, `the backlog`). Always used mid-sentence, never sentence-initial.
|
|
31
|
+
- `{verify-command}` — the verify command for that stage (e.g.
|
|
32
|
+
`` `/feature-forge:forge-verify {feature}` ``).
|
|
33
|
+
- `{next-command}` — the command that starts the next stage (e.g.
|
|
34
|
+
`` `/feature-forge:forge-2-tech {feature}` ``).
|
|
35
|
+
|
|
36
|
+
`{feature}` / `{epic}` and similar remain runtime placeholders that pass through
|
|
37
|
+
untouched — they are resolved by the skill at run time, not by this template.
|
|
38
|
+
|
|
39
|
+
## Stamp sites
|
|
40
|
+
|
|
41
|
+
| Stamp site | Block | `{stage}` |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `forge-0-epic` (epic → first PRD) | standard | the epic decomposition |
|
|
44
|
+
| `forge-1-prd` | standard | the PRD |
|
|
45
|
+
| `forge-2-tech` | standard | the tech spec |
|
|
46
|
+
| `forge-3-specs` | standard | the implementation specs |
|
|
47
|
+
| `forge-4-backlog` | standard | the backlog |
|
|
48
|
+
| `forge-5-loop` (step-6 epic-member handoff) | standard | feature `{feature}`'s loop |
|
|
49
|
+
| `forge-5-loop` (all-done closing → docs) | warm | — |
|
|
50
|
+
|
|
51
|
+
`forge-6-docs` is **terminal** — it stamps no exit block. It is the *target* of the
|
|
52
|
+
warm variant, not a stamp site.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Standard block
|
|
57
|
+
|
|
58
|
+
Stamp this at every authoring-stage boundary. It self-adapts: step 1's verify gate
|
|
59
|
+
only fires when verification is actually outstanding, so at a boundary where verify
|
|
60
|
+
already ran (or was explicitly skipped, or auto-verify is on) it silently collapses to
|
|
61
|
+
just the `/clear` → next-command steps.
|
|
62
|
+
|
|
63
|
+
<!-- BEGIN: standard-exit-block -->
|
|
64
|
+
**This stage is done — walk the user through the Stage Exit Protocol** before moving on. The order is fixed, and step 2 is something only the user can do:
|
|
65
|
+
|
|
66
|
+
1. **Verify {stage} first — if it isn't already verified.** When this stage has no fresh verification on record (`verifyState` is **missing or stale**) **and** `autoVerify` is off for it, verify **now, before clearing**. If verify already ran, is pending under auto-verify, or the stage was explicitly skipped, say so and go straight to step 2. Present the **Standard Verify Gate** using `AskUserQuestion` with exactly these three options — but only when the host has a question mechanism **and** the clean-room path is available (the `Agent` tool plus a dispatchable `forge-verifier` subagent):
|
|
67
|
+
- **Verify {stage} now** *(recommended)* — dispatch the clean-room `forge-verifier` subagent from this session in require-clean mode; the digest returns here so any fix decision keeps its context. One-time — it does **not** change config.
|
|
68
|
+
- **Verify now + enable auto-verify going forward** — verify now **and** patch `"autoVerify": true` into `forge.config.json` in place (preserve formatting and every other key) so future stages verify automatically, no prompt. This complements the `forge-init` opt-in. **Do not auto-commit this config change** — treat it like `notes`: a user-facing edit the user commits on their own cadence, never folded into a stage's artifact commit.
|
|
69
|
+
- **Skip for now** — go straight to `/clear` and the next command without verifying. Record this stage's verify status as `"skipped"` in pipeline state (mirroring the existing skip handling) **only** on an explicit skip — a skip does not go stale.
|
|
70
|
+
|
|
71
|
+
**Host / clean-room fallback (not a user-selectable option):** if the question mechanism, the `Agent` tool, or the `forge-verifier` subagent is unavailable, do **not** run clean-room — degrade to printing `{verify-command}` for the user to run inline/manually (mirroring `autoInvokeNextStage`), and offer the auto-verify enable as plain text only if a config write is possible.
|
|
72
|
+
2. **Then `/clear`.** Recommended **unconditionally** at this boundary for a clean start — independent of how full the context window is. Every artifact is on disk, so the work survives the clear. **I can't `/clear` for you — you have to run it yourself.**
|
|
73
|
+
3. **Then run `{next-command}`** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk.
|
|
74
|
+
<!-- END: standard-exit-block -->
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Warm-acceptable variant
|
|
79
|
+
|
|
80
|
+
Stamp this only at the `forge-5-loop → forge-6-docs` boundary (the all-done result
|
|
81
|
+
report). Here clearing is **optional**: the docs stage benefits from the still-warm
|
|
82
|
+
context of what the loop actually did, and impl-verify is already offered interactively
|
|
83
|
+
by the loop itself, so this block defers rather than re-presenting a gate.
|
|
84
|
+
|
|
85
|
+
> **Note — no literal `/clear` here.** The warm block lives in `result-reporting.md`, a
|
|
86
|
+
> skill-*own* reference that the adapter build copies **verbatim** (unlike skill bodies,
|
|
87
|
+
> it is not host-term translated), so a literal `/clear` would reach non-Claude adapters
|
|
88
|
+
> undegraded. The warm variant says "clearing is optional" anyway, so it is phrased
|
|
89
|
+
> host-neutrally without the token on purpose — do not reintroduce `/clear` here. (The
|
|
90
|
+
> standard block *does* use `/clear`; that is fine because every standard stamp site is a
|
|
91
|
+
> skill **body**, where `scripts/build-adapters.py` degrades it.)
|
|
92
|
+
|
|
93
|
+
<!-- BEGIN: warm-exit-block -->
|
|
94
|
+
**The loop is complete — this is the one boundary where clearing before the next stage is optional.**
|
|
95
|
+
|
|
96
|
+
1. **Verify is already offered above.** Impl-verify is offered interactively right after this report (Step 5b for a standalone feature, Step 6.1 for an epic member) — run it there rather than as a second gate. It runs clean-room, so it needs no fresh session.
|
|
97
|
+
2. **Clearing is optional here — warm is fine.** `forge-6-docs` benefits from the still-warm context of what the loop actually did, so continuing in this same session is the easy default. A cold start also works — every artifact is on disk — but there is no need to force it.
|
|
98
|
+
3. **Then run `{next-command}`** — in this warm session, or a fresh one if you prefer.
|
|
99
|
+
<!-- END: warm-exit-block -->
|
|
@@ -312,6 +312,16 @@ def atomic_write(path: Path, data: dict) -> None:
|
|
|
312
312
|
handle.flush()
|
|
313
313
|
os.fsync(handle.fileno())
|
|
314
314
|
os.replace(tmp_path, path)
|
|
315
|
+
# fsync the parent dir so the rename itself is durable on crash, not just
|
|
316
|
+
# the file bytes (best-effort — some filesystems reject O_RDONLY dir fsync).
|
|
317
|
+
try:
|
|
318
|
+
dir_fd = os.open(parent, os.O_RDONLY)
|
|
319
|
+
try:
|
|
320
|
+
os.fsync(dir_fd)
|
|
321
|
+
finally:
|
|
322
|
+
os.close(dir_fd)
|
|
323
|
+
except OSError:
|
|
324
|
+
pass
|
|
315
325
|
except OSError as exc:
|
|
316
326
|
tmp_path.unlink(missing_ok=True)
|
|
317
327
|
raise UsageError(f"atomic write to {path} failed: {exc}")
|
|
@@ -26,6 +26,8 @@ import argparse
|
|
|
26
26
|
import json
|
|
27
27
|
import os
|
|
28
28
|
import re
|
|
29
|
+
import shlex
|
|
30
|
+
import shutil
|
|
29
31
|
import subprocess
|
|
30
32
|
import sys
|
|
31
33
|
import tempfile
|
|
@@ -281,6 +283,32 @@ def _json_text(obj: object) -> str:
|
|
|
281
283
|
return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
|
|
282
284
|
|
|
283
285
|
|
|
286
|
+
def contained_path(base: Path, *parts: str) -> Path:
|
|
287
|
+
"""Join ``parts`` onto ``base`` and assert the result stays within ``base``.
|
|
288
|
+
|
|
289
|
+
Ports epic-manifest.py's ``contained_path`` guard: canonicalizes (symlink-
|
|
290
|
+
resolves) both ends and verifies containment so no scaffold write or verify
|
|
291
|
+
cwd can escape the target repo — defense in depth behind ``_validate_answers``
|
|
292
|
+
should a crafted member path slip through. Containment violations surface only
|
|
293
|
+
as exit-2 usage errors.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
base: The containing directory (the target repo root), already known to exist.
|
|
297
|
+
*parts: Repo-relative segments to append (member path, artifact rel path).
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
The resolved, contained absolute path.
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
UsageError: If the resolved path escapes ``base`` (exit 2).
|
|
304
|
+
"""
|
|
305
|
+
base_real = base.resolve()
|
|
306
|
+
resolved = (base_real / Path(*parts)).resolve()
|
|
307
|
+
if resolved != base_real and base_real not in resolved.parents:
|
|
308
|
+
raise UsageError(f"path escapes target dir: {os.path.join(*parts)}")
|
|
309
|
+
return resolved
|
|
310
|
+
|
|
311
|
+
|
|
284
312
|
def _atomic_write_text(path: Path, text: str) -> None:
|
|
285
313
|
"""Write ``text`` to ``path`` atomically via a same-dir temp file + os.replace.
|
|
286
314
|
|
|
@@ -381,7 +409,7 @@ def run(
|
|
|
381
409
|
|
|
382
410
|
|
|
383
411
|
# --------------------------------------------------------------------------- #
|
|
384
|
-
#
|
|
412
|
+
# Subcommands: check / scaffold / verify / commit / status (02 §8.2)
|
|
385
413
|
# --------------------------------------------------------------------------- #
|
|
386
414
|
|
|
387
415
|
|
|
@@ -476,6 +504,7 @@ def _write_artifact(
|
|
|
476
504
|
"""
|
|
477
505
|
if rel_path in sentinel["artifactsWritten"]:
|
|
478
506
|
return
|
|
507
|
+
contained_path(target, rel_path) # never write outside the target repo (exit 2)
|
|
479
508
|
dest = target / rel_path
|
|
480
509
|
if dest.exists():
|
|
481
510
|
return
|
|
@@ -529,7 +558,7 @@ def _resolve_commands(member: Member) -> tuple[str, str]:
|
|
|
529
558
|
|
|
530
559
|
|
|
531
560
|
def write_config(answers: Answers, target: Path, sentinel: Sentinel) -> None:
|
|
532
|
-
"""Write forge.config.json
|
|
561
|
+
"""Write forge.config.json == forge-init's field set + loopRunner (02 §4.3, 00 §7)."""
|
|
533
562
|
config: dict = {
|
|
534
563
|
"specsDir": "./specs",
|
|
535
564
|
"docsDir": "./docs/architecture",
|
|
@@ -540,6 +569,14 @@ def write_config(answers: Answers, target: Path, sentinel: Sentinel) -> None:
|
|
|
540
569
|
"typeCheckCommand": None,
|
|
541
570
|
"testCommand": None,
|
|
542
571
|
"loopIterationMultiplier": 1.5,
|
|
572
|
+
# Navigator keys — kept in lockstep with forge-init.sh's emitted field set
|
|
573
|
+
# (00 §7, REQ-CFG-02) so a bootstrapped config == forge-init's + loopRunner.
|
|
574
|
+
"autoInvokeNextStage": True,
|
|
575
|
+
"contextWindowTokens": None,
|
|
576
|
+
"contextWarnThreshold": 0.7,
|
|
577
|
+
"autoVerify": False,
|
|
578
|
+
"autoVerifyStages": {},
|
|
579
|
+
"autoFix": False,
|
|
543
580
|
"loopRunner": {"name": "rauf", "bin": "rauf"},
|
|
544
581
|
}
|
|
545
582
|
if answers["layout"] == "single":
|
|
@@ -697,9 +734,9 @@ def scaffold(target: Path, answers: Answers) -> list[str]:
|
|
|
697
734
|
def toolchain_present(required: list[str]) -> bool:
|
|
698
735
|
"""Return True iff every required tool is on PATH (REQ-LIFE-03).
|
|
699
736
|
|
|
700
|
-
Probes each binary with
|
|
701
|
-
|
|
702
|
-
|
|
737
|
+
Probes each binary with :func:`shutil.which` (a pure PATH lookup, no shell
|
|
738
|
+
spawned). A single missing tool yields False, driving the distinct
|
|
739
|
+
missing-toolchain outcome (exit 2, 00 §9): the skill then offers
|
|
703
740
|
scaffold-anyway-unverified vs abort and marks the baseline unverified
|
|
704
741
|
(REQ-LIFE-04). Bootstrap NEVER installs a toolchain (tech-spec §9).
|
|
705
742
|
|
|
@@ -708,17 +745,10 @@ def toolchain_present(required: list[str]) -> bool:
|
|
|
708
745
|
already {pm}-substituted.
|
|
709
746
|
|
|
710
747
|
Returns:
|
|
711
|
-
True iff
|
|
748
|
+
True iff every entry resolves on PATH.
|
|
712
749
|
"""
|
|
713
750
|
for tool in required:
|
|
714
|
-
|
|
715
|
-
proc = run(["sh", "-c", f"command -v {tool}"], cwd=Path.cwd(), check=False)
|
|
716
|
-
except UsageError:
|
|
717
|
-
# The probe itself could not be launched (e.g. an empty PATH leaves no
|
|
718
|
-
# `sh`): treat that as the tool being absent, the missing-toolchain
|
|
719
|
-
# outcome, never an internal error (REQ-LIFE-03/04).
|
|
720
|
-
return False
|
|
721
|
-
if proc.returncode != 0:
|
|
751
|
+
if shutil.which(tool) is None:
|
|
722
752
|
return False
|
|
723
753
|
return True
|
|
724
754
|
|
|
@@ -763,9 +793,11 @@ def verify(target: Path, answers: Answers) -> VerifyResult:
|
|
|
763
793
|
test: list[CommandOutcome] = []
|
|
764
794
|
for member in answers["members"]:
|
|
765
795
|
lint_cmd, test_cmd = _resolve_commands(member)
|
|
766
|
-
cwd = target
|
|
796
|
+
cwd = contained_path(target, member["path"]) # never run outside the target
|
|
767
797
|
for bucket, cmd in ((lint, lint_cmd), (test, test_cmd)):
|
|
768
|
-
|
|
798
|
+
# STACK_COMMANDS templates with an allow-listed {pm} (see _validate_answers)
|
|
799
|
+
# — split into an argv token list and run without a shell (no `sh -c`).
|
|
800
|
+
proc = run(shlex.split(cmd), cwd=cwd, check=False)
|
|
769
801
|
bucket.append(
|
|
770
802
|
{"command": cmd, "ok": proc.returncode == 0, "member": member["path"]}
|
|
771
803
|
)
|
|
@@ -867,9 +899,55 @@ def _parse_answers(raw: str) -> Answers:
|
|
|
867
899
|
raise UsageError(f"malformed --answers JSON: {exc}")
|
|
868
900
|
if not isinstance(parsed, dict):
|
|
869
901
|
raise UsageError("--answers must be a JSON object")
|
|
902
|
+
_validate_answers(parsed)
|
|
870
903
|
return parsed
|
|
871
904
|
|
|
872
905
|
|
|
906
|
+
def _validate_answers(answers: Answers) -> None:
|
|
907
|
+
"""Reject non-allowlisted stack/packageManager and unsafe member paths (exit 2).
|
|
908
|
+
|
|
909
|
+
Runs before any ``{pm}`` substitution, ``STACK_COMMANDS[...]`` subscript, or
|
|
910
|
+
artifact write so a malformed ``--answers`` payload fails with a stderr
|
|
911
|
+
diagnostic, never a KeyError traceback or a path escape. Mirrors the
|
|
912
|
+
input-validation rigor epic-manifest.py applies via ``assert_safe_name`` /
|
|
913
|
+
``contained_path``:
|
|
914
|
+
|
|
915
|
+
- ``stack`` must be a known :data:`Stack` (a ``STACK_COMMANDS`` key).
|
|
916
|
+
- ``packageManager`` must be in ``PACKAGE_MANAGERS[stack]`` for a stack that
|
|
917
|
+
has a choice; stacks with no choice (go/rust/generic) must leave it ``None``.
|
|
918
|
+
- ``path`` must be a repo-relative string — no absolute path, no ``..``
|
|
919
|
+
component, no backslash — so it can never resolve outside the target.
|
|
920
|
+
"""
|
|
921
|
+
members = answers.get("members")
|
|
922
|
+
if not isinstance(members, list) or not members:
|
|
923
|
+
raise UsageError("--answers must include a non-empty members[] array")
|
|
924
|
+
for member in members:
|
|
925
|
+
stack = member.get("stack")
|
|
926
|
+
if stack not in STACK_COMMANDS:
|
|
927
|
+
raise UsageError(
|
|
928
|
+
f"unknown stack {stack!r}: expected one of {sorted(STACK_COMMANDS)}"
|
|
929
|
+
)
|
|
930
|
+
pm = member.get("packageManager")
|
|
931
|
+
choices = PACKAGE_MANAGERS.get(stack)
|
|
932
|
+
if choices is not None:
|
|
933
|
+
if pm not in choices:
|
|
934
|
+
raise UsageError(
|
|
935
|
+
f"invalid packageManager {pm!r} for stack {stack!r}: "
|
|
936
|
+
f"expected one of {choices}"
|
|
937
|
+
)
|
|
938
|
+
elif pm is not None:
|
|
939
|
+
raise UsageError(
|
|
940
|
+
f"stack {stack!r} takes no packageManager, got {pm!r}"
|
|
941
|
+
)
|
|
942
|
+
path = member.get("path")
|
|
943
|
+
if not isinstance(path, str) or not path:
|
|
944
|
+
raise UsageError(f"member path must be a non-empty string, got {path!r}")
|
|
945
|
+
if os.path.isabs(path) or "\\" in path or ".." in Path(path).parts:
|
|
946
|
+
raise UsageError(
|
|
947
|
+
f"unsafe member path {path!r}: must be repo-relative with no '..'"
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
|
|
873
951
|
def _dispatch(args: argparse.Namespace, target: Path) -> int:
|
|
874
952
|
"""Route a parsed command to its handler, translating outcomes into exit codes.
|
|
875
953
|
|
|
@@ -24,7 +24,10 @@ cat > "$CONFIG_FILE" << 'EOF'
|
|
|
24
24
|
"loopIterationMultiplier": 1.5,
|
|
25
25
|
"autoInvokeNextStage": true,
|
|
26
26
|
"contextWindowTokens": null,
|
|
27
|
-
"contextWarnThreshold": 0.7
|
|
27
|
+
"contextWarnThreshold": 0.7,
|
|
28
|
+
"autoVerify": false,
|
|
29
|
+
"autoVerifyStages": {},
|
|
30
|
+
"autoFix": false
|
|
28
31
|
}
|
|
29
32
|
EOF
|
|
30
33
|
|
|
@@ -43,6 +46,9 @@ echo " loopIterationMultiplier: 1.5 (multiplier for loop iterations)"
|
|
|
43
46
|
echo " autoInvokeNextStage: true (navigator auto-starts the next stage after you confirm)"
|
|
44
47
|
echo " contextWindowTokens: null (infer; set to 1000000 on a 1M-context model)"
|
|
45
48
|
echo " contextWarnThreshold: 0.7 (suggest a clean session past this fraction of the window)"
|
|
49
|
+
echo " autoVerify: false (set true to run forge-verify automatically after each stage)"
|
|
50
|
+
echo " autoVerifyStages: {} (per-stage overrides for autoVerify)"
|
|
51
|
+
echo " autoFix: false (set true to chain forge-fix after an auto-verify finds issues)"
|
|
46
52
|
echo ""
|
|
47
53
|
echo "The loop runner defaults to rauf. To target a different ralph-style runner,"
|
|
48
54
|
echo "add a \"loopRunner\" block (see references/forge-config-schema.json)."
|