@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
|
@@ -37,7 +37,7 @@ from __future__ import annotations
|
|
|
37
37
|
import argparse
|
|
38
38
|
import json
|
|
39
39
|
import sys
|
|
40
|
-
from datetime import datetime
|
|
40
|
+
from datetime import datetime, timezone
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
from typing import Final, TypedDict
|
|
43
43
|
|
|
@@ -102,6 +102,10 @@ class FeatureRow(TypedDict):
|
|
|
102
102
|
nextCommand: str | None
|
|
103
103
|
verifyPending: bool
|
|
104
104
|
verifyCommand: str | None
|
|
105
|
+
verifyStage: str | None
|
|
106
|
+
verifyState: str
|
|
107
|
+
autoVerify: bool
|
|
108
|
+
autoFix: bool
|
|
105
109
|
|
|
106
110
|
|
|
107
111
|
class UsageError(Exception):
|
|
@@ -181,13 +185,52 @@ def next_stage(state: dict) -> str | None:
|
|
|
181
185
|
return None
|
|
182
186
|
|
|
183
187
|
|
|
184
|
-
def
|
|
185
|
-
"""Return the
|
|
188
|
+
def _stage_version(state: dict, stage: str) -> int | None:
|
|
189
|
+
"""Return the recorded ``version`` of a stage entry, or None if absent."""
|
|
190
|
+
stages = state.get("stages")
|
|
191
|
+
if not isinstance(stages, dict):
|
|
192
|
+
return None
|
|
193
|
+
entry = stages.get(stage)
|
|
194
|
+
if not isinstance(entry, dict):
|
|
195
|
+
return None
|
|
196
|
+
version = entry.get("version")
|
|
197
|
+
return version if isinstance(version, int) else None
|
|
198
|
+
|
|
186
199
|
|
|
187
|
-
|
|
188
|
-
``forge-verify-*``
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
def _verify_entry(state: dict, verify_key: str) -> dict:
|
|
201
|
+
"""Return the ``forge-verify-*`` entry dict, or ``{}`` if absent."""
|
|
202
|
+
stages = state.get("stages")
|
|
203
|
+
if not isinstance(stages, dict):
|
|
204
|
+
return {}
|
|
205
|
+
entry = stages.get(verify_key)
|
|
206
|
+
return entry if isinstance(entry, dict) else {}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def verify_state(state: dict) -> tuple[str | None, str]:
|
|
210
|
+
"""Classify verify freshness for the most-recently-completed stage.
|
|
211
|
+
|
|
212
|
+
Returns ``(stage, state_label)`` where ``state_label`` is one of:
|
|
213
|
+
|
|
214
|
+
- ``fresh`` — verify is resolved AND its ``verifiedStageVersion`` matches the
|
|
215
|
+
stage's current ``version`` (so no re-verify is needed).
|
|
216
|
+
- ``stale`` — verify was resolved once, but the stage version has since moved
|
|
217
|
+
(artifact revised) OR the entry predates the freshness ledger (no
|
|
218
|
+
``verifiedStageVersion``). A revised artifact must be re-verified.
|
|
219
|
+
- ``failing`` — verify ran and reported findings that are not yet applied
|
|
220
|
+
(``findings-reported``).
|
|
221
|
+
- ``never`` — the stage completed but verify has not run at all.
|
|
222
|
+
- ``skipped`` — the user explicitly chose to proceed without verifying. A
|
|
223
|
+
resolved, non-pending state: it is deliberately NOT re-offered or
|
|
224
|
+
auto-verified, and (unlike a genuine verification result) it does not go
|
|
225
|
+
stale on an artifact revision — skip writers record no version to compare
|
|
226
|
+
against, and re-surfacing would override an explicit human decision.
|
|
227
|
+
- ``none`` — no completed verify-capable stage (nothing to verify), stage
|
|
228
|
+
is ``None``.
|
|
229
|
+
|
|
230
|
+
Only the most-recent completed production stage is considered, matching the
|
|
231
|
+
navigator's "verify before continuing" gate. Absent ``verifiedStageVersion``
|
|
232
|
+
on a ``passed``/``findings-applied`` entry (legacy state) is deliberately
|
|
233
|
+
treated as ``stale`` — verify rather than skip.
|
|
191
234
|
"""
|
|
192
235
|
for stage in reversed(PRODUCTION_STAGES):
|
|
193
236
|
if _stage_status(state, stage) != _DONE_STATUS:
|
|
@@ -195,11 +238,41 @@ def pending_verify(state: dict) -> str | None:
|
|
|
195
238
|
token = VERIFY_TOKEN_BY_STAGE.get(stage)
|
|
196
239
|
if token is None:
|
|
197
240
|
continue # forge-6-docs has no verify step
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
241
|
+
entry = _verify_entry(state, f"forge-verify-{token}")
|
|
242
|
+
status = entry.get("status")
|
|
243
|
+
if status == "skipped":
|
|
244
|
+
# An explicit skip is resolved and non-pending — preserve the user's
|
|
245
|
+
# decision. It never goes stale (no recorded version to compare), so
|
|
246
|
+
# the freshness check below deliberately does not apply.
|
|
247
|
+
return stage, "skipped"
|
|
248
|
+
if status not in _VERIFY_RESOLVED:
|
|
249
|
+
if status == "findings-reported":
|
|
250
|
+
return stage, "failing"
|
|
251
|
+
return stage, "never"
|
|
252
|
+
verified_version = entry.get("verifiedStageVersion")
|
|
253
|
+
stage_version = _stage_version(state, stage)
|
|
254
|
+
if (
|
|
255
|
+
isinstance(verified_version, int)
|
|
256
|
+
and stage_version is not None
|
|
257
|
+
and verified_version == stage_version
|
|
258
|
+
):
|
|
259
|
+
return stage, "fresh"
|
|
260
|
+
return stage, "stale"
|
|
261
|
+
return None, "none"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def pending_verify(state: dict) -> str | None:
|
|
265
|
+
"""Return the production stage whose verify is outstanding, if any.
|
|
266
|
+
|
|
267
|
+
Outstanding means the most-recently-completed production stage's verify is not
|
|
268
|
+
``fresh`` (never run, reported findings, or gone stale after an artifact
|
|
269
|
+
revision). An explicit ``skipped`` is treated as resolved (never outstanding).
|
|
270
|
+
Surfaced so the navigator can offer "verify before continuing" as an
|
|
271
|
+
alternative to advancing. Returns ``None`` when the latest stage is fresh,
|
|
272
|
+
skipped, or there is nothing to verify.
|
|
273
|
+
"""
|
|
274
|
+
stage, label = verify_state(state)
|
|
275
|
+
return stage if label not in ("fresh", "none", "skipped") else None
|
|
203
276
|
|
|
204
277
|
|
|
205
278
|
def _parse_ts(value: str | None) -> datetime | None:
|
|
@@ -207,25 +280,37 @@ def _parse_ts(value: str | None) -> datetime | None:
|
|
|
207
280
|
if not isinstance(value, str):
|
|
208
281
|
return None
|
|
209
282
|
try:
|
|
210
|
-
|
|
283
|
+
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
211
284
|
except ValueError:
|
|
212
285
|
return None
|
|
286
|
+
if dt.tzinfo is None:
|
|
287
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
288
|
+
return dt
|
|
213
289
|
|
|
214
290
|
|
|
215
|
-
def build_rows(specs_dir: Path) -> list[FeatureRow]:
|
|
291
|
+
def build_rows(specs_dir: Path, config: dict | None = None) -> list[FeatureRow]:
|
|
216
292
|
"""Build the recency-ranked active-feature rows (the rank-features payload).
|
|
217
293
|
|
|
218
294
|
Active features (``pipelineStatus == "active"``, the default when absent) are
|
|
219
295
|
sorted by ``updatedAt`` descending — most recently touched first — so the
|
|
220
296
|
navigator's recency default is row 0.
|
|
297
|
+
|
|
298
|
+
``config`` is the loaded forge.config.json (or ``{}``); it drives the effective
|
|
299
|
+
``autoVerify``/``autoFix`` per stage so the navigator can branch without
|
|
300
|
+
re-reading config.
|
|
221
301
|
"""
|
|
302
|
+
config = config or {}
|
|
303
|
+
# Fail closed: only a literal JSON ``true`` enables artifact-mutating autoFix.
|
|
304
|
+
global_auto_fix = config.get("autoFix") is True
|
|
222
305
|
rows: list[FeatureRow] = []
|
|
223
306
|
for name, epic, state in _scan_features(specs_dir):
|
|
224
307
|
status = state.get("pipelineStatus", "active")
|
|
225
308
|
if status != "active":
|
|
226
309
|
continue
|
|
227
310
|
nxt = next_stage(state)
|
|
228
|
-
|
|
311
|
+
vstage, vlabel = verify_state(state)
|
|
312
|
+
verify_pending = vstage is not None and vlabel not in ("fresh", "none", "skipped")
|
|
313
|
+
effective_auto_verify = auto_verify_for(config, vstage) if vstage else False
|
|
229
314
|
branch = state.get("branch")
|
|
230
315
|
updated = state.get("updatedAt")
|
|
231
316
|
rows.append({
|
|
@@ -237,12 +322,16 @@ def build_rows(specs_dir: Path) -> list[FeatureRow]:
|
|
|
237
322
|
"complete": nxt is None,
|
|
238
323
|
"nextStage": nxt,
|
|
239
324
|
"nextCommand": f"/feature-forge:{nxt} {name}" if nxt else None,
|
|
240
|
-
"verifyPending":
|
|
241
|
-
"verifyCommand": f"/feature-forge:forge-verify {name}" if
|
|
325
|
+
"verifyPending": verify_pending,
|
|
326
|
+
"verifyCommand": f"/feature-forge:forge-verify {name}" if verify_pending else None,
|
|
327
|
+
"verifyStage": vstage,
|
|
328
|
+
"verifyState": vlabel,
|
|
329
|
+
"autoVerify": effective_auto_verify,
|
|
330
|
+
"autoFix": global_auto_fix and effective_auto_verify,
|
|
242
331
|
})
|
|
243
332
|
# Sort by updatedAt desc; rows without a parseable timestamp sort last.
|
|
244
333
|
rows.sort(
|
|
245
|
-
key=lambda r: (_parse_ts(r["updatedAt"]) or datetime.min.replace(tzinfo=
|
|
334
|
+
key=lambda r: (_parse_ts(r["updatedAt"]) or datetime.min.replace(tzinfo=timezone.utc)),
|
|
246
335
|
reverse=True,
|
|
247
336
|
)
|
|
248
337
|
return rows
|
|
@@ -308,12 +397,17 @@ def _last_usage(transcript: Path) -> tuple[int, str | None] | None:
|
|
|
308
397
|
usage = message.get("usage") if isinstance(message, dict) else record.get("usage")
|
|
309
398
|
if not isinstance(usage, dict):
|
|
310
399
|
continue
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
400
|
+
# A malformed transcript may carry a non-numeric usage field; skip that
|
|
401
|
+
# record rather than crash the whole context-usage read (ValueError/TypeError).
|
|
402
|
+
try:
|
|
403
|
+
total = (
|
|
404
|
+
int(usage.get("input_tokens", 0) or 0)
|
|
405
|
+
+ int(usage.get("cache_creation_input_tokens", 0) or 0)
|
|
406
|
+
+ int(usage.get("cache_read_input_tokens", 0) or 0)
|
|
407
|
+
+ int(usage.get("output_tokens", 0) or 0)
|
|
408
|
+
)
|
|
409
|
+
except (TypeError, ValueError):
|
|
410
|
+
continue
|
|
317
411
|
if total <= 0:
|
|
318
412
|
continue
|
|
319
413
|
model = message.get("model") if isinstance(message, dict) else record.get("model")
|
|
@@ -328,13 +422,53 @@ def _infer_window(model: str | None) -> int:
|
|
|
328
422
|
return _DEFAULT_WINDOW
|
|
329
423
|
|
|
330
424
|
|
|
331
|
-
def
|
|
332
|
-
"""Read
|
|
425
|
+
def _load_config(config_path: Path) -> dict:
|
|
426
|
+
"""Read forge.config.json into a dict, tolerating missing/corrupt files.
|
|
427
|
+
|
|
428
|
+
A missing, unreadable, or non-object config downgrades to ``{}`` so callers
|
|
429
|
+
read every key through absent-safe ``.get`` defaults.
|
|
430
|
+
"""
|
|
333
431
|
try:
|
|
334
432
|
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
335
433
|
except (OSError, json.JSONDecodeError):
|
|
336
|
-
return
|
|
337
|
-
return config
|
|
434
|
+
return {}
|
|
435
|
+
return config if isinstance(config, dict) else {}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _config_value(config_path: Path, key: str):
|
|
439
|
+
"""Read a single key from forge.config.json, or None if absent/unreadable."""
|
|
440
|
+
return _load_config(config_path).get(key)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def auto_verify_for(config: dict, stage: str) -> bool:
|
|
444
|
+
"""Return the effective auto-verify setting for ``stage``.
|
|
445
|
+
|
|
446
|
+
Per-stage override in ``autoVerifyStages`` wins over the global ``autoVerify``;
|
|
447
|
+
both default to off, so a config with neither key means "no auto-verify".
|
|
448
|
+
|
|
449
|
+
Parsing is strict and **fails closed**: only a literal JSON ``true`` enables
|
|
450
|
+
auto-verify. A non-boolean value (e.g. the string ``"false"``, which is truthy
|
|
451
|
+
in Python) is treated as off, not on. The schema already rejects non-booleans
|
|
452
|
+
at author time; this guards a hand-edited config from silently enabling
|
|
453
|
+
automation.
|
|
454
|
+
"""
|
|
455
|
+
stages = config.get("autoVerifyStages")
|
|
456
|
+
if isinstance(stages, dict) and stage in stages:
|
|
457
|
+
return stages[stage] is True
|
|
458
|
+
return config.get("autoVerify") is True
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def invalid_auto_verify_keys(config: dict) -> list[str]:
|
|
462
|
+
"""Return ``autoVerifyStages`` keys outside the verify-capable stage ids.
|
|
463
|
+
|
|
464
|
+
An unknown/typo key (e.g. ``forge-1-prod``) would silently never take effect,
|
|
465
|
+
turning an intended off-switch into a no-op. Surfacing it lets the navigator
|
|
466
|
+
warn instead of failing quietly. Mirrors the schema's ``propertyNames.enum``.
|
|
467
|
+
"""
|
|
468
|
+
stages = config.get("autoVerifyStages")
|
|
469
|
+
if not isinstance(stages, dict):
|
|
470
|
+
return []
|
|
471
|
+
return [key for key in stages if key not in VERIFY_TOKEN_BY_STAGE]
|
|
338
472
|
|
|
339
473
|
|
|
340
474
|
def context_usage(
|
|
@@ -449,6 +583,7 @@ def main() -> int:
|
|
|
449
583
|
|
|
450
584
|
p_rank = sub.add_parser("rank-features", help="Rank active features by recency")
|
|
451
585
|
p_rank.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
586
|
+
p_rank.add_argument("--config", default="./forge.config.json", help="forge.config.json path")
|
|
452
587
|
p_rank.add_argument("--json", action="store_true", dest="json_output")
|
|
453
588
|
|
|
454
589
|
p_ctx = sub.add_parser("context-usage", help="Report live context-window usage")
|
|
@@ -462,12 +597,22 @@ def main() -> int:
|
|
|
462
597
|
try:
|
|
463
598
|
if args.cmd == "rank-features":
|
|
464
599
|
specs_dir = Path(args.specs_dir)
|
|
465
|
-
|
|
600
|
+
config = _load_config(Path(args.config))
|
|
601
|
+
rows = build_rows(specs_dir, config)
|
|
466
602
|
counts = _counts(specs_dir)
|
|
603
|
+
invalid_keys = invalid_auto_verify_keys(config)
|
|
467
604
|
if args.json_output:
|
|
468
|
-
|
|
605
|
+
payload = {"active": rows, "counts": counts}
|
|
606
|
+
if invalid_keys:
|
|
607
|
+
payload["invalidAutoVerifyKeys"] = invalid_keys
|
|
608
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
469
609
|
else:
|
|
470
610
|
_print_rank_table(rows, counts)
|
|
611
|
+
if invalid_keys:
|
|
612
|
+
print(
|
|
613
|
+
" ! invalid autoVerifyStages keys (ignored): "
|
|
614
|
+
+ ", ".join(invalid_keys)
|
|
615
|
+
)
|
|
471
616
|
return 0
|
|
472
617
|
|
|
473
618
|
if args.cmd == "context-usage":
|
|
@@ -29,19 +29,19 @@ For pipeline architecture details, read `references/process-overview.md`.
|
|
|
29
29
|
|
|
30
30
|
1. **Epics first.** Identify epic directories as any `{specsDir}/*/` that directly contains an `epic-manifest.json` **and no `.pipeline-state.json` of its own** (an epic root is never itself a feature). For each epic, run:
|
|
31
31
|
```bash
|
|
32
|
-
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)"
|
|
32
|
+
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')"
|
|
33
33
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
34
34
|
python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specsDir}" --json
|
|
35
35
|
```
|
|
36
36
|
and show one rollup line: `{epic} — {complete}/{total} complete, next: {nextCommand}`.
|
|
37
37
|
2. **Standalone features below.** Scan the remaining `{specsDir}/*/` that directly contain a `.pipeline-state.json` **without** an `epic` back-pointer. A nested member's `.pipeline-state.json` is **attributed to its epic (Tier 1), never listed as a standalone feature**.
|
|
38
|
-
- **Rank by recency.** Run the recency ranker so the most-recently-touched active feature is the default — the user rarely has to type a name (especially on mobile after a
|
|
38
|
+
- **Rank by recency.** Run the recency ranker so the most-recently-touched active feature is the default — the user rarely has to type a name (especially on mobile after a clear your session / start a fresh session):
|
|
39
39
|
```bash
|
|
40
|
-
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)"
|
|
40
|
+
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')"
|
|
41
41
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
42
42
|
python3 "$R/scripts/forge-session.py" rank-features --specs-dir "{specsDir}" --json
|
|
43
43
|
```
|
|
44
|
-
This returns `{active: [...], counts: {...}}` with active features sorted by `updatedAt` **descending** (row 0 is the most recent). Each row carries `currentStage`, `nextStage`, `nextCommand`, `verifyPending`, and `
|
|
44
|
+
This returns `{active: [...], counts: {...}}` with active features sorted by `updatedAt` **descending** (row 0 is the most recent). Each row carries `currentStage`, `nextStage`, `nextCommand`, `verifyPending`, `verifyCommand`, `verifyStage`, `verifyState` (`fresh`/`stale`/`failing`/`never`/`none`), `autoVerify` (the effective per-stage setting), and `autoFix` (the single source of stage order). A top-level `invalidAutoVerifyKeys` array appears when `forge.config.json` has `autoVerifyStages` keys outside the five verify-capable stages — surface it as a one-line warning. The `active` list excludes nested epic members surfaced in Tier 1 — but the ranker scans them too, so ignore rows whose `epic` is non-null here (they belong to the epic rollup).
|
|
45
45
|
- **Pick the feature:** if exactly one active standalone pipeline exists, show its dashboard. If multiple exist, use the host's question mechanism — **list the most-recently-updated first, labeled `(recommended)`**, each option's description showing its `currentStage` and a relative age ("updated 2h ago"). Always include a free-form escape ("A different feature / something else") so the user is never boxed in. Then render the chosen feature's dashboard.
|
|
46
46
|
|
|
47
47
|
If no epics and no standalone features exist, say: "No active feature pipelines found. Start one with `/feature-forge:forge-1-prd <feature-name>` or group several with `/feature-forge:forge-0-epic <epic-name>`."
|
|
@@ -87,31 +87,45 @@ Use these status indicators:
|
|
|
87
87
|
|
|
88
88
|
### 3b. Drive to the Next Stage
|
|
89
89
|
|
|
90
|
-
After rendering a **per-feature** dashboard for an **active** pipeline (skip this for paused/abandoned pipelines and for the Epic Dashboard), don't stop at a text suggestion — actively offer to start the next stage. This removes the copy-paste-after
|
|
90
|
+
After rendering a **per-feature** dashboard for an **active** pipeline (skip this for paused/abandoned pipelines and for the Epic Dashboard), don't stop at a text suggestion — actively offer to start the next stage. This removes the copy-paste-after-clear your session / start a fresh session chore that makes long, multi-stage runs painful (especially on mobile).
|
|
91
91
|
|
|
92
|
-
**1. Read the next step.** From the `rank-features --json` output (above), find this feature's row and read its `nextStage`, `nextCommand`, `verifyPending`, and `
|
|
92
|
+
**1. Read the next step.** From the `rank-features --json` output (above), find this feature's row and read its `nextStage`, `nextCommand`, `verifyPending`, `verifyCommand`, `verifyStage`, `verifyState`, `autoVerify`, and `autoFix`. If the feature is not in the `active` list (paused/abandoned), or `nextStage` is `null` (every production stage complete), skip the drive prompt — instead congratulate the user and, if `forge-6-docs` has not run, offer it; otherwise note the pipeline is complete. If the payload has a non-empty `invalidAutoVerifyKeys`, print a one-line warning first (e.g. "⚠️ forge.config.json `autoVerifyStages` has unknown keys: … — they are ignored; fix the typo").
|
|
93
93
|
|
|
94
94
|
**2. Check the context window.** Run the context-usage helper so you can advise whether to continue here or start the next stage in a fresh session:
|
|
95
95
|
```bash
|
|
96
|
-
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)"
|
|
96
|
+
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')"
|
|
97
97
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
98
98
|
python3 "$R/scripts/forge-session.py" context-usage --json
|
|
99
99
|
```
|
|
100
100
|
- `{"available": true, ...}` → note `pct` (e.g. "context ~68% full") and `overThreshold`. Window/threshold come from `contextWindowTokens` / `contextWarnThreshold` in `forge.config.json` (the helper defaults to a 200k window and 0.7 threshold, and auto-bumps the assumed window to 1M once observed usage exceeds 200k; **on a 1M-context model set `contextWindowTokens: 1000000` so the percentage is accurate below 200k too** — 1M can't be detected from the transcript until usage crosses 200k).
|
|
101
101
|
- `{"available": false, ...}` → omit context advice silently (non-Claude host, or a fresh session with no transcript). Never treat this as an error.
|
|
102
102
|
|
|
103
|
-
**
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
- **Verify `{stage}` first**
|
|
103
|
+
**2b. Auto-verify branch (when `verifyPending` is true).** Before offering the advance gate, decide whether verify runs automatically:
|
|
104
|
+
|
|
105
|
+
- **`autoVerify` is true for the just-completed `verifyStage`** → **skip the verify question entirely** and run verify now, *provided it can run clean-room*. Auto-verify is safe to run unattended only because verify executes in a fresh `forge-verifier` subagent that inherits none of this session's context (so no clear your session / start a fresh session is needed and only a compact digest returns). Guard the clean-room assumption: proceed unattended **only when the host's subagent mechanism + `forge-verifier` subagent are available**. Invoke `feature-forge:forge-verify` via the host's skill-invocation mechanism in **require-clean (`auto`) mode** — in that mode forge-verify refuses to run inline and returns a sentinel if the subagent is not dispatchable (see `skills/forge-verify/SKILL.md`). Then:
|
|
106
|
+
- **Sentinel returned (clean-room unavailable)** → do **not** run verify inline. Degrade to the manual gate: fall through to step 3 with the **"Verify `{stage}` first (manual)"** option included, and if `overThreshold`, recommend "clear your session / start a fresh session, then verify in a clean session." Verify state stays outstanding; the stage is never advanced on false assurance.
|
|
107
|
+
- **Verify passed / no findings** → proceed to the normal advance gate (step 3), no verify option.
|
|
108
|
+
- **Verify found findings** →
|
|
109
|
+
- **`autoFix` is true AND preconditions hold** — the findings document has **zero unresolved decision points** and the **working tree is clean** → invoke `feature-forge:forge-fix` via the host's skill-invocation mechanism, then run a **mandatory re-verify** (require-clean mode). Advance only if the re-verify passes. On **any** precondition miss (decisions required, dirty tree), a forge-fix early stop, or a red re-verify → fall back to the digest + prompt below (never a silent partial mutation).
|
|
110
|
+
- **`autoFix` is false (default), or preconditions failed** → present a **compact findings digest** as text, then the host's question mechanism: *Apply fixes now (forge-fix)* / *Review findings* / *Skip & advance*.
|
|
111
|
+
- **`autoVerify` is false/unconfigured** → do not auto-run; use the advance gate in step 3, which gains the folded opt-in verify options ("Verify `{stage}` now" and "Verify `{stage}` now + enable auto-verify going forward").
|
|
112
|
+
|
|
113
|
+
**3. Offer the next step via the host's question mechanism.** (Reached when auto-verify did not fully resolve the step, or `verifyPending` is false.) Output the dashboard + a one-line context note as text, then ask (per the Decision Support protocol in `references/shared-conventions.md`). This is the same **Stage Exit Protocol** the stage skills stamp (`references/stage-exit-protocol.md`) — a clean session at the boundary is recommended on its own merits, and `overThreshold` only modulates *how emphatically*, never *whether*. Options, in this order:
|
|
114
|
+
- **Start `{nextStage}` in a clean session** — **recommended, unconditionally**, at every boundary. A clean session is the right default for the next stage on its own merits, independent of window size. The work survives a clear because all state is on disk: instruct the user to clear your session / start a fresh session, then re-run `/feature-forge:forge {feature}` (or run `{nextCommand}` directly) in the fresh session. Note plainly that you cannot clear your session / start a fresh session for them. **When `overThreshold` is true**, strengthen this wording (the window is also genuinely full) and add mid-stage compaction advice; **when the window is healthy**, still recommend the clean session — `overThreshold` is a *secondary, additive* modifier here, not the clear on/off switch.
|
|
115
|
+
- **Continue in this session** — the always-available alternative, reasonable when the window is nearly empty and the user would rather not clear. Not the default.
|
|
116
|
+
- **Verify `{stage}` now** — include **only when `verifyPending` is true and `autoVerify` is false**, and the clean-room path is available. **Recommended**, since verify is clean-room and rarely worth skipping. Runs verify now (require-clean mode); **does not** change config.
|
|
117
|
+
- **Verify `{stage}` now + enable auto-verify going forward** — same trigger as the option above; runs verify now **and** patches `autoVerify: true` into `forge.config.json` in place (preserve formatting and other keys) so every future stage verifies automatically without prompting. This is the **folded** config-enable — it replaces the old post-hoc "make auto-verify the default?" follow-up. No silent config writes: the config changes **only** when the user picks this option.
|
|
118
|
+
- **Verify `{stage}` first (manual)** — include **only when `verifyPending` is true** and the clean-room path is unavailable (the Tier-2 degradation above), or the host lacks the host's skill-invocation and subagent mechanisms; selecting it runs `{verifyCommand}` (inline / manual). Offer the "enable auto-verify" choice as text only if a config write is possible.
|
|
107
119
|
- **Pick a different stage** — free-form escape to any stage or other action.
|
|
108
120
|
|
|
109
121
|
**4. Act on the choice.**
|
|
110
|
-
- **
|
|
111
|
-
- **
|
|
112
|
-
- **Verify** → invoke `feature-forge:forge-verify` via the
|
|
122
|
+
- **Clean session** (the default) → give the exact next command and the clear your session / start a fresh session-then-re-run instruction; do not invoke anything (you cannot clear your session / start a fresh session for the user).
|
|
123
|
+
- **Continue in this session** → if `autoInvokeNextStage` is true (default) **and** the host's skill-invocation mechanism is available, invoke the chosen stage **via the host's skill-invocation mechanism** in this same session (e.g. `skill: "feature-forge:forge-3-specs"`, `args: "{feature}"`) — no retyping, no paste. If `autoInvokeNextStage` is false, or the host's skill-invocation mechanism is unavailable (a non-Claude host), fall back to printing `{nextCommand}` prominently for the user to run.
|
|
124
|
+
- **Verify now / Verify now + enable auto-verify** → invoke `feature-forge:forge-verify` via the host's skill-invocation mechanism (require-clean mode). For the **enable-auto-verify** variant, additionally patch `autoVerify: true` into `forge.config.json` in place (preserve formatting and other keys) — never a silent write. On a non-Claude host or when degrading to the manual/inline gate, print `{verifyCommand}` instead.
|
|
113
125
|
- **Different stage** → honor the free-form request.
|
|
114
126
|
|
|
127
|
+
**Host fallback.** On a non-Claude host or when the host's skill-invocation and subagent mechanisms are unavailable, auto-verify never runs unattended — fall back to printing `{verifyCommand}` exactly as today, mirroring `autoInvokeNextStage`.
|
|
128
|
+
|
|
115
129
|
This applies whether the feature was named explicitly (`/feature-forge:forge {feature}`) or resolved from the recency default.
|
|
116
130
|
|
|
117
131
|
### Epic Dashboard
|
|
@@ -119,7 +133,7 @@ This applies whether the feature was named explicitly (`/feature-forge:forge {fe
|
|
|
119
133
|
When the named argument is an epic (`{specsDir}/{name}/epic-manifest.json` exists), render the epic dashboard instead of a per-feature one. Run:
|
|
120
134
|
|
|
121
135
|
```bash
|
|
122
|
-
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)"
|
|
136
|
+
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')"
|
|
123
137
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
124
138
|
python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specsDir}" --json
|
|
125
139
|
```
|
|
@@ -181,16 +195,16 @@ Support these sub-commands for pipeline lifecycle management:
|
|
|
181
195
|
- `/feature-forge:forge abandon {feature}` — Set `pipelineStatus` to `"abandoned"`. Use the host's question mechanism to confirm first, and state what's reversible: abandoning does not delete artifacts and can be undone with `/feature-forge:forge resume {feature}`, so the cost is low — but if the user really means "stop and discard," point out that `pause` is the better choice when they're only setting it aside. Offer **Abandon** · **Pause instead** · **Cancel**.
|
|
182
196
|
- `/feature-forge:forge run [{feature}]` — **Opt-in auto-advance.** Drive the feature through consecutive stages in one session instead of confirming each boundary. This is a convenience wrapper over **3b. Drive to the Next Stage** — same stage order, same context gate — just looped:
|
|
183
197
|
1. Resolve the feature (if omitted, use the recency default from `rank-features`; if multiple are equally plausible, ask once via the host's question mechanism).
|
|
184
|
-
2. **Before each stage,** run `forge-session.py context-usage`. If `overThreshold` is true, **stop** and recommend a clean session (give the exact `{nextCommand}` and the
|
|
185
|
-
3. Otherwise invoke the next stage's skill via the
|
|
198
|
+
2. **Before each stage,** run `forge-session.py context-usage`. If `overThreshold` is true, **stop** and recommend a clean session (give the exact `{nextCommand}` and the clear your session / start a fresh session-then-re-run instruction) — never auto-clear your session / start a fresh session.
|
|
199
|
+
3. Otherwise invoke the next stage's skill via the host's skill-invocation mechanism, let it run to its natural stopping point, then re-read state and continue from step 2.
|
|
186
200
|
4. **Stop conditions:** the next stage is an interview/decision point that calls the host's question mechanism (PRD and tech inherently pause for input — let them); `nextStage` is `null` (pipeline complete); context over threshold; or a stage signals it needs human input / is blocked. Always report where the loop stopped and why.
|
|
187
|
-
Per-stage confirmation (3b) remains the default — `run` is only used when the user explicitly asks to "run" / "drive" / "auto-advance" the pipeline. On a non-Claude host where the
|
|
201
|
+
Per-stage confirmation (3b) remains the default — `run` is only used when the user explicitly asks to "run" / "drive" / "auto-advance" the pipeline. On a non-Claude host where the host's skill-invocation mechanism is unavailable, fall back to printing the ordered list of commands to run.
|
|
188
202
|
|
|
189
203
|
**Epic lifecycle.** When the argument names an **epic** (`{specsDir}/{name}/epic-manifest.json` exists), `pause` / `resume` / `abandon` operate on the epic manifest, not a `.pipeline-state.json`:
|
|
190
204
|
|
|
191
205
|
- Set the manifest's top-level `status` (`paused` / `active` / `abandoned`) via the helper's `set-status` mutator — an atomic write that also bumps `updatedAt`:
|
|
192
206
|
```bash
|
|
193
|
-
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)"
|
|
207
|
+
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')"
|
|
194
208
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
195
209
|
python3 "$R/scripts/epic-manifest.py" set-status "{epic}" --status paused --specs-dir "{specsDir}"
|
|
196
210
|
```
|
|
@@ -20,7 +20,7 @@ question in inline prose — every question goes through the host's question mec
|
|
|
20
20
|
|
|
21
21
|
Read and follow `references/shared-conventions.md` for:
|
|
22
22
|
- the **Feature Name Requirement** (applied here to the *epic* name — see below),
|
|
23
|
-
- the **User Input Protocol** (the
|
|
23
|
+
- the **User Input Protocol** (the host's question mechanism guardrail — all questions go through the tool),
|
|
24
24
|
- **Configuration Reading**, and
|
|
25
25
|
- the **Git Commit Protocol**.
|
|
26
26
|
|
|
@@ -39,7 +39,7 @@ checks but still load any on-disk artifacts.
|
|
|
39
39
|
plugin path and the configured specs dir:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
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)"
|
|
42
|
+
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')"
|
|
43
43
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
44
44
|
python3 "$R/scripts/epic-manifest.py" <subcommand> ... --specs-dir "{specsDir}"
|
|
45
45
|
```
|
|
@@ -73,14 +73,14 @@ Resolve the epic subtree path `{specsDir}/{epic}/` and decide which branch to ru
|
|
|
73
73
|
epic, confirm the epic name itself does not collide with any existing feature or epic:
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
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)"
|
|
76
|
+
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')"
|
|
77
77
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
78
78
|
python3 "$R/scripts/epic-manifest.py" check-name "{epic}" --specs-dir "{specsDir}"
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
- Exit `0` → the name is free; proceed to C1.
|
|
82
82
|
- Exit `1` (`duplicate-name`) → STOP and surface the helper's finding **verbatim**; ask
|
|
83
|
-
|
|
83
|
+
for a different epic name, then re-run check-name.
|
|
84
84
|
- Exit `2` (unsafe name) → STOP and surface the finding; ask for a corrected name.
|
|
85
85
|
|
|
86
86
|
---
|
|
@@ -117,14 +117,14 @@ For **each** proposed feature name, before accepting it into the set, enforce gl
|
|
|
117
117
|
and name safety via the helper:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
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)"
|
|
120
|
+
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')"
|
|
121
121
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
122
122
|
python3 "$R/scripts/epic-manifest.py" check-name "{feature}" --specs-dir "{specsDir}"
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
- Exit `0` → accept the name.
|
|
126
126
|
- Exit `1` (`duplicate-name`) → reject that name; surface the finding verbatim and re-prompt
|
|
127
|
-
|
|
127
|
+
for a different name.
|
|
128
128
|
- Exit `2` (`unsafe-name`) → reject; surface the finding and re-prompt.
|
|
129
129
|
|
|
130
130
|
Never accept a feature name that has not passed `check-name` exit 0.
|
|
@@ -177,7 +177,7 @@ For the *initial* creation write the skill writes the file directly — atomic g
|
|
|
177
177
|
required for in-place mutation, which is the helper mutators' job. Creating the epic dir first creates `{specsDir}/`, so after writing the manifest invoke the **Specs Directory Hygiene** block in `references/shared-conventions.md` (idempotent; stage anything it writes with this stage's commit). Then validate:
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
|
-
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)"
|
|
180
|
+
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')"
|
|
181
181
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
182
182
|
python3 "$R/scripts/epic-manifest.py" validate "{epic}" --specs-dir "{specsDir}" --json
|
|
183
183
|
```
|
|
@@ -248,18 +248,23 @@ now self-contained: manifest + EPIC.md + one subdirectory per member.
|
|
|
248
248
|
captures `epic-manifest.json`, `EPIC.md`, and all member `.pipeline-state.json` files
|
|
249
249
|
atomically.
|
|
250
250
|
- Commit with message `"{commitPrefix}({epic}): create epic with {N} features"`.
|
|
251
|
-
- On success, capture the commit hash
|
|
252
|
-
|
|
251
|
+
- On success, capture the commit hash for the closing message only — the epic manifest has no
|
|
252
|
+
`commitHash` field, so nothing is written back into a committed file and the two-commit step of
|
|
253
|
+
the Git Commit Protocol does not apply here. On failure (pre-commit hook, conflict), report and
|
|
254
|
+
do not mark complete; never use `--amend`/`--no-verify`/`--force`.
|
|
253
255
|
|
|
254
|
-
3. **Closing message.**
|
|
256
|
+
3. **Closing message — the Stage Exit Protocol.** Congratulate the user ("Epic `{epic}` created with {N} features."), then close with the Stage Exit Protocol below (single-sourced in `references/stage-exit-protocol.md`; the epic → first-PRD boundary is a full stage boundary — do not improvise a "Next steps" list). `{first-actionable-feature}` = any feature with empty `dependsOn` (or the first entry of `render-status`'s `actionable` set):
|
|
255
257
|
|
|
256
|
-
|
|
257
|
-
> - `/feature-forge:forge {epic}` to see the epic dashboard
|
|
258
|
-
> - `/feature-forge:forge-verify {epic}` to verify the epic
|
|
259
|
-
> - `/feature-forge:forge-1-prd {first-actionable-feature}` to start the first feature
|
|
258
|
+
**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:
|
|
260
259
|
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
1. **Verify the epic decomposition 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 the host's question mechanism with exactly these three options — but only when the host has a question mechanism **and** the clean-room path is available (the host's subagent mechanism plus a dispatchable `forge-verifier` subagent):
|
|
261
|
+
- **Verify the epic decomposition 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.
|
|
262
|
+
- **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.
|
|
263
|
+
- **Skip for now** — go straight to clear your session / start a fresh session 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.
|
|
264
|
+
|
|
265
|
+
**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 {epic}` 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.
|
|
266
|
+
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.**
|
|
267
|
+
3. **Then run `/feature-forge:forge-1-prd {first-actionable-feature}`** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk.
|
|
263
268
|
|
|
264
269
|
---
|
|
265
270
|
|
|
@@ -16,7 +16,7 @@ question goes through `AskUserQuestion`.
|
|
|
16
16
|
Before offering any edit, validate the existing manifest:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
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)"
|
|
19
|
+
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')"
|
|
20
20
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
21
21
|
python3 "$R/scripts/epic-manifest.py" validate "{epic}" --specs-dir "{specsDir}" --json
|
|
22
22
|
```
|
|
@@ -77,7 +77,7 @@ status is **not** `not-started`, warn the user. Read the **live** status (never
|
|
|
77
77
|
completion in prose):
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
|
-
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)"
|
|
80
|
+
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')"
|
|
81
81
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
82
82
|
python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specsDir}" --json
|
|
83
83
|
```
|
|
@@ -160,8 +160,10 @@ follow the Git Commit Protocol in shared-conventions:
|
|
|
160
160
|
`"forge({epic}): create epic with 4 features"`, `"forge({epic}): add feature api-gateway"`,
|
|
161
161
|
`"forge({epic}): remove feature legacy-session"`, `"forge({epic}): reorder features"`,
|
|
162
162
|
`"forge({epic}): set dependency on token-service"`, or `"forge({epic}): set status paused"`.
|
|
163
|
-
3. On success, capture the commit hash
|
|
164
|
-
|
|
163
|
+
3. On success, capture the commit hash for reporting only — the epic manifest has no `commitHash`
|
|
164
|
+
field, so nothing is written back into a committed file and the two-commit step of the Git Commit
|
|
165
|
+
Protocol does not apply here. On failure (pre-commit hook, conflict), report and do **not** mark
|
|
166
|
+
complete; never use `--amend`/`--no-verify`/`--force`.
|
|
165
167
|
|
|
166
168
|
Because every mutation is committed, the git history of `epic-manifest.json` is the audit trail; no
|
|
167
169
|
separate in-manifest audit log is kept.
|
|
@@ -12,7 +12,7 @@ the write if it would introduce a cycle, dangling ref, duplicate, or schema viol
|
|
|
12
12
|
flag surface (owned by 02 §7):
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
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)"
|
|
15
|
+
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')"
|
|
16
16
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
17
17
|
# Add a feature — seeds EMPTY exposes/consumes; contracts are populated below.
|
|
18
18
|
python3 "$R/scripts/epic-manifest.py" add-feature "{epic}" "{feature}" \
|
|
@@ -110,10 +110,20 @@ Write pipeline state conforming to `references/pipeline-state-schema.json`.
|
|
|
110
110
|
- Record `artifacts`, `completedAt`
|
|
111
111
|
- Set `stages.forge-1-prd.basedOnVersions` to `{}` (no upstream dependencies)
|
|
112
112
|
- Check downstream stages (`forge-2-tech`, `forge-3-specs`, `forge-4-backlog`, `forge-5-loop`, `forge-6-docs`). If any have `basedOnVersions` referencing an older version of `forge-1-prd`, set their status to `stale`.
|
|
113
|
-
2.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
2. **Offer a note — don't force one.** As a statement (not a blocking question), let the user know they can jot anything worth preserving across sessions and you'll store it in the `notes` field. If they volunteer something, store it; otherwise proceed.
|
|
114
|
+
3. If `gitCommitAfterStage` is true, follow the Git Commit Protocol in `references/shared-conventions.md`: stage files (including `{specsDir}/AGENTS.md` / `{specsDir}/CLAUDE.md` if the Specs Directory Hygiene step just wrote them), attempt commit with message `"{commitPrefix}({feature}): complete PRD v{n}"` (marking `stages.forge-1-prd.status` `complete` with `commitHash: null` in that commit), then record the artifact-commit hash via the protocol's two-commit follow-up (never `--amend`) only on success. If commit fails, leave status as `in-progress`.
|
|
115
|
+
4. **Close with the Stage Exit Protocol** (single-sourced in `references/stage-exit-protocol.md`; do not improvise a "Next steps" list):
|
|
116
|
+
|
|
117
|
+
**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:
|
|
118
|
+
|
|
119
|
+
1. **Verify the PRD 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 the host's question mechanism with exactly these three options — but only when the host has a question mechanism **and** the clean-room path is available (the host's subagent mechanism plus a dispatchable `forge-verifier` subagent):
|
|
120
|
+
- **Verify the PRD 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.
|
|
121
|
+
- **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.
|
|
122
|
+
- **Skip for now** — go straight to clear your session / start a fresh session 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.
|
|
123
|
+
|
|
124
|
+
**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}` 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.
|
|
125
|
+
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.**
|
|
126
|
+
3. **Then run `/feature-forge:forge-2-tech {feature}`** in the fresh session — or re-run `/feature-forge:forge` to let the navigator resume from disk.
|
|
117
127
|
|
|
118
128
|
## Gotchas
|
|
119
129
|
|