@garygentry/feature-forge 0.2.7 → 0.2.9
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/adapters/claude/.feature-forge-bundle.json +1 -1
- package/adapters/claude/references/pipeline-state-schema.json +40 -0
- package/adapters/claude/references/stage-exit-protocol.md +32 -2
- package/adapters/claude/scripts/epic-manifest.py +26 -0
- package/adapters/claude/scripts/forge-session.py +93 -7
- package/adapters/claude/skills/forge/SKILL.md +3 -1
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/claude/skills/forge-1-prd/SKILL.md +2 -0
- package/adapters/claude/skills/forge-2-tech/SKILL.md +2 -0
- package/adapters/claude/skills/forge-5-loop/SKILL.md +7 -5
- package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/claude/skills/forge-verify/SKILL.md +2 -2
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +12 -0
- package/adapters/codex/.feature-forge-bundle.json +1 -1
- package/adapters/codex/references/pipeline-state-schema.json +40 -0
- package/adapters/codex/references/stage-exit-protocol.md +32 -2
- package/adapters/codex/scripts/epic-manifest.py +26 -0
- package/adapters/codex/scripts/forge-session.py +93 -7
- package/adapters/codex/skills/forge/SKILL.md +3 -1
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/codex/skills/forge-1-prd/SKILL.md +2 -0
- package/adapters/codex/skills/forge-2-tech/SKILL.md +2 -0
- package/adapters/codex/skills/forge-5-loop/SKILL.md +7 -5
- package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/codex/skills/forge-verify/SKILL.md +2 -2
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +12 -0
- package/adapters/copilot/.feature-forge-bundle.json +1 -1
- package/adapters/copilot/references/pipeline-state-schema.json +40 -0
- package/adapters/copilot/references/stage-exit-protocol.md +32 -2
- package/adapters/copilot/scripts/epic-manifest.py +26 -0
- package/adapters/copilot/scripts/forge-session.py +93 -7
- package/adapters/copilot/skills/forge/forge.md +3 -1
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +2 -0
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +2 -0
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +7 -5
- package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +2 -2
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +12 -0
- package/adapters/cursor/.feature-forge-bundle.json +1 -1
- package/adapters/cursor/references/pipeline-state-schema.json +40 -0
- package/adapters/cursor/references/stage-exit-protocol.md +32 -2
- package/adapters/cursor/scripts/epic-manifest.py +26 -0
- package/adapters/cursor/scripts/forge-session.py +93 -7
- package/adapters/cursor/skills/forge/forge.mdc +3 -1
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +2 -0
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +2 -0
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +7 -5
- package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +2 -2
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +12 -0
- package/adapters/gemini/.feature-forge-bundle.json +1 -1
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/pipeline-state-schema.json +40 -0
- package/adapters/gemini/references/stage-exit-protocol.md +32 -2
- package/adapters/gemini/scripts/epic-manifest.py +26 -0
- package/adapters/gemini/scripts/forge-session.py +93 -7
- package/adapters/gemini/skills/forge/forge.md +3 -1
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +42 -0
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +2 -0
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +2 -0
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +7 -5
- package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +5 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +2 -2
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +12 -0
- package/package.json +1 -1
|
@@ -1243,13 +1243,24 @@ def _resolve_feature_dir(specs_dir: Path, feature: str, epic: str | None) -> Pat
|
|
|
1243
1243
|
return flat
|
|
1244
1244
|
|
|
1245
1245
|
|
|
1246
|
-
def _next_steps_block(
|
|
1246
|
+
def _next_steps_block(
|
|
1247
|
+
next_command: str, host: str, reconcile: dict | None = None
|
|
1248
|
+
) -> str:
|
|
1247
1249
|
"""Render the sentinel-terminated NEXT-STEPS block for the given host.
|
|
1248
1250
|
|
|
1249
1251
|
The Claude wording uses the literal ``/clear`` slash-command; the generic
|
|
1250
1252
|
wording is host-neutral (matching the adapter build's host-term table, so
|
|
1251
1253
|
a non-Claude bundle invoking ``--host generic`` never instructs a fake
|
|
1252
1254
|
slash-command).
|
|
1255
|
+
|
|
1256
|
+
``reconcile`` carries the epic-backflow routing (§Epic backflow in
|
|
1257
|
+
``references/stage-exit-protocol.md``). When it marks a **blocking** request
|
|
1258
|
+
(``required: true``), the fenced primary command becomes the epic reconcile
|
|
1259
|
+
command and the normal next stage is demoted to a follow-up line. When it
|
|
1260
|
+
marks only **non-blocking** requests (``reminder: true``), the fenced command
|
|
1261
|
+
stays the normal next stage and a reminder line is appended. Either way the
|
|
1262
|
+
added prose is host-neutral (no literal ``/clear``) so it survives verbatim
|
|
1263
|
+
into a generic bundle.
|
|
1253
1264
|
"""
|
|
1254
1265
|
if host == "claude":
|
|
1255
1266
|
clear_line = (
|
|
@@ -1258,8 +1269,8 @@ def _next_steps_block(next_command: str, host: str) -> str:
|
|
|
1258
1269
|
"I can't `/clear` for you — you have to run it yourself."
|
|
1259
1270
|
)
|
|
1260
1271
|
next_line = (
|
|
1261
|
-
|
|
1262
|
-
"`/feature-forge:forge` to let the navigator resume from disk."
|
|
1272
|
+
"2. Then start a fresh session and run the next stage below — or "
|
|
1273
|
+
"re-run `/feature-forge:forge` to let the navigator resume from disk."
|
|
1263
1274
|
)
|
|
1264
1275
|
else:
|
|
1265
1276
|
clear_line = (
|
|
@@ -1268,10 +1279,43 @@ def _next_steps_block(next_command: str, host: str) -> str:
|
|
|
1268
1279
|
"disk, so the work survives it."
|
|
1269
1280
|
)
|
|
1270
1281
|
next_line = (
|
|
1271
|
-
|
|
1272
|
-
"the forge navigator skill to resume from disk."
|
|
1282
|
+
"2. Then start a fresh session and run the next stage below — or "
|
|
1283
|
+
"re-run the forge navigator skill to resume from disk."
|
|
1284
|
+
)
|
|
1285
|
+
blocking = bool(reconcile and reconcile.get("required"))
|
|
1286
|
+
# The primary actionable command goes in a fenced block so mobile/remote hosts
|
|
1287
|
+
# get a native copy button (inline code is not tap-to-copy). For a blocking
|
|
1288
|
+
# epic-change request the primary is the reconcile command; otherwise it is the
|
|
1289
|
+
# normal next-stage command. The fence sits before the sentinel, so the
|
|
1290
|
+
# sentinel remains the absolute last line.
|
|
1291
|
+
fenced_command = reconcile["command"] if blocking else next_command
|
|
1292
|
+
lines = ["**Next steps**", clear_line]
|
|
1293
|
+
if blocking:
|
|
1294
|
+
count = reconcile["count"]
|
|
1295
|
+
plural = "s" if count != 1 else ""
|
|
1296
|
+
lines.append(
|
|
1297
|
+
f"2. Then reconcile the epic **before** the next stage — {count} "
|
|
1298
|
+
f"blocking epic change request{plural} flagged, and proceeding would "
|
|
1299
|
+
"build this feature's artifacts on a decomposition that is about to "
|
|
1300
|
+
"change. Run the reconcile command below first."
|
|
1301
|
+
)
|
|
1302
|
+
else:
|
|
1303
|
+
lines.append(next_line)
|
|
1304
|
+
lines.append("")
|
|
1305
|
+
lines.append(f"```\n{fenced_command}\n```")
|
|
1306
|
+
if blocking and reconcile.get("deferred"):
|
|
1307
|
+
lines.append(
|
|
1308
|
+
f"After reconciling, continue the pipeline with: `{reconcile['deferred']}`"
|
|
1273
1309
|
)
|
|
1274
|
-
|
|
1310
|
+
elif reconcile and reconcile.get("reminder"):
|
|
1311
|
+
count = reconcile["count"]
|
|
1312
|
+
plural = "s" if count != 1 else ""
|
|
1313
|
+
lines.append(
|
|
1314
|
+
f"You also flagged {count} epic change{plural} to reconcile when "
|
|
1315
|
+
f"convenient: `{reconcile['command']}`"
|
|
1316
|
+
)
|
|
1317
|
+
lines.append(NEXT_STEPS_SENTINEL)
|
|
1318
|
+
return "\n".join(lines)
|
|
1275
1319
|
|
|
1276
1320
|
|
|
1277
1321
|
def stage_exit(
|
|
@@ -1304,6 +1348,13 @@ def stage_exit(
|
|
|
1304
1348
|
the fixed successor. ``--next-feature`` names the first actionable
|
|
1305
1349
|
feature for the epic handoff; without it the runtime placeholder
|
|
1306
1350
|
``{first-actionable-feature}`` passes through for the skill to resolve.
|
|
1351
|
+
- ``epicReconcile`` — present only when the exiting member carries
|
|
1352
|
+
``open`` ``epicChangeRequests`` (epic-backflow). ``required: true`` (any
|
|
1353
|
+
``blocksCurrent: true`` request) interposes a reconcile-first exit: the
|
|
1354
|
+
NEXT-STEPS primary command becomes ``/feature-forge:forge-0-epic {epic}``
|
|
1355
|
+
and the normal next stage is deferred. Only non-blocking requests set
|
|
1356
|
+
``reminder: true`` and append a non-blocking reminder line. Absent when
|
|
1357
|
+
there are no open requests (common path) or the epic name is unresolvable.
|
|
1307
1358
|
|
|
1308
1359
|
Read-only, deterministic, exit 0 — errors degrade to defaults, never
|
|
1309
1360
|
crash a stage closing.
|
|
@@ -1349,6 +1400,37 @@ def stage_exit(
|
|
|
1349
1400
|
)
|
|
1350
1401
|
next_command = f"/feature-forge:{next_stage_id} {next_arg}" if next_stage_id else None
|
|
1351
1402
|
|
|
1403
|
+
# Epic backflow routing: an exiting member may carry epic-level change requests
|
|
1404
|
+
# (recorded by forge-1-prd/forge-2-tech). A `blocksCurrent: true` request means
|
|
1405
|
+
# the current feature's next stage would build on a soon-to-change decomposition,
|
|
1406
|
+
# so the exit interposes a reconcile-first step; only-`false` requests append a
|
|
1407
|
+
# non-blocking reminder. Read-only; the common path (no open requests) is a no-op.
|
|
1408
|
+
# The epic name comes from the `--epic` arg or the state's `epic` back-pointer.
|
|
1409
|
+
epic_reconcile: dict | None = None
|
|
1410
|
+
epic_name = epic or state.get("epic")
|
|
1411
|
+
open_requests = [
|
|
1412
|
+
r
|
|
1413
|
+
for r in state.get("epicChangeRequests", [])
|
|
1414
|
+
if isinstance(r, dict) and r.get("status") == "open"
|
|
1415
|
+
]
|
|
1416
|
+
if open_requests and epic_name:
|
|
1417
|
+
reconcile_command = f"/feature-forge:forge-0-epic {epic_name}"
|
|
1418
|
+
blocking = [r for r in open_requests if r.get("blocksCurrent") is True]
|
|
1419
|
+
if blocking:
|
|
1420
|
+
epic_reconcile = {
|
|
1421
|
+
"required": True,
|
|
1422
|
+
"command": reconcile_command,
|
|
1423
|
+
"count": len(blocking),
|
|
1424
|
+
"deferred": next_command,
|
|
1425
|
+
}
|
|
1426
|
+
else:
|
|
1427
|
+
epic_reconcile = {
|
|
1428
|
+
"required": False,
|
|
1429
|
+
"reminder": True,
|
|
1430
|
+
"command": reconcile_command,
|
|
1431
|
+
"count": len(open_requests),
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1352
1434
|
directives = {
|
|
1353
1435
|
"stage": stage,
|
|
1354
1436
|
"stageNoun": STAGE_NOUN.get(stage, stage),
|
|
@@ -1366,9 +1448,13 @@ def stage_exit(
|
|
|
1366
1448
|
"cleanTree": clean_tree,
|
|
1367
1449
|
"host": host,
|
|
1368
1450
|
}
|
|
1451
|
+
if epic_reconcile is not None:
|
|
1452
|
+
directives["epicReconcile"] = epic_reconcile
|
|
1369
1453
|
return {
|
|
1370
1454
|
"directives": directives,
|
|
1371
|
-
"nextSteps": _next_steps_block(
|
|
1455
|
+
"nextSteps": _next_steps_block(
|
|
1456
|
+
next_command or "/feature-forge:forge", host, epic_reconcile
|
|
1457
|
+
),
|
|
1372
1458
|
"sentinel": NEXT_STEPS_SENTINEL,
|
|
1373
1459
|
}
|
|
1374
1460
|
|
|
@@ -143,6 +143,7 @@ and render from its output:
|
|
|
143
143
|
- **Epic header:** name + `status` (active | paused | abandoned | complete).
|
|
144
144
|
- **Dependency graph:** each feature with its `dependsOn`, as an arrow list or indented tree (the helper guarantees the graph is acyclic).
|
|
145
145
|
- **Per-feature rows:** reuse the **existing status indicators** below (✅/✅⚠️/🔄/⬜/❌/✅🔍/⏭️/⚠️), driven by each feature's derived `stage`/`status`. Mark `blocked` features and list their `unmetDeps`.
|
|
146
|
+
- **Pending epic changes:** for any feature with `openEpicChangeRequests > 0`, append a ⚠️ marker and a hint: *"N pending epic change(s) — run `/feature-forge:forge-0-epic {epic}` to reconcile."* If `blockingEpicChangeRequests > 0`, use a stronger marker (⚠️ **blocking**) and word it *"reconcile the epic **before** writing specs"* — this mirrors the pause-now vs finish-then split that stage-exit already routes on. Take these counts **only** from `render-status --json` (`features[].openEpicChangeRequests` / `.blockingEpicChangeRequests`); do not read member `.pipeline-state.json` directly for them.
|
|
146
147
|
- **Actionable vs blocked:** list the `actionable` set and the recommended `nextCommand`.
|
|
147
148
|
- **Rollup:** `{complete}/{total} features complete`.
|
|
148
149
|
|
|
@@ -158,12 +159,13 @@ Dependency graph:
|
|
|
158
159
|
audit-log (no deps)
|
|
159
160
|
|
|
160
161
|
✅ config-store complete
|
|
161
|
-
🔄 token-service forge-3-specs (in progress)
|
|
162
|
+
🔄 token-service forge-3-specs (in progress) — ⚠️ 1 pending epic change
|
|
162
163
|
⬜ api-gateway blocked — waiting on token-service
|
|
163
164
|
✅ audit-log complete
|
|
164
165
|
|
|
165
166
|
Actionable now: token-service
|
|
166
167
|
Next: /feature-forge:forge-3-specs token-service
|
|
168
|
+
⚠️ Pending epic changes: token-service (1). Run /feature-forge:forge-0-epic auth-overhaul to reconcile.
|
|
167
169
|
```
|
|
168
170
|
|
|
169
171
|
All of this is reconstructed **purely from disk** — the manifest plus each member's `.pipeline-state.json`, with no in-memory state — so a fresh session renders the same dashboard. If `render-status` fails, do not render a partial dashboard; surface per the exit-1/exit-2 split in the **Feature Directory Resolution** block of `references/shared-conventions.md` (exit 1 → parse `{findings[]}` from stdout; exit 2 → surface the plain `Error:` stderr line verbatim).
|
|
@@ -28,6 +28,48 @@ python3 "$R/scripts/epic-manifest.py" validate "{epic}" --specs-dir "{specsDir}"
|
|
|
28
28
|
manifest by hand. **Never auto-repair**, never offer an edit operation, and never proceed past
|
|
29
29
|
this gate. Tell the user what is wrong and STOP.
|
|
30
30
|
|
|
31
|
+
## Step E0-read — Surface Pending Epic Change Requests (backflow)
|
|
32
|
+
|
|
33
|
+
Immediately after E1's validate gate passes, and **before** offering the E2 operation menu,
|
|
34
|
+
scan for epic-level change requests that a member stage recorded during the pipeline (the
|
|
35
|
+
backflow path — see `references/pipeline-state-schema.json` `epicChangeRequests[]`, written by
|
|
36
|
+
`forge-1-prd`/`forge-2-tech`). Enumerate the manifest's feature list (already loaded in E1) and
|
|
37
|
+
read each member's `{specsDir}/{epic}/{member}/.pipeline-state.json`, collecting every
|
|
38
|
+
`epicChangeRequests[]` entry with `status: "open"`. If a member state is missing or unreadable,
|
|
39
|
+
report it and continue with the rest — do **not** abort edit mode over one bad member file (mirror
|
|
40
|
+
E1's "report, never silently repair" posture).
|
|
41
|
+
|
|
42
|
+
If any open requests exist, present them grouped (show `kind`, `target`, `rationale`,
|
|
43
|
+
`blocksCurrent`, `raisedBy`), then for **each** request use `AskUserQuestion` to offer **Apply**,
|
|
44
|
+
**Dismiss**, or **Skip for now**:
|
|
45
|
+
|
|
46
|
+
- **Apply — simple kinds (`add-feature`, `redep`):** pre-fill the matching E2 operation and flow
|
|
47
|
+
through E3→E4→E5→E6 unchanged. `add-feature` seeds the new feature's charter from the request's
|
|
48
|
+
`rationale` (the user still edits `exposes`/`consumes`/`dependsOn` before commit, exactly as
|
|
49
|
+
C3/C4/C5); `redep` pre-fills `set-dep {epic} {target} --depends-on "…"`. The existing E4 impact
|
|
50
|
+
warning fires normally — a `blocksCurrent` boundary change naturally trips it, which is correct.
|
|
51
|
+
- **Apply — composite kinds (`move-boundary`, `split`):** there is no single mutator (v1). Walk the
|
|
52
|
+
user through a **guided-manual** sequence — the relevant `set-dep` and/or direct `exposes`/
|
|
53
|
+
`consumes` edits on the composed manifest entries (per E3's "Contracts have no mutator" rule),
|
|
54
|
+
across **both** affected features — re-validating after each step. Confirm each mutation via
|
|
55
|
+
`AskUserQuestion`; never batch-apply.
|
|
56
|
+
- **Dismiss:** the user decides the epic is fine after all — flip the source request's `status` to
|
|
57
|
+
`"dismissed"` (no manifest mutation). Explicit only; there is **no auto-expiry**.
|
|
58
|
+
- **Skip for now:** leave the request `open`; it resurfaces on the next edit-mode entry and stays
|
|
59
|
+
visible in the navigator/verify (once Phase 2 surfacing lands).
|
|
60
|
+
|
|
61
|
+
**On a successful Apply** (mutator exit 0, or the guided-manual sequence confirmed), flip the
|
|
62
|
+
**source** request's `status` from `"open"` to `"applied"` in its member `.pipeline-state.json`,
|
|
63
|
+
using the same atomic temp-file + `os.replace` write the skill uses for any state edit. The E6 git
|
|
64
|
+
step already stages the whole `{specsDir}/{epic}/` subtree, so the flipped member state is captured
|
|
65
|
+
in the **same commit** as the manifest mutation — no new commit machinery. If a **Dismiss** is the
|
|
66
|
+
only action taken (no manifest mutation follows), still commit the member-state change under the
|
|
67
|
+
standard edit-mode message form, e.g. `forge({epic}): dismiss epic change request`.
|
|
68
|
+
|
|
69
|
+
E0-read is **read-then-offer** — it never auto-applies. Every apply still passes through E3/E4 with
|
|
70
|
+
explicit human confirmation, preserving the human-approves-every-mutation invariant. If **no** open
|
|
71
|
+
requests exist, say nothing and proceed straight to E2.
|
|
72
|
+
|
|
31
73
|
## Step E2 — Choose Operation
|
|
32
74
|
|
|
33
75
|
Use `AskUserQuestion` to offer the edit operations, each mapping to one helper mutator:
|
|
@@ -81,6 +81,8 @@ Before moving to Step 4, summarize your coverage as text, then use the host's qu
|
|
|
81
81
|
|
|
82
82
|
**Parking lot:** If the user raises a concern that belongs to a different pipeline stage, acknowledge it and note it in the pipeline state's `notes` field: "Good point — I've noted that for the [tech spec/implementation specs]. Let's continue with [current stage]."
|
|
83
83
|
|
|
84
|
+
**Epic-level concern (backflow):** The parking lot above is for concerns about a *later stage of THIS feature*. If instead the interview reveals the **epic decomposition itself** is wrong — a **sibling feature must be added**, a **frozen boundary between features must move**, a feature must **split**, or a **dependency edge is wrong** — that is an *epic-level* concern and does **not** go in `notes`. It only applies when this feature is an epic member (its `.pipeline-state.json` has an `epic` back-pointer); for a standalone feature there is no epic to reconcile, so treat the concern as same-feature or out of scope. To record one, append an entry to the member state's `epicChangeRequests[]` array (same direct-edit path as `notes`; schema in `references/pipeline-state-schema.json`): `kind` (`add-feature`|`redep`|`move-boundary`|`split`), `target`, `rationale`, `raisedBy: "forge-1-prd"`, `raisedAt` (ISO-8601 UTC), `status: "open"`, and `blocksCurrent`. Set `blocksCurrent: true` when the change alters a contract (`exposes`/`consumes`) or dependency edge this feature relies on for its *next* stage (proceeding would build specs on a soon-to-change decomposition); `false` for a peer/downstream change this feature does not consume. When the change touches a contract/dep edge and the classification is genuinely ambiguous, confirm `blocksCurrent` with a single the host's question mechanism, defaulting to `true` (a false negative silently diverges two members' contracts). **Do not** edit `epic-manifest.json` here — recording is not applying; only `/feature-forge:forge-0-epic` edit mode mutates the epic. Then acknowledge without blocking: "That's an epic-level change — I've recorded it so `forge-0-epic` can reconcile it. [Blocking: We'll want to reconcile the epic before writing specs. | Non-blocking: We can finish this feature first and reconcile when convenient.]" and continue the interview.
|
|
85
|
+
|
|
84
86
|
## Step 4: Write the PRD
|
|
85
87
|
|
|
86
88
|
Once the interview is thorough, write `{resolvedFeatureDir}/PRD.md` following the structure in `references/prd-template.md`.
|
|
@@ -92,6 +92,8 @@ Interview the user about technology decisions. Unlike the PRD interview, here yo
|
|
|
92
92
|
|
|
93
93
|
**Parking lot:** If the user raises a concern that belongs to a different pipeline stage (e.g., backlog granularity, documentation format), acknowledge it and note it in the pipeline state's `notes` field: "Good point — I've noted that for the [specs/backlog/docs stage]. Let's continue with the tech spec."
|
|
94
94
|
|
|
95
|
+
**Epic-level concern (backflow):** The parking lot above is for concerns about a *later stage of THIS feature*. If instead the design reveals the **epic decomposition itself** is wrong — a **sibling feature must be added**, a **frozen boundary between features must move**, a feature must **split**, or a **dependency edge is wrong** — that is an *epic-level* concern and does **not** go in `notes`. It only applies when this feature is an epic member (its `.pipeline-state.json` has an `epic` back-pointer); for a standalone feature there is no epic to reconcile. To record one, append an entry to the member state's `epicChangeRequests[]` array (same direct-edit path as `notes`; schema in `references/pipeline-state-schema.json`): `kind` (`add-feature`|`redep`|`move-boundary`|`split`), `target`, `rationale`, `raisedBy: "forge-2-tech"`, `raisedAt` (ISO-8601 UTC), `status: "open"`, and `blocksCurrent`. Set `blocksCurrent: true` when the change alters a contract (`exposes`/`consumes`) or dependency edge this feature relies on for its *next* stage — proceeding to specs would build on a soon-to-change decomposition (this is the point of no cheap return, so bias toward `true`); `false` for a peer/downstream change this feature does not consume. When a contract/dep edge is touched and the classification is ambiguous, confirm `blocksCurrent` with a single the host's question mechanism, defaulting to `true`. **Do not** edit `epic-manifest.json` here — recording is not applying; only `/feature-forge:forge-0-epic` edit mode mutates the epic. Then acknowledge without blocking and continue the tech spec.
|
|
96
|
+
|
|
95
97
|
### Key Decision Areas to Cover
|
|
96
98
|
|
|
97
99
|
Work through these areas across multiple turns, grouping related areas (1-2 per message):
|
|
@@ -289,17 +289,19 @@ Update `{resolvedFeatureDir}/.pipeline-state.json`:
|
|
|
289
289
|
|
|
290
290
|
**Host / clean-room fallback (not a user-selectable option):** if the question mechanism, the host's subagent mechanism, or the `forge-verifier` subagent is unavailable, do **not** run clean-room — degrade to printing `/feature-forge:forge-verify {feature} impl` 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.
|
|
291
291
|
2. **Then clear your session / start a fresh session.** 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 your session / start a fresh session for you — you have to run it yourself.**
|
|
292
|
-
3. **Then run
|
|
292
|
+
3. **Then run the next command** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk:
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
/feature-forge:forge-1-prd {chosen}
|
|
296
|
+
```
|
|
293
297
|
|
|
294
298
|
## Gotchas
|
|
295
299
|
|
|
296
300
|
- **Plugin-root discovery (1b-epic helper) covers installed paths, not workspace-dev checkouts.** The `forge-root.sh` search in 1b-epic probes `~/.claude/skills/feature-forge`, `~/.claude/plugins/cache/*/feature-forge/*` (marketplace-cache installs), `~/.claude/plugins/*/feature-forge`, and `./.agents/skills/feature-forge` — the locations of an **installed** plugin. A feature-forge **source checkout** (e.g. `~/workspace/feature-forge`) is not on that list, so the helper exits "cannot locate plugin root." That is expected in a dev environment, not a bug; run the epic-manifest script from the checkout directly (`python3 <checkout>/scripts/epic-manifest.py …`). The bootstrap prelude wraps its candidate loop in `bash -c` so the `~/.claude/plugins/*/feature-forge` glob is zsh-safe: an empty expansion no longer aborts the loop under zsh's `nomatch`.
|
|
297
301
|
- `{backlogDir}` is a **directory path**, not a file path. Pass `specs/auth`, not `specs/auth/backlog.json`.
|
|
298
|
-
- rauf resolves `RAUF.md` with fallback
|
|
299
|
-
-
|
|
300
|
-
- If the session disconnects during a long-running loop, the runner process continues independently. The user can check results later with the status / list commands.
|
|
302
|
+
- rauf resolves `RAUF.md` with fallback (`{backlogDir}/.rauf/RAUF.md` first, then the project's `.rauf/RAUF.md`) — found as long as the runner is installed in the project. State files (state.json, {loopRunner.logFile}, etc.) are created at `{backlogDir}/{loopRunner.stateDir}/`, within the feature's spec directory (expected) and isolated per backlog dir, so concurrent features don't collide.
|
|
303
|
+
- If the session disconnects during a long-running loop, the runner process continues independently — the user can check results later with the status / list commands. If a previous run left a stale lock, the user may need to pass `--force` to clear it (rauf reports this error clearly).
|
|
301
304
|
- Never run the run command in the foreground (without the host's background-execution mechanism) — it blocks and will hit the Bash tool timeout for any non-trivial backlog. "Don't block the foreground" is NOT "stay silent": supervise via the host's monitoring mechanism (3d), never `sleep`/poll in the foreground. The host's monitoring mechanism must use `persistent: true` (not a bounded `timeout_ms`), watch the **structured** surface (`events.ndjson`), and never filter on raw `RAUF_*` tokens — they appear in agent prose and false-match. A `needs_human`/`blocked`/`review` signal does **not** pause the loop — the runner sets the item aside and keeps going; surface it live but don't tell the user the loop is waiting. See `references/runner-contract.md` for the full monitoring rules.
|
|
302
|
-
- If a previous loop run left a stale lock, the user may need to pass `--force` to clear it. rauf will report this error clearly.
|
|
303
305
|
- The version gate (1c) uses the `--json` form on purpose; never parse `rauf version`'s human output.
|
|
304
306
|
- **Implementation artifacts must not cite specs.** The loop should **read** the specs and `backlog.json` freely — they are the source of truth for what to build, and the backlog rightly references specs for provenance. But the artifacts the loop **writes into the target repo** (source code, generated `SKILL.md`/agent files, configs, code comments) must be **self-contained**: they must NOT reference feature-forge spec files (no `See specs/{feature}/NN-*.md`, no "source spec" provenance notes in shipped output). Specs are pre-implementation inputs that may be archived or deleted once the feature ships; the implementation must stand on its own. This applies only to shipped implementation output — never to the backlog or spec documents, which should keep citing specs.
|
|
305
307
|
|
|
@@ -16,7 +16,11 @@ Loop completed for {feature}. All {N} items implemented successfully.
|
|
|
16
16
|
|
|
17
17
|
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.
|
|
18
18
|
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.
|
|
19
|
-
3. **Then run
|
|
19
|
+
3. **Then run the next command** — in this warm session, or a fresh one if you prefer:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
/feature-forge:forge-6-docs {feature}
|
|
23
|
+
```
|
|
20
24
|
|
|
21
25
|
**Runner review pass.** A review flag (e.g. rauf's `--review`) makes the runner run
|
|
22
26
|
a post-loop review that **auto-creates and implements fix items** rather than handing
|
|
@@ -162,9 +162,9 @@ Load into context ALL artifacts for this feature based on mode:
|
|
|
162
162
|
|
|
163
163
|
Read `references/verification-checklists.md` for the detailed checklists per mode. Execute every check. Do not skip checks because things "look fine." That same reference also holds the relocated **Findings Document Template (Step 4)**, the worked **Example Findings (Step 4)**, and the **Epic Mode State Write Detail (Step 6)** sections used later in this skill.
|
|
164
164
|
|
|
165
|
-
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: ~
|
|
165
|
+
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: ~9 checks), you likely skipped checks — go back and complete them.
|
|
166
166
|
|
|
167
|
-
**Epic mode dispatch.** Epic mode is a small (~
|
|
167
|
+
**Epic mode dispatch.** Epic mode is a small (~9-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..E09 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 and E09 are verifier judgment) and returns its findings.
|
|
168
168
|
|
|
169
169
|
### Important: Be Specific, Not General
|
|
170
170
|
|
|
@@ -223,6 +223,18 @@ python3 "$R/scripts/epic-manifest.py" validate "{epic}" --specs-dir "{specsDir}"
|
|
|
223
223
|
`.pipeline-state.json` `epic` value names this epic, and every `features[]` entry has a
|
|
224
224
|
matching member directory. On conflict the **manifest wins** (REQ-STATE-01); report, do
|
|
225
225
|
not auto-repair.
|
|
226
|
+
- [ ] **CHECK-E09**: **open epic change requests** — any member whose `.pipeline-state.json`
|
|
227
|
+
carries `epicChangeRequests[]` entries with `status: "open"` is surfaced as a **non-fatal**
|
|
228
|
+
finding (one per open request). Severity keys off `blocksCurrent`: a **blocking** request →
|
|
229
|
+
`inconsistency` (the epic decomposition and an in-flight member disagree; specs written now
|
|
230
|
+
would build on a soon-invalid premise), a **non-blocking** request → `improvement` (a
|
|
231
|
+
peer/downstream change to reconcile when convenient). Name the request's `kind`, `target`,
|
|
232
|
+
and `rationale`, and point at `/feature-forge:forge-0-epic {epic}` to reconcile. **Report, do
|
|
233
|
+
not repair** (same posture as CHECK-E07). Which members have open requests comes from the
|
|
234
|
+
same `render-status --json` counts the navigator uses (`features[].openEpicChangeRequests` /
|
|
235
|
+
`.blockingEpicChangeRequests`); the per-request `kind`/`target`/`rationale` detail is read
|
|
236
|
+
from the member `.pipeline-state.json` already loaded in Step 2. This is the pre-emptive
|
|
237
|
+
surface for the divergence class CHECK-E06/E07 otherwise catch only after the fact.
|
|
226
238
|
|
|
227
239
|
## Findings Document Template (Step 4)
|
|
228
240
|
|
|
@@ -39,6 +39,46 @@
|
|
|
39
39
|
"type": "string",
|
|
40
40
|
"description": "Free-form notes persisted between sessions (user can add context before stepping away)"
|
|
41
41
|
},
|
|
42
|
+
"epicChangeRequests": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"description": "Epic-level change requests raised by a member stage (forge-1-prd/forge-2-tech) when the epic DECOMPOSITION itself must change — distinct from the same-feature `notes` parking lot. Read by forge-0-epic edit mode (which applies them) and by forge-session stage-exit (which routes the exit on `blocksCurrent`). Absent/empty for standalone features. Additive/optional: legacy states without it validate unchanged.",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["kind", "target", "rationale", "blocksCurrent", "raisedBy", "raisedAt", "status"],
|
|
48
|
+
"additionalProperties": false,
|
|
49
|
+
"properties": {
|
|
50
|
+
"kind": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"enum": ["add-feature", "redep", "move-boundary", "split"],
|
|
53
|
+
"description": "The decomposition change. add-feature/redep map 1:1 onto edit-mode mutators; move-boundary/split are composite (applied guided-manual in v1)."
|
|
54
|
+
},
|
|
55
|
+
"target": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "The sibling feature to add, or the feature/boundary affected."
|
|
58
|
+
},
|
|
59
|
+
"rationale": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Why the epic must change; seeds the edit-mode charter/prompt when applied."
|
|
62
|
+
},
|
|
63
|
+
"blocksCurrent": {
|
|
64
|
+
"type": "boolean",
|
|
65
|
+
"description": "true → the current feature's next stage would build on a soon-to-change decomposition (pause-now: reconcile before proceeding). false → a peer/downstream change (finish-then-edit). Drives stage-exit routing."
|
|
66
|
+
},
|
|
67
|
+
"raisedBy": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"enum": ["forge-1-prd", "forge-2-tech"],
|
|
70
|
+
"description": "The stage that detected the epic-level concern."
|
|
71
|
+
},
|
|
72
|
+
"raisedAt": { "type": "string", "format": "date-time" },
|
|
73
|
+
"status": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"enum": ["open", "applied", "dismissed"],
|
|
76
|
+
"default": "open",
|
|
77
|
+
"description": "Lifecycle: open → applied|dismissed. Only forge-0-epic edit mode flips open→applied/dismissed; recording stages only create open entries; navigator/verify only read."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
42
82
|
"stages": {
|
|
43
83
|
"type": "object",
|
|
44
84
|
"properties": {
|
|
@@ -151,6 +151,28 @@ outstanding, so the navigator catch-up can fire later.
|
|
|
151
151
|
Verification is already resolved (fresh or explicitly skipped) or the in-stage run
|
|
152
152
|
above covers it. Say so in one line and continue to the NEXT-STEPS block.
|
|
153
153
|
|
|
154
|
+
### `epicReconcile` (epic backflow — present only when there are open requests)
|
|
155
|
+
|
|
156
|
+
Emitted only when the exiting member carries `open` `epicChangeRequests` (recorded by
|
|
157
|
+
`forge-1-prd`/`forge-2-tech` when the epic *decomposition* itself must change — see
|
|
158
|
+
`references/pipeline-state-schema.json`). Absent on the common path and for standalone
|
|
159
|
+
features. The script has already folded the routing into the NEXT-STEPS block, so this
|
|
160
|
+
directive is informational — you do **not** re-derive the wording:
|
|
161
|
+
|
|
162
|
+
- `required: true` (at least one `blocksCurrent: true` request) — the NEXT-STEPS block's
|
|
163
|
+
fenced **primary** command is the epic reconcile command
|
|
164
|
+
(`/feature-forge:forge-0-epic {epic}`), and the normal next stage is demoted to a
|
|
165
|
+
follow-up line ("After reconciling, continue with …"). This is *reconcile-before-specs*:
|
|
166
|
+
proceeding would author artifacts against a decomposition that is about to change. It is
|
|
167
|
+
strongest when exiting `forge-2-tech` (next is `forge-3-specs`, the point of no cheap
|
|
168
|
+
return).
|
|
169
|
+
- `reminder: true` (only `blocksCurrent: false` requests) — normal next-stage routing is
|
|
170
|
+
unchanged; the block appends a non-blocking reminder line ("You also flagged N epic
|
|
171
|
+
change(s) to reconcile when convenient …"). This is *finish-then-edit*.
|
|
172
|
+
|
|
173
|
+
Either way the added lines are host-neutral (no literal `/clear`) and sit **above** the
|
|
174
|
+
sentinel; just print the NEXT-STEPS block verbatim as always.
|
|
175
|
+
|
|
154
176
|
### The NEXT-STEPS block (always last)
|
|
155
177
|
|
|
156
178
|
Print the script's NEXT-STEPS block **verbatim as your absolute last output**. Nothing
|
|
@@ -181,7 +203,11 @@ Slots: `{stage}` (a lowercase noun phrase), `{verify-command}`, `{next-command}`
|
|
|
181
203
|
|
|
182
204
|
**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.
|
|
183
205
|
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.**
|
|
184
|
-
3. **Then run
|
|
206
|
+
3. **Then run the next command** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
{next-command}
|
|
210
|
+
```
|
|
185
211
|
<!-- END: standard-exit-block -->
|
|
186
212
|
|
|
187
213
|
---
|
|
@@ -206,5 +232,9 @@ by the loop itself, so this block defers rather than re-presenting a gate.
|
|
|
206
232
|
|
|
207
233
|
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.
|
|
208
234
|
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.
|
|
209
|
-
3. **Then run
|
|
235
|
+
3. **Then run the next command** — in this warm session, or a fresh one if you prefer:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
{next-command}
|
|
239
|
+
```
|
|
210
240
|
<!-- END: warm-exit-block -->
|
|
@@ -99,6 +99,13 @@ class FeatureStatus(TypedDict):
|
|
|
99
99
|
blocked: True if any entry in unmetDeps is non-empty.
|
|
100
100
|
unmetDeps: Names of this feature's direct dependencies that are not yet
|
|
101
101
|
complete-for-orchestration (00 §7). Empty when actionable or complete.
|
|
102
|
+
openEpicChangeRequests: Count of this member's ``epicChangeRequests``
|
|
103
|
+
entries with ``status == "open"`` — epic-level change requests raised
|
|
104
|
+
by a member stage that forge-0-epic edit mode has not yet reconciled.
|
|
105
|
+
0 for standalone features or members with no pending requests.
|
|
106
|
+
blockingEpicChangeRequests: The subset of ``openEpicChangeRequests`` with
|
|
107
|
+
``blocksCurrent == true`` (pause-now, reconcile-before-specs). Always
|
|
108
|
+
``<= openEpicChangeRequests``.
|
|
102
109
|
"""
|
|
103
110
|
|
|
104
111
|
name: str
|
|
@@ -106,6 +113,8 @@ class FeatureStatus(TypedDict):
|
|
|
106
113
|
status: DerivedStatus
|
|
107
114
|
blocked: bool
|
|
108
115
|
unmetDeps: list[str]
|
|
116
|
+
openEpicChangeRequests: int
|
|
117
|
+
blockingEpicChangeRequests: int
|
|
109
118
|
|
|
110
119
|
|
|
111
120
|
class Rollup(TypedDict):
|
|
@@ -836,12 +845,26 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
|
|
|
836
845
|
)
|
|
837
846
|
derived = "in-progress" if started else "not-started"
|
|
838
847
|
|
|
848
|
+
# Epic-backflow surfacing (Phase 2): count open epicChangeRequests from the
|
|
849
|
+
# same state dict. A missing/torn state, a non-list value, or non-dict items
|
|
850
|
+
# count as 0 — a malformed request must never crash the dashboard, mirroring
|
|
851
|
+
# the torn-state -> not-started tolerance above.
|
|
852
|
+
requests = state.get("epicChangeRequests", [])
|
|
853
|
+
open_reqs = [
|
|
854
|
+
r for r in requests
|
|
855
|
+
if isinstance(r, dict) and r.get("status") == "open"
|
|
856
|
+
] if isinstance(requests, list) else []
|
|
857
|
+
open_count = len(open_reqs)
|
|
858
|
+
blocking_count = sum(1 for r in open_reqs if r.get("blocksCurrent") is True)
|
|
859
|
+
|
|
839
860
|
return {
|
|
840
861
|
"name": name,
|
|
841
862
|
"stage": stage,
|
|
842
863
|
"status": derived,
|
|
843
864
|
"blocked": False,
|
|
844
865
|
"unmetDeps": [],
|
|
866
|
+
"openEpicChangeRequests": open_count,
|
|
867
|
+
"blockingEpicChangeRequests": blocking_count,
|
|
845
868
|
}
|
|
846
869
|
|
|
847
870
|
|
|
@@ -1212,6 +1235,9 @@ def _print_status_table(status: RenderStatus) -> None:
|
|
|
1212
1235
|
line = f" - {row['name']}: {row['status']} (stage {row['stage']})"
|
|
1213
1236
|
if row["blocked"]:
|
|
1214
1237
|
line += f" — blocked on {', '.join(row['unmetDeps'])}"
|
|
1238
|
+
if row["openEpicChangeRequests"]:
|
|
1239
|
+
marker = "⚠️ BLOCKING" if row["blockingEpicChangeRequests"] else "⚠️"
|
|
1240
|
+
line += f" — {marker} {row['openEpicChangeRequests']} pending epic change(s)"
|
|
1215
1241
|
print(line)
|
|
1216
1242
|
if status["actionable"]:
|
|
1217
1243
|
print(f"Actionable: {', '.join(status['actionable'])}")
|