@garygentry/feature-forge 0.2.6 → 0.2.7
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/portable-root.md +18 -9
- package/adapters/claude/references/shared-conventions.md +21 -4
- package/adapters/claude/references/stage-exit-protocol.md +1 -1
- package/adapters/claude/references/vendor-construct-inventory.md +1 -0
- package/adapters/claude/scripts/forge-session.py +248 -5
- package/adapters/claude/skills/forge/SKILL.md +9 -9
- package/adapters/claude/skills/forge-0-epic/SKILL.md +5 -5
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/claude/skills/forge-1-prd/SKILL.md +1 -1
- package/adapters/claude/skills/forge-2-tech/SKILL.md +1 -1
- package/adapters/claude/skills/forge-3-specs/SKILL.md +1 -1
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +1 -1
- package/adapters/claude/skills/forge-5-loop/SKILL.md +2 -2
- package/adapters/claude/skills/forge-6-docs/SKILL.md +1 -1
- package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
- package/adapters/claude/skills/forge-init/SKILL.md +1 -1
- package/adapters/claude/skills/forge-verify/SKILL.md +1 -1
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/codex/.feature-forge-bundle.json +1 -1
- package/adapters/codex/references/portable-root.md +18 -9
- package/adapters/codex/references/shared-conventions.md +21 -4
- package/adapters/codex/references/stage-exit-protocol.md +1 -1
- package/adapters/codex/references/vendor-construct-inventory.md +1 -0
- package/adapters/codex/scripts/forge-session.py +248 -5
- package/adapters/codex/skills/forge/SKILL.md +9 -9
- package/adapters/codex/skills/forge-0-epic/SKILL.md +5 -5
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/codex/skills/forge-1-prd/SKILL.md +1 -1
- package/adapters/codex/skills/forge-2-tech/SKILL.md +1 -1
- package/adapters/codex/skills/forge-3-specs/SKILL.md +1 -1
- package/adapters/codex/skills/forge-4-backlog/SKILL.md +1 -1
- package/adapters/codex/skills/forge-5-loop/SKILL.md +2 -2
- package/adapters/codex/skills/forge-6-docs/SKILL.md +1 -1
- package/adapters/codex/skills/forge-bootstrap/SKILL.md +4 -4
- package/adapters/codex/skills/forge-init/SKILL.md +1 -1
- package/adapters/codex/skills/forge-verify/SKILL.md +1 -1
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/copilot/.feature-forge-bundle.json +1 -1
- package/adapters/copilot/references/portable-root.md +18 -9
- package/adapters/copilot/references/shared-conventions.md +21 -4
- package/adapters/copilot/references/stage-exit-protocol.md +1 -1
- package/adapters/copilot/references/vendor-construct-inventory.md +1 -0
- package/adapters/copilot/scripts/forge-session.py +248 -5
- package/adapters/copilot/skills/forge/forge.md +9 -9
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +5 -5
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +1 -1
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +1 -1
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +1 -1
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +1 -1
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +2 -2
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +1 -1
- package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +4 -4
- package/adapters/copilot/skills/forge-init/forge-init.md +1 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +1 -1
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/cursor/.feature-forge-bundle.json +1 -1
- package/adapters/cursor/references/portable-root.md +18 -9
- package/adapters/cursor/references/shared-conventions.md +21 -4
- package/adapters/cursor/references/stage-exit-protocol.md +1 -1
- package/adapters/cursor/references/vendor-construct-inventory.md +1 -0
- package/adapters/cursor/scripts/forge-session.py +248 -5
- package/adapters/cursor/skills/forge/forge.mdc +9 -9
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +5 -5
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +1 -1
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +1 -1
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +1 -1
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +1 -1
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +2 -2
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +1 -1
- package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +4 -4
- package/adapters/cursor/skills/forge-init/forge-init.mdc +1 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +1 -1
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/gemini/.feature-forge-bundle.json +1 -1
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/portable-root.md +18 -9
- package/adapters/gemini/references/shared-conventions.md +21 -4
- package/adapters/gemini/references/stage-exit-protocol.md +1 -1
- package/adapters/gemini/references/vendor-construct-inventory.md +1 -0
- package/adapters/gemini/scripts/forge-session.py +248 -5
- package/adapters/gemini/skills/forge/forge.md +9 -9
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +5 -5
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +1 -1
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +1 -1
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +1 -1
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +1 -1
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +2 -2
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +1 -1
- package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +4 -4
- package/adapters/gemini/skills/forge-init/forge-init.md +1 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +1 -1
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,9 @@ root navigator:
|
|
|
8
8
|
python3 forge-session.py context-usage [--config FILE] [--window N] \
|
|
9
9
|
[--threshold F] [--json]
|
|
10
10
|
python3 forge-session.py doctor [--specs-dir DIR] [--config FILE] [--json]
|
|
11
|
-
python3 forge-session.py discover-feature NAME [--specs-dir DIR] [--json]
|
|
11
|
+
python3 forge-session.py discover-feature [NAME | --all] [--specs-dir DIR] [--json]
|
|
12
|
+
python3 forge-session.py reconcile-branch --feature F [--specs-dir DIR] \
|
|
13
|
+
[--config FILE] [--epic E] [--json]
|
|
12
14
|
python3 forge-session.py stage-exit --feature F --stage S [--specs-dir DIR] \
|
|
13
15
|
[--config FILE] [--epic E] [--next-feature N] [--host claude|generic] [--json]
|
|
14
16
|
|
|
@@ -135,6 +137,7 @@ class FeatureRow(TypedDict):
|
|
|
135
137
|
verifyState: str
|
|
136
138
|
autoVerify: bool
|
|
137
139
|
autoFix: bool
|
|
140
|
+
verifyGate: str
|
|
138
141
|
|
|
139
142
|
|
|
140
143
|
class UsageError(Exception):
|
|
@@ -357,6 +360,16 @@ def build_rows(specs_dir: Path, config: dict | None = None) -> list[FeatureRow]:
|
|
|
357
360
|
"verifyState": vlabel,
|
|
358
361
|
"autoVerify": effective_auto_verify,
|
|
359
362
|
"autoFix": global_auto_fix and effective_auto_verify,
|
|
363
|
+
# Single resolved verify-gate classification (5b — one exit computation,
|
|
364
|
+
# mirroring stage-exit's `verifyGate`): the navigator reads this instead of
|
|
365
|
+
# re-deriving from verifyPending + autoVerify in prose. `auto` = the §2b
|
|
366
|
+
# catch-up runs it unattended; `standard` = the §3 gate (degrades to
|
|
367
|
+
# manual-print on a non-Claude host); `none` = nothing outstanding.
|
|
368
|
+
"verifyGate": (
|
|
369
|
+
"none" if not verify_pending
|
|
370
|
+
else "auto" if effective_auto_verify
|
|
371
|
+
else "standard"
|
|
372
|
+
),
|
|
360
373
|
})
|
|
361
374
|
# Sort by updatedAt desc; rows without a parseable timestamp sort last.
|
|
362
375
|
rows.sort(
|
|
@@ -659,11 +672,18 @@ def doctor_report(specs_dir: Path, config_path: Path) -> dict:
|
|
|
659
672
|
# --show-current (not rev-parse HEAD) so an unborn branch (fresh repo,
|
|
660
673
|
# no commits yet) still reports its name instead of failing.
|
|
661
674
|
current_branch = _git_output(["branch", "--show-current"])
|
|
675
|
+
default_branch = _default_branch()
|
|
662
676
|
rows = build_rows(specs_dir, config)
|
|
663
677
|
features = []
|
|
664
678
|
for row in rows:
|
|
665
679
|
backlog = _backlog_path(config, row["name"], row["epic"], specs_dir)
|
|
666
680
|
state_branch = row["branch"]
|
|
681
|
+
mismatch = bool(state_branch and current_branch and state_branch != current_branch)
|
|
682
|
+
# Classify a mismatch: on a topic branch it is adoptable (imposed/session-branch
|
|
683
|
+
# drift, Chunk 6); on the default branch it is real drift-back, only a warning.
|
|
684
|
+
branch_reconcile = None
|
|
685
|
+
if mismatch:
|
|
686
|
+
branch_reconcile = "warn-drift" if current_branch == default_branch else "adopt-current"
|
|
667
687
|
features.append({
|
|
668
688
|
"name": row["name"],
|
|
669
689
|
"epic": row["epic"],
|
|
@@ -676,6 +696,7 @@ def doctor_report(specs_dir: Path, config_path: Path) -> dict:
|
|
|
676
696
|
if state_branch and current_branch
|
|
677
697
|
else None
|
|
678
698
|
),
|
|
699
|
+
"branchReconcile": branch_reconcile,
|
|
679
700
|
"backlogPath": str(backlog),
|
|
680
701
|
"backlogExists": backlog.is_file(),
|
|
681
702
|
})
|
|
@@ -720,7 +741,12 @@ def _print_doctor(report: dict) -> None:
|
|
|
720
741
|
label = feat["name"] + (f" [{feat['epic']}]" if feat["epic"] else "")
|
|
721
742
|
branch = feat["stateBranch"] or "?"
|
|
722
743
|
if feat["branchMatchesState"] is False:
|
|
723
|
-
|
|
744
|
+
if feat.get("branchReconcile") == "adopt-current":
|
|
745
|
+
branch += " (MISMATCH — reconcile: adopt current branch)"
|
|
746
|
+
elif feat.get("branchReconcile") == "warn-drift":
|
|
747
|
+
branch += " (MISMATCH — on default branch; create a topic branch)"
|
|
748
|
+
else:
|
|
749
|
+
branch += " (MISMATCH vs current)"
|
|
724
750
|
backlog = "exists" if feat["backlogExists"] else "MISSING"
|
|
725
751
|
print(
|
|
726
752
|
f" - {label}: stage={feat['currentStage']} "
|
|
@@ -937,6 +963,193 @@ def _print_discover(payload: dict) -> None:
|
|
|
937
963
|
print(f" switch: {cand['switchCommand']}")
|
|
938
964
|
|
|
939
965
|
|
|
966
|
+
def _all_state_paths_in_ref(ref: str, specs_rel: str) -> list[tuple[str, str]]:
|
|
967
|
+
"""Every feature-shaped ``.pipeline-state.json`` in one ref as ``(path, feature)``.
|
|
968
|
+
|
|
969
|
+
The ``--all`` counterpart to ``_state_paths_in_ref``: same flat/nested bound
|
|
970
|
+
(``{specsDir}/{name}/…`` or ``{specsDir}/{epic}/{name}/…``) but for every
|
|
971
|
+
feature, not one named one.
|
|
972
|
+
"""
|
|
973
|
+
listing = _git_output(["ls-tree", "-r", "--name-only", ref, "--", specs_rel])
|
|
974
|
+
if not listing:
|
|
975
|
+
return []
|
|
976
|
+
hits: list[tuple[str, str]] = []
|
|
977
|
+
prefix = specs_rel + "/"
|
|
978
|
+
for path in listing.splitlines():
|
|
979
|
+
if not path.startswith(prefix) or not path.endswith("/" + PIPELINE_STATE_FILENAME):
|
|
980
|
+
continue
|
|
981
|
+
segments = path[len(prefix):].split("/")
|
|
982
|
+
if len(segments) == 2: # [name, state-file] (flat)
|
|
983
|
+
hits.append((path, segments[0]))
|
|
984
|
+
elif len(segments) == 3: # [epic, name, state-file] (nested)
|
|
985
|
+
hits.append((path, segments[1]))
|
|
986
|
+
return hits
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def discover_all(specs_dir: str) -> dict:
|
|
990
|
+
"""Discover EVERY feature's pipeline state across all branches (read-only, Chunk 5c).
|
|
991
|
+
|
|
992
|
+
The empty-dashboard counterpart to ``discover-feature <name>``: enumerates every
|
|
993
|
+
feature-shaped state across local heads + remote-tracking refs and groups the
|
|
994
|
+
candidates by feature, so a fresh clone / default-branch session can see the whole
|
|
995
|
+
branch-scattered pipeline set instead of nothing. Never mutates anything.
|
|
996
|
+
"""
|
|
997
|
+
if _git_output(["rev-parse", "--git-dir"]) is None:
|
|
998
|
+
return {"gitRepo": False, "currentBranch": None, "features": []}
|
|
999
|
+
current_branch = _git_output(["branch", "--show-current"])
|
|
1000
|
+
specs_rel = _specs_rel(specs_dir)
|
|
1001
|
+
refs = [(ref, date, False) for ref, date in _list_refs("refs/heads")]
|
|
1002
|
+
refs += [(ref, date, True) for ref, date in _list_refs("refs/remotes")]
|
|
1003
|
+
|
|
1004
|
+
by_feature: dict[str, list[dict]] = {}
|
|
1005
|
+
for ref, commit_date, is_remote in refs:
|
|
1006
|
+
branch = ref.split("/", 1)[1] if is_remote else ref
|
|
1007
|
+
if is_remote and (not branch or branch == "HEAD"):
|
|
1008
|
+
continue
|
|
1009
|
+
for path, feature in _all_state_paths_in_ref(ref, specs_rel):
|
|
1010
|
+
seen = by_feature.setdefault(feature, [])
|
|
1011
|
+
if any(c["branch"] == branch for c in seen):
|
|
1012
|
+
continue # a local head already yielded this branch's state
|
|
1013
|
+
state = _read_state_at_ref(ref, path)
|
|
1014
|
+
state_branch = state.get("branch")
|
|
1015
|
+
state_branch = state_branch if isinstance(state_branch, str) else None
|
|
1016
|
+
seen.append({
|
|
1017
|
+
"branch": branch,
|
|
1018
|
+
"remoteTracking": is_remote,
|
|
1019
|
+
"path": path,
|
|
1020
|
+
"stateBranch": state_branch,
|
|
1021
|
+
"stateBranchMatches": state_branch == branch,
|
|
1022
|
+
"currentStage": state.get("currentStage"),
|
|
1023
|
+
"pipelineStatus": state.get("pipelineStatus", "active"),
|
|
1024
|
+
"commitDate": commit_date or None,
|
|
1025
|
+
"isCurrentBranch": branch == current_branch,
|
|
1026
|
+
"switchCommand": f"git switch {branch}",
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
def _rank(cand: dict) -> tuple:
|
|
1030
|
+
ts = _parse_ts(cand["commitDate"]) or datetime.min.replace(tzinfo=timezone.utc)
|
|
1031
|
+
return (not cand["stateBranchMatches"], cand["remoteTracking"], -ts.timestamp())
|
|
1032
|
+
|
|
1033
|
+
features = []
|
|
1034
|
+
for feature in sorted(by_feature):
|
|
1035
|
+
cands = sorted(by_feature[feature], key=_rank)
|
|
1036
|
+
features.append({"feature": feature, "candidates": cands})
|
|
1037
|
+
return {"gitRepo": True, "currentBranch": current_branch, "features": features}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _print_discover_all(payload: dict) -> None:
|
|
1041
|
+
"""Human-readable ``discover-feature --all`` report."""
|
|
1042
|
+
if not payload["gitRepo"]:
|
|
1043
|
+
print("discover-feature --all: not a git repository — nothing to scan")
|
|
1044
|
+
return
|
|
1045
|
+
if not payload["features"]:
|
|
1046
|
+
print("discover-feature --all: no pipeline state found on any local or "
|
|
1047
|
+
"remote-tracking branch")
|
|
1048
|
+
return
|
|
1049
|
+
for feat in payload["features"]:
|
|
1050
|
+
print(f"{feat['feature']}:")
|
|
1051
|
+
for cand in feat["candidates"]:
|
|
1052
|
+
marks = []
|
|
1053
|
+
if cand["isCurrentBranch"]:
|
|
1054
|
+
marks.append("current branch")
|
|
1055
|
+
if cand["remoteTracking"]:
|
|
1056
|
+
marks.append("remote-tracking")
|
|
1057
|
+
if not cand["stateBranchMatches"] and cand["stateBranch"]:
|
|
1058
|
+
marks.append(f"state records branch {cand['stateBranch']}")
|
|
1059
|
+
suffix = f" ({'; '.join(marks)})" if marks else ""
|
|
1060
|
+
print(f" {cand['branch']}: stage={cand['currentStage'] or '?'} "
|
|
1061
|
+
f"status={cand['pipelineStatus']}{suffix}")
|
|
1062
|
+
if not cand["isCurrentBranch"]:
|
|
1063
|
+
print(f" switch: {cand['switchCommand']}")
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
# --------------------------------------------------------------------------- #
|
|
1067
|
+
# Branch reconciliation (Chunk 6) — imposed/session-branch drift
|
|
1068
|
+
# --------------------------------------------------------------------------- #
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def _default_branch() -> str | None:
|
|
1072
|
+
"""The repo's default branch: origin/HEAD target, else `main`/`master` if present."""
|
|
1073
|
+
ref = _git_output(["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"])
|
|
1074
|
+
if ref:
|
|
1075
|
+
return ref.rsplit("/", 1)[-1]
|
|
1076
|
+
for cand in ("main", "master"):
|
|
1077
|
+
if _git_output(["rev-parse", "--verify", "--quiet", f"refs/heads/{cand}"]) is not None:
|
|
1078
|
+
return cand
|
|
1079
|
+
return None
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def reconcile_branch(
|
|
1083
|
+
name: str, specs_dir: Path, config_path: Path, epic: str | None = None
|
|
1084
|
+
) -> dict:
|
|
1085
|
+
"""Decide whether a feature's recorded ``branch`` should adopt the current branch.
|
|
1086
|
+
|
|
1087
|
+
Read-only: it emits a decision; the caller performs any state write. A hosted
|
|
1088
|
+
environment (Claude.ai remote, cloud agents) imposes an arbitrary session branch
|
|
1089
|
+
that Branch Setup silently records; when the user moves to the intended branch the
|
|
1090
|
+
recorded ``branch`` goes stale and every branch-aware mechanism keys off it. This
|
|
1091
|
+
reconciler treats *where the state actually resolves* as the source of truth, with a
|
|
1092
|
+
default-branch guardrail so genuine drift-back-to-default is still surfaced, not
|
|
1093
|
+
silently adopted.
|
|
1094
|
+
"""
|
|
1095
|
+
if _git_output(["rev-parse", "--git-dir"]) is None:
|
|
1096
|
+
return {"feature": name, "gitRepo": False, "reconcile": False,
|
|
1097
|
+
"action": "none", "reason": "not a git repository"}
|
|
1098
|
+
current = _git_output(["branch", "--show-current"])
|
|
1099
|
+
default = _default_branch()
|
|
1100
|
+
config = _load_config(config_path)
|
|
1101
|
+
row = next(
|
|
1102
|
+
(r for r in build_rows(specs_dir, config)
|
|
1103
|
+
if r["name"] == name and (epic is None or r["epic"] == epic)),
|
|
1104
|
+
None,
|
|
1105
|
+
)
|
|
1106
|
+
state_path = None
|
|
1107
|
+
if row is not None:
|
|
1108
|
+
parent = specs_dir / row["epic"] / name if row["epic"] else specs_dir / name
|
|
1109
|
+
state_path = str(parent / PIPELINE_STATE_FILENAME)
|
|
1110
|
+
base = {
|
|
1111
|
+
"feature": name,
|
|
1112
|
+
"gitRepo": True,
|
|
1113
|
+
"currentBranch": current,
|
|
1114
|
+
"defaultBranch": default,
|
|
1115
|
+
"stateBranch": row["branch"] if row else None,
|
|
1116
|
+
"resolvesOnCurrentBranch": row is not None,
|
|
1117
|
+
"statePath": state_path,
|
|
1118
|
+
"newBranch": None,
|
|
1119
|
+
}
|
|
1120
|
+
if current is None:
|
|
1121
|
+
return {**base, "reconcile": False, "action": "none",
|
|
1122
|
+
"reason": "no current branch (detached HEAD or unborn branch)"}
|
|
1123
|
+
if row is None:
|
|
1124
|
+
return {**base, "reconcile": False, "action": "not-resolved",
|
|
1125
|
+
"reason": "feature state does not resolve on the current branch — "
|
|
1126
|
+
"use discover-feature to locate it"}
|
|
1127
|
+
state_branch = base["stateBranch"]
|
|
1128
|
+
if state_branch == current:
|
|
1129
|
+
return {**base, "reconcile": False, "action": "none",
|
|
1130
|
+
"reason": "recorded branch already matches the current branch"}
|
|
1131
|
+
if current == default:
|
|
1132
|
+
return {**base, "reconcile": False, "action": "warn-drift",
|
|
1133
|
+
"reason": f"on the default branch ({default}); recording it would commit "
|
|
1134
|
+
"here — create/switch to a topic branch instead of reconciling"}
|
|
1135
|
+
detail = (f"recorded branch {state_branch!r} differs from the current topic branch"
|
|
1136
|
+
if state_branch else "no branch recorded")
|
|
1137
|
+
return {**base, "reconcile": True, "action": "adopt-current", "newBranch": current,
|
|
1138
|
+
"reason": f"{detail}; the feature state resolves here, so adopt the current branch"}
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _print_reconcile(payload: dict) -> None:
|
|
1142
|
+
"""Human-readable reconcile-branch report."""
|
|
1143
|
+
if not payload["gitRepo"]:
|
|
1144
|
+
print(f"reconcile-branch {payload['feature']}: not a git repository")
|
|
1145
|
+
return
|
|
1146
|
+
print(f"reconcile-branch {payload['feature']}: {payload['action']} — {payload['reason']}")
|
|
1147
|
+
print(f" current={payload['currentBranch']} recorded={payload['stateBranch'] or '(none)'} "
|
|
1148
|
+
f"default={payload['defaultBranch']}")
|
|
1149
|
+
if payload["reconcile"]:
|
|
1150
|
+
print(f" → write state branch := {payload['newBranch']} ({payload['statePath']})")
|
|
1151
|
+
|
|
1152
|
+
|
|
940
1153
|
# --------------------------------------------------------------------------- #
|
|
941
1154
|
# Scripted Stage Exit
|
|
942
1155
|
# --------------------------------------------------------------------------- #
|
|
@@ -1230,10 +1443,23 @@ def main() -> int:
|
|
|
1230
1443
|
p_disc = sub.add_parser(
|
|
1231
1444
|
"discover-feature", help="Find a feature's pipeline state across all branches"
|
|
1232
1445
|
)
|
|
1233
|
-
p_disc.add_argument("name",
|
|
1446
|
+
p_disc.add_argument("name", nargs="?", default=None,
|
|
1447
|
+
help="Feature name to discover (omit with --all)")
|
|
1448
|
+
p_disc.add_argument("--all", action="store_true", dest="discover_all",
|
|
1449
|
+
help="Discover every feature across all branches (empty-dashboard)")
|
|
1234
1450
|
p_disc.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
1235
1451
|
p_disc.add_argument("--json", action="store_true", dest="json_output")
|
|
1236
1452
|
|
|
1453
|
+
p_recon = sub.add_parser(
|
|
1454
|
+
"reconcile-branch",
|
|
1455
|
+
help="Decide whether a feature's recorded branch should adopt the current branch",
|
|
1456
|
+
)
|
|
1457
|
+
p_recon.add_argument("--feature", required=True, help="Feature name")
|
|
1458
|
+
p_recon.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
1459
|
+
p_recon.add_argument("--config", default="./forge.config.json", help="forge.config.json path")
|
|
1460
|
+
p_recon.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
1461
|
+
p_recon.add_argument("--json", action="store_true", dest="json_output")
|
|
1462
|
+
|
|
1237
1463
|
p_exit = sub.add_parser(
|
|
1238
1464
|
"stage-exit", help="Emit the Scripted Stage Exit directives + NEXT-STEPS block"
|
|
1239
1465
|
)
|
|
@@ -1290,11 +1516,28 @@ def main() -> int:
|
|
|
1290
1516
|
return 0
|
|
1291
1517
|
|
|
1292
1518
|
if args.cmd == "discover-feature":
|
|
1293
|
-
|
|
1519
|
+
if args.discover_all:
|
|
1520
|
+
payload = discover_all(args.specs_dir)
|
|
1521
|
+
printer = _print_discover_all
|
|
1522
|
+
elif args.name:
|
|
1523
|
+
payload = discover_feature(args.name, args.specs_dir)
|
|
1524
|
+
printer = _print_discover
|
|
1525
|
+
else:
|
|
1526
|
+
parser.error("discover-feature requires a NAME or --all")
|
|
1527
|
+
if args.json_output:
|
|
1528
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
1529
|
+
else:
|
|
1530
|
+
printer(payload)
|
|
1531
|
+
return 0
|
|
1532
|
+
|
|
1533
|
+
if args.cmd == "reconcile-branch":
|
|
1534
|
+
payload = reconcile_branch(
|
|
1535
|
+
args.feature, Path(args.specs_dir), Path(args.config), args.epic
|
|
1536
|
+
)
|
|
1294
1537
|
if args.json_output:
|
|
1295
1538
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
1296
1539
|
else:
|
|
1297
|
-
|
|
1540
|
+
_print_reconcile(payload)
|
|
1298
1541
|
return 0
|
|
1299
1542
|
|
|
1300
1543
|
if args.cmd == "stage-exit":
|
|
@@ -29,7 +29,7 @@ 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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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
|
```
|
|
@@ -37,14 +37,14 @@ python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specs
|
|
|
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
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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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`, `verifyCommand`, `verifyStage`, `verifyState` (`fresh`/`stale`/`failing`/`never`/`none`), `autoVerify` (the effective per-stage setting),
|
|
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), `autoFix` (the single source of stage order), and `verifyGate` (`none`/`auto`/`standard` — the single resolved verify-gate, computed once by the ranker; read it rather than re-deriving from `verifyPending`+`autoVerify`). 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
|
-
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>`."
|
|
47
|
+
If no epics and no standalone features exist **on the current branch**, do not conclude the pipeline is empty yet — the state may live on topic branches (a fresh clone / default-branch session sees nothing on disk). Run `discover-feature --all` (via the standard prelude: `python3 "$R/scripts/forge-session.py" discover-feature --all --specs-dir "{specsDir}" --json`). If it surfaces features, list them with their branches and offer to switch (explicit accept + clean tree only, per the Feature Directory Resolution block), then render that feature's dashboard. Only when `--all` also returns nothing, 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>`." Never fabricate a dashboard from memory of earlier sessions.
|
|
48
48
|
|
|
49
49
|
The feature name must be a single kebab-case token. If the user provides multiple words (e.g., "user auth flow"), convert to kebab-case: `user-auth-flow`.
|
|
50
50
|
|
|
@@ -89,11 +89,11 @@ Use these status indicators:
|
|
|
89
89
|
|
|
90
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`, `verifyCommand`, `verifyStage`, `verifyState`, `autoVerify`, 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`, `autoFix`, and `verifyGate` (the resolved gate: `none` → no verify action; `auto` → the §2b catch-up; `standard` → the §3 gate). 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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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
|
```
|
|
@@ -110,7 +110,7 @@ python3 "$R/scripts/forge-session.py" context-usage --json
|
|
|
110
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
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
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:
|
|
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`). **Present this gate exactly once and act only on the option the user chooses — never also narrate the not-taken branch.** Concretely: do not print the "start in a clean session" recommendation as prose and *then* auto-invoke the next stage in the same session (or the reverse) — the host's question mechanism answer is the single decision, and only the chosen path's action follows. 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
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
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
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.
|
|
@@ -133,7 +133,7 @@ This applies whether the feature was named explicitly (`/feature-forge:forge {fe
|
|
|
133
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:
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
137
137
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
138
138
|
python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specsDir}" --json
|
|
139
139
|
```
|
|
@@ -204,7 +204,7 @@ Support these sub-commands for pipeline lifecycle management:
|
|
|
204
204
|
|
|
205
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`:
|
|
206
206
|
```bash
|
|
207
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
208
208
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
209
209
|
python3 "$R/scripts/epic-manifest.py" set-status "{epic}" --status paused --specs-dir "{specsDir}"
|
|
210
210
|
```
|
|
@@ -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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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,7 +73,7 @@ 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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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
|
```
|
|
@@ -117,7 +117,7 @@ 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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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
|
```
|
|
@@ -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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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
|
```
|
|
@@ -249,7 +249,7 @@ now self-contained: manifest + EPIC.md + one subdirectory per member.
|
|
|
249
249
|
**Close this stage with the Scripted Stage Exit** (contract: `references/stage-exit-protocol.md`; do not improvise a "Next steps" list). Run:
|
|
250
250
|
|
|
251
251
|
```bash
|
|
252
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
252
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
253
253
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
254
254
|
python3 "$R/scripts/forge-session.py" stage-exit --feature "{epic}" --stage forge-0-epic --next-feature "{first-actionable-feature}" --specs-dir "{specsDir}" --host generic
|
|
255
255
|
```
|
|
@@ -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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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
|
```
|
|
@@ -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="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${CLAUDE_PLUGIN_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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}" \
|
|
@@ -117,7 +117,7 @@ Write pipeline state conforming to `references/pipeline-state-schema.json`.
|
|
|
117
117
|
**Close this stage with the Scripted Stage Exit** (contract: `references/stage-exit-protocol.md`; do not improvise a "Next steps" list). Run:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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 "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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/forge-session.py" stage-exit --feature "{feature}" --stage forge-1-prd --specs-dir "{specsDir}" --host generic
|
|
123
123
|
```
|
|
@@ -194,7 +194,7 @@ Write pipeline state conforming to `references/pipeline-state-schema.json`.
|
|
|
194
194
|
**Close this stage with the Scripted Stage Exit** (contract: `references/stage-exit-protocol.md`; do not improvise a "Next steps" list). Run:
|
|
195
195
|
|
|
196
196
|
```bash
|
|
197
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
197
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
198
198
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
199
199
|
python3 "$R/scripts/forge-session.py" stage-exit --feature "{feature}" --stage forge-2-tech --specs-dir "{specsDir}" --host generic
|
|
200
200
|
```
|
|
@@ -148,7 +148,7 @@ Write pipeline state conforming to `references/pipeline-state-schema.json`.
|
|
|
148
148
|
**Close this stage with the Scripted Stage Exit** (contract: `references/stage-exit-protocol.md`; do not improvise a "Next steps" list). Run:
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
151
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
152
152
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
153
153
|
python3 "$R/scripts/forge-session.py" stage-exit --feature "{feature}" --stage forge-3-specs --specs-dir "{specsDir}" --host generic
|
|
154
154
|
```
|
|
@@ -142,7 +142,7 @@ Write pipeline state conforming to `references/pipeline-state-schema.json`. Foll
|
|
|
142
142
|
**Close this stage with the Scripted Stage Exit** (contract: `references/stage-exit-protocol.md`; do not improvise a "Next steps" list). Run:
|
|
143
143
|
|
|
144
144
|
```bash
|
|
145
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
145
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
146
146
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
147
147
|
python3 "$R/scripts/forge-session.py" stage-exit --feature "{feature}" --stage forge-4-backlog --specs-dir "{specsDir}" --host generic
|
|
148
148
|
```
|
|
@@ -61,7 +61,7 @@ Read the resolved feature's `.pipeline-state.json`. **If it has no `epic` key, s
|
|
|
61
61
|
1. Run `render-status "{epic}" --specs-dir "{specsDir}" --json` via the helper:
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
64
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
65
65
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
66
66
|
python3 "$R/scripts/epic-manifest.py" \
|
|
67
67
|
render-status "{epic}" --specs-dir "{specsDir}" --json
|
|
@@ -114,7 +114,7 @@ Verify the file exists on disk. If not, STOP and tell the user: "No backlog.json
|
|
|
114
114
|
|
|
115
115
|
### 1f. Branch Pre-flight (if using git)
|
|
116
116
|
|
|
117
|
-
The runner commits each
|
|
117
|
+
The runner commits each item onto the current branch. Skip if not a git repo or `branchPerFeature` is false. Otherwise run the **Branch Reconciliation** block in `references/shared-conventions.md` (it runs `reconcile-branch` and, on `warn-drift` — you are on the default branch — strongly recommends creating `{branchPrefix}{feature}` via the host's question mechanism before the loop commits; on `adopt-current` it updates the recorded branch to the current one, never pushing you back to a stale/imposed branch). Never hard-stop.
|
|
118
118
|
|
|
119
119
|
## Step 2: Construct the Loop Command
|
|
120
120
|
|
|
@@ -44,7 +44,7 @@ Check `.pipeline-state.json` for `stages.forge-verify-impl`. If it is **absent**
|
|
|
44
44
|
If the resolved feature has an `epic` back-pointer in its `.pipeline-state.json`, run:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
47
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
48
48
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
49
49
|
python3 "$R/scripts/epic-manifest.py" render-status "{epic}" --specs-dir "{specsDir}" --json
|
|
50
50
|
```
|
|
@@ -59,7 +59,7 @@ Every bash invocation begins with the byte-identical portable-root prelude, then
|
|
|
59
59
|
helper. Pass `--specs-dir ./specs` (the default) so the gate allow-lists the specs directory.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
62
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
63
63
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
64
64
|
python3 "$R/scripts/forge-bootstrap.py" check "<target-dir>" --json --specs-dir ./specs
|
|
65
65
|
```
|
|
@@ -136,7 +136,7 @@ when running under a Claude host (e.g. the host's question mechanism is availabl
|
|
|
136
136
|
`CLAUDE.md` only when `host == "claude"`.
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/feature-forge/* "$HOME"/.claude/plugins/*/feature-forge "$HOME"/.agents/skills/feature-forge ./.agents/skills/feature-forge; do [ -x "$d/scripts/forge-root.sh" ] && exec "$d/scripts/forge-root.sh"; done')"
|
|
139
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
140
140
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
141
141
|
python3 "$R/scripts/forge-bootstrap.py" scaffold "<target-dir>" --json --answers '<Answers JSON>'
|
|
142
142
|
```
|
|
@@ -144,7 +144,7 @@ python3 "$R/scripts/forge-bootstrap.py" scaffold "<target-dir>" --json --answers
|
|
|
144
144
|
### Step 5 — verify
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
147
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
148
148
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
149
149
|
python3 "$R/scripts/forge-bootstrap.py" verify "<target-dir>" --json --answers '<Answers JSON>'
|
|
150
150
|
```
|
|
@@ -170,7 +170,7 @@ and removes the sentinel before staging so it never enters history. Read
|
|
|
170
170
|
leave the sentinel in-progress (resumable) — do **not** declare success.
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
|
-
R="$(bash -c 'for d in "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
173
|
+
R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-forge "$HOME"/.claude/plugins/cache/*/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')"
|
|
174
174
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
175
175
|
python3 "$R/scripts/forge-bootstrap.py" commit "<target-dir>" --json --answers '<Answers JSON>' [--stage-only]
|
|
176
176
|
```
|