@garygentry/feature-forge 0.2.3 → 0.2.5

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.
Files changed (159) hide show
  1. package/README.md +6 -1
  2. package/adapters/GENERATION-REPORT.md +5 -1
  3. package/adapters/claude/.feature-forge-bundle.json +1 -1
  4. package/adapters/claude/references/forge-config-schema.json +25 -3
  5. package/adapters/claude/references/pipeline-state-schema.json +3 -2
  6. package/adapters/claude/references/portable-root.md +6 -3
  7. package/adapters/claude/references/process-overview.md +10 -0
  8. package/adapters/claude/references/shared-conventions.md +27 -9
  9. package/adapters/claude/references/stage-exit-protocol.md +206 -0
  10. package/adapters/claude/scripts/epic-manifest.py +10 -0
  11. package/adapters/claude/scripts/forge-bootstrap.py +94 -16
  12. package/adapters/claude/scripts/forge-init.sh +7 -1
  13. package/adapters/claude/scripts/forge-root.sh +20 -1
  14. package/adapters/claude/scripts/forge-session.py +866 -31
  15. package/adapters/claude/skills/forge/SKILL.md +29 -15
  16. package/adapters/claude/skills/forge-0-epic/SKILL.md +19 -24
  17. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +6 -4
  18. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  19. package/adapters/claude/skills/forge-1-prd/SKILL.md +13 -4
  20. package/adapters/claude/skills/forge-2-tech/SKILL.md +13 -3
  21. package/adapters/claude/skills/forge-3-specs/SKILL.md +13 -3
  22. package/adapters/claude/skills/forge-4-backlog/SKILL.md +15 -5
  23. package/adapters/claude/skills/forge-5-loop/SKILL.md +19 -21
  24. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +10 -5
  25. package/adapters/claude/skills/forge-6-docs/SKILL.md +6 -6
  26. package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
  27. package/adapters/claude/skills/forge-fix/SKILL.md +29 -6
  28. package/adapters/claude/skills/forge-guide/SKILL.md +182 -0
  29. package/adapters/claude/skills/forge-init/SKILL.md +29 -1
  30. package/adapters/claude/skills/forge-verify/SKILL.md +46 -15
  31. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
  32. package/adapters/codex/.feature-forge-bundle.json +1 -1
  33. package/adapters/codex/references/forge-config-schema.json +25 -3
  34. package/adapters/codex/references/pipeline-state-schema.json +3 -2
  35. package/adapters/codex/references/portable-root.md +6 -3
  36. package/adapters/codex/references/process-overview.md +10 -0
  37. package/adapters/codex/references/shared-conventions.md +27 -9
  38. package/adapters/codex/references/stage-exit-protocol.md +206 -0
  39. package/adapters/codex/scripts/epic-manifest.py +10 -0
  40. package/adapters/codex/scripts/forge-bootstrap.py +94 -16
  41. package/adapters/codex/scripts/forge-init.sh +7 -1
  42. package/adapters/codex/scripts/forge-root.sh +20 -1
  43. package/adapters/codex/scripts/forge-session.py +866 -31
  44. package/adapters/codex/skills/forge/SKILL.md +34 -20
  45. package/adapters/codex/skills/forge-0-epic/SKILL.md +20 -25
  46. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +6 -4
  47. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  48. package/adapters/codex/skills/forge-1-prd/SKILL.md +13 -4
  49. package/adapters/codex/skills/forge-2-tech/SKILL.md +14 -4
  50. package/adapters/codex/skills/forge-3-specs/SKILL.md +13 -3
  51. package/adapters/codex/skills/forge-4-backlog/SKILL.md +15 -5
  52. package/adapters/codex/skills/forge-5-loop/SKILL.md +21 -23
  53. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +10 -5
  54. package/adapters/codex/skills/forge-6-docs/SKILL.md +6 -6
  55. package/adapters/codex/skills/forge-bootstrap/SKILL.md +4 -4
  56. package/adapters/codex/skills/forge-fix/SKILL.md +29 -6
  57. package/adapters/codex/skills/forge-guide/SKILL.md +191 -0
  58. package/adapters/codex/skills/forge-init/SKILL.md +29 -1
  59. package/adapters/codex/skills/forge-verify/SKILL.md +45 -14
  60. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
  61. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  62. package/adapters/copilot/references/forge-config-schema.json +25 -3
  63. package/adapters/copilot/references/pipeline-state-schema.json +3 -2
  64. package/adapters/copilot/references/portable-root.md +6 -3
  65. package/adapters/copilot/references/process-overview.md +10 -0
  66. package/adapters/copilot/references/shared-conventions.md +27 -9
  67. package/adapters/copilot/references/stage-exit-protocol.md +206 -0
  68. package/adapters/copilot/scripts/epic-manifest.py +10 -0
  69. package/adapters/copilot/scripts/forge-bootstrap.py +94 -16
  70. package/adapters/copilot/scripts/forge-init.sh +7 -1
  71. package/adapters/copilot/scripts/forge-root.sh +20 -1
  72. package/adapters/copilot/scripts/forge-session.py +866 -31
  73. package/adapters/copilot/skills/forge/forge.md +34 -20
  74. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +20 -25
  75. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +6 -4
  76. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  77. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +13 -4
  78. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +14 -4
  79. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +13 -3
  80. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +15 -5
  81. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +21 -23
  82. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +10 -5
  83. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +6 -6
  84. package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +4 -4
  85. package/adapters/copilot/skills/forge-fix/forge-fix.md +29 -6
  86. package/adapters/copilot/skills/forge-guide/forge-guide.md +191 -0
  87. package/adapters/copilot/skills/forge-init/forge-init.md +29 -1
  88. package/adapters/copilot/skills/forge-verify/forge-verify.md +45 -14
  89. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
  90. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  91. package/adapters/cursor/references/forge-config-schema.json +25 -3
  92. package/adapters/cursor/references/pipeline-state-schema.json +3 -2
  93. package/adapters/cursor/references/portable-root.md +6 -3
  94. package/adapters/cursor/references/process-overview.md +10 -0
  95. package/adapters/cursor/references/shared-conventions.md +27 -9
  96. package/adapters/cursor/references/stage-exit-protocol.md +206 -0
  97. package/adapters/cursor/scripts/epic-manifest.py +10 -0
  98. package/adapters/cursor/scripts/forge-bootstrap.py +94 -16
  99. package/adapters/cursor/scripts/forge-init.sh +7 -1
  100. package/adapters/cursor/scripts/forge-root.sh +20 -1
  101. package/adapters/cursor/scripts/forge-session.py +866 -31
  102. package/adapters/cursor/skills/forge/forge.mdc +34 -20
  103. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +20 -25
  104. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +6 -4
  105. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  106. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +13 -4
  107. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +14 -4
  108. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +13 -3
  109. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +15 -5
  110. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +21 -23
  111. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +10 -5
  112. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +6 -6
  113. package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +4 -4
  114. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +29 -6
  115. package/adapters/cursor/skills/forge-guide/forge-guide.mdc +192 -0
  116. package/adapters/cursor/skills/forge-init/forge-init.mdc +29 -1
  117. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +45 -14
  118. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
  119. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  120. package/adapters/gemini/gemini-extension.json +5 -1
  121. package/adapters/gemini/references/forge-config-schema.json +25 -3
  122. package/adapters/gemini/references/pipeline-state-schema.json +3 -2
  123. package/adapters/gemini/references/portable-root.md +6 -3
  124. package/adapters/gemini/references/process-overview.md +10 -0
  125. package/adapters/gemini/references/shared-conventions.md +27 -9
  126. package/adapters/gemini/references/stage-exit-protocol.md +206 -0
  127. package/adapters/gemini/scripts/epic-manifest.py +10 -0
  128. package/adapters/gemini/scripts/forge-bootstrap.py +94 -16
  129. package/adapters/gemini/scripts/forge-init.sh +7 -1
  130. package/adapters/gemini/scripts/forge-root.sh +20 -1
  131. package/adapters/gemini/scripts/forge-session.py +866 -31
  132. package/adapters/gemini/skills/forge/forge.md +34 -20
  133. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +20 -25
  134. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +6 -4
  135. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  136. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +13 -4
  137. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +14 -4
  138. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +13 -3
  139. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +15 -5
  140. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +21 -23
  141. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +10 -5
  142. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +6 -6
  143. package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +4 -4
  144. package/adapters/gemini/skills/forge-fix/forge-fix.md +29 -6
  145. package/adapters/gemini/skills/forge-guide/forge-guide.md +191 -0
  146. package/adapters/gemini/skills/forge-init/forge-init.md +29 -1
  147. package/adapters/gemini/skills/forge-verify/forge-verify.md +45 -14
  148. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
  149. package/dist/apply.js +34 -8
  150. package/dist/cli.js +40 -4
  151. package/dist/fsutil.d.ts +0 -12
  152. package/dist/fsutil.js +10 -1
  153. package/dist/manifest.d.ts +1 -1
  154. package/dist/plan.js +22 -2
  155. package/dist/rauf.d.ts +4 -4
  156. package/dist/rauf.js +3 -3
  157. package/dist/report.js +1 -1
  158. package/dist/types.d.ts +1 -1
  159. package/package.json +1 -1
@@ -26,6 +26,8 @@ import argparse
26
26
  import json
27
27
  import os
28
28
  import re
29
+ import shlex
30
+ import shutil
29
31
  import subprocess
30
32
  import sys
31
33
  import tempfile
@@ -281,6 +283,32 @@ def _json_text(obj: object) -> str:
281
283
  return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
282
284
 
283
285
 
286
+ def contained_path(base: Path, *parts: str) -> Path:
287
+ """Join ``parts`` onto ``base`` and assert the result stays within ``base``.
288
+
289
+ Ports epic-manifest.py's ``contained_path`` guard: canonicalizes (symlink-
290
+ resolves) both ends and verifies containment so no scaffold write or verify
291
+ cwd can escape the target repo — defense in depth behind ``_validate_answers``
292
+ should a crafted member path slip through. Containment violations surface only
293
+ as exit-2 usage errors.
294
+
295
+ Args:
296
+ base: The containing directory (the target repo root), already known to exist.
297
+ *parts: Repo-relative segments to append (member path, artifact rel path).
298
+
299
+ Returns:
300
+ The resolved, contained absolute path.
301
+
302
+ Raises:
303
+ UsageError: If the resolved path escapes ``base`` (exit 2).
304
+ """
305
+ base_real = base.resolve()
306
+ resolved = (base_real / Path(*parts)).resolve()
307
+ if resolved != base_real and base_real not in resolved.parents:
308
+ raise UsageError(f"path escapes target dir: {os.path.join(*parts)}")
309
+ return resolved
310
+
311
+
284
312
  def _atomic_write_text(path: Path, text: str) -> None:
285
313
  """Write ``text`` to ``path`` atomically via a same-dir temp file + os.replace.
286
314
 
@@ -381,7 +409,7 @@ def run(
381
409
 
382
410
 
383
411
  # --------------------------------------------------------------------------- #
384
- # Subcommand stubs (filled by later backlog items 003/006/008/009/010)
412
+ # Subcommands: check / scaffold / verify / commit / status (02 §8.2)
385
413
  # --------------------------------------------------------------------------- #
386
414
 
387
415
 
@@ -476,6 +504,7 @@ def _write_artifact(
476
504
  """
477
505
  if rel_path in sentinel["artifactsWritten"]:
478
506
  return
507
+ contained_path(target, rel_path) # never write outside the target repo (exit 2)
479
508
  dest = target / rel_path
480
509
  if dest.exists():
481
510
  return
@@ -529,7 +558,7 @@ def _resolve_commands(member: Member) -> tuple[str, str]:
529
558
 
530
559
 
531
560
  def write_config(answers: Answers, target: Path, sentinel: Sentinel) -> None:
532
- """Write forge.config.json equivalent to forge-init's output (02 §4.3, 00 §7)."""
561
+ """Write forge.config.json == forge-init's field set + loopRunner (02 §4.3, 00 §7)."""
533
562
  config: dict = {
534
563
  "specsDir": "./specs",
535
564
  "docsDir": "./docs/architecture",
@@ -540,6 +569,14 @@ def write_config(answers: Answers, target: Path, sentinel: Sentinel) -> None:
540
569
  "typeCheckCommand": None,
541
570
  "testCommand": None,
542
571
  "loopIterationMultiplier": 1.5,
572
+ # Navigator keys — kept in lockstep with forge-init.sh's emitted field set
573
+ # (00 §7, REQ-CFG-02) so a bootstrapped config == forge-init's + loopRunner.
574
+ "autoInvokeNextStage": True,
575
+ "contextWindowTokens": None,
576
+ "contextWarnThreshold": 0.7,
577
+ "autoVerify": False,
578
+ "autoVerifyStages": {},
579
+ "autoFix": False,
543
580
  "loopRunner": {"name": "rauf", "bin": "rauf"},
544
581
  }
545
582
  if answers["layout"] == "single":
@@ -697,9 +734,9 @@ def scaffold(target: Path, answers: Answers) -> list[str]:
697
734
  def toolchain_present(required: list[str]) -> bool:
698
735
  """Return True iff every required tool is on PATH (REQ-LIFE-03).
699
736
 
700
- Probes each binary with ``command -v`` via the run wrapper (a shell builtin,
701
- invoked through ``sh -c``). A single missing tool yields False, driving the
702
- distinct missing-toolchain outcome (exit 2, 00 §9): the skill then offers
737
+ Probes each binary with :func:`shutil.which` (a pure PATH lookup, no shell
738
+ spawned). A single missing tool yields False, driving the distinct
739
+ missing-toolchain outcome (exit 2, 00 §9): the skill then offers
703
740
  scaffold-anyway-unverified vs abort and marks the baseline unverified
704
741
  (REQ-LIFE-04). Bootstrap NEVER installs a toolchain (tech-spec §9).
705
742
 
@@ -708,17 +745,10 @@ def toolchain_present(required: list[str]) -> bool:
708
745
  already {pm}-substituted.
709
746
 
710
747
  Returns:
711
- True iff ``command -v`` succeeds for every entry.
748
+ True iff every entry resolves on PATH.
712
749
  """
713
750
  for tool in required:
714
- try:
715
- proc = run(["sh", "-c", f"command -v {tool}"], cwd=Path.cwd(), check=False)
716
- except UsageError:
717
- # The probe itself could not be launched (e.g. an empty PATH leaves no
718
- # `sh`): treat that as the tool being absent, the missing-toolchain
719
- # outcome, never an internal error (REQ-LIFE-03/04).
720
- return False
721
- if proc.returncode != 0:
751
+ if shutil.which(tool) is None:
722
752
  return False
723
753
  return True
724
754
 
@@ -763,9 +793,11 @@ def verify(target: Path, answers: Answers) -> VerifyResult:
763
793
  test: list[CommandOutcome] = []
764
794
  for member in answers["members"]:
765
795
  lint_cmd, test_cmd = _resolve_commands(member)
766
- cwd = target / member["path"]
796
+ cwd = contained_path(target, member["path"]) # never run outside the target
767
797
  for bucket, cmd in ((lint, lint_cmd), (test, test_cmd)):
768
- proc = run(["sh", "-c", cmd], cwd=cwd, check=False)
798
+ # STACK_COMMANDS templates with an allow-listed {pm} (see _validate_answers)
799
+ # — split into an argv token list and run without a shell (no `sh -c`).
800
+ proc = run(shlex.split(cmd), cwd=cwd, check=False)
769
801
  bucket.append(
770
802
  {"command": cmd, "ok": proc.returncode == 0, "member": member["path"]}
771
803
  )
@@ -867,9 +899,55 @@ def _parse_answers(raw: str) -> Answers:
867
899
  raise UsageError(f"malformed --answers JSON: {exc}")
868
900
  if not isinstance(parsed, dict):
869
901
  raise UsageError("--answers must be a JSON object")
902
+ _validate_answers(parsed)
870
903
  return parsed
871
904
 
872
905
 
906
+ def _validate_answers(answers: Answers) -> None:
907
+ """Reject non-allowlisted stack/packageManager and unsafe member paths (exit 2).
908
+
909
+ Runs before any ``{pm}`` substitution, ``STACK_COMMANDS[...]`` subscript, or
910
+ artifact write so a malformed ``--answers`` payload fails with a stderr
911
+ diagnostic, never a KeyError traceback or a path escape. Mirrors the
912
+ input-validation rigor epic-manifest.py applies via ``assert_safe_name`` /
913
+ ``contained_path``:
914
+
915
+ - ``stack`` must be a known :data:`Stack` (a ``STACK_COMMANDS`` key).
916
+ - ``packageManager`` must be in ``PACKAGE_MANAGERS[stack]`` for a stack that
917
+ has a choice; stacks with no choice (go/rust/generic) must leave it ``None``.
918
+ - ``path`` must be a repo-relative string — no absolute path, no ``..``
919
+ component, no backslash — so it can never resolve outside the target.
920
+ """
921
+ members = answers.get("members")
922
+ if not isinstance(members, list) or not members:
923
+ raise UsageError("--answers must include a non-empty members[] array")
924
+ for member in members:
925
+ stack = member.get("stack")
926
+ if stack not in STACK_COMMANDS:
927
+ raise UsageError(
928
+ f"unknown stack {stack!r}: expected one of {sorted(STACK_COMMANDS)}"
929
+ )
930
+ pm = member.get("packageManager")
931
+ choices = PACKAGE_MANAGERS.get(stack)
932
+ if choices is not None:
933
+ if pm not in choices:
934
+ raise UsageError(
935
+ f"invalid packageManager {pm!r} for stack {stack!r}: "
936
+ f"expected one of {choices}"
937
+ )
938
+ elif pm is not None:
939
+ raise UsageError(
940
+ f"stack {stack!r} takes no packageManager, got {pm!r}"
941
+ )
942
+ path = member.get("path")
943
+ if not isinstance(path, str) or not path:
944
+ raise UsageError(f"member path must be a non-empty string, got {path!r}")
945
+ if os.path.isabs(path) or "\\" in path or ".." in Path(path).parts:
946
+ raise UsageError(
947
+ f"unsafe member path {path!r}: must be repo-relative with no '..'"
948
+ )
949
+
950
+
873
951
  def _dispatch(args: argparse.Namespace, target: Path) -> int:
874
952
  """Route a parsed command to its handler, translating outcomes into exit codes.
875
953
 
@@ -24,7 +24,10 @@ cat > "$CONFIG_FILE" << 'EOF'
24
24
  "loopIterationMultiplier": 1.5,
25
25
  "autoInvokeNextStage": true,
26
26
  "contextWindowTokens": null,
27
- "contextWarnThreshold": 0.7
27
+ "contextWarnThreshold": 0.7,
28
+ "autoVerify": false,
29
+ "autoVerifyStages": {},
30
+ "autoFix": false
28
31
  }
29
32
  EOF
30
33
 
@@ -43,6 +46,9 @@ echo " loopIterationMultiplier: 1.5 (multiplier for loop iterations)"
43
46
  echo " autoInvokeNextStage: true (navigator auto-starts the next stage after you confirm)"
44
47
  echo " contextWindowTokens: null (infer; set to 1000000 on a 1M-context model)"
45
48
  echo " contextWarnThreshold: 0.7 (suggest a clean session past this fraction of the window)"
49
+ echo " autoVerify: false (set true to run forge-verify automatically after each stage)"
50
+ echo " autoVerifyStages: {} (per-stage overrides for autoVerify)"
51
+ echo " autoFix: false (set true to chain forge-fix after an auto-verify finds issues)"
46
52
  echo ""
47
53
  echo "The loop runner defaults to rauf. To target a different ralph-style runner,"
48
54
  echo "add a \"loopRunner\" block (see references/forge-config-schema.json)."
@@ -31,14 +31,33 @@ if is_root "$root"; then
31
31
  exit 0
32
32
  fi
33
33
 
34
+ # ── Step 2a: Claude marketplace-cache installs — ~/.claude/plugins/cache/<mp>/<plugin>/<ver>/.
35
+ # This is where Claude Code actually installs marketplace plugins (three segments below
36
+ # plugins/, so the single-star plugins/*/feature-forge glob below can never match it). Version
37
+ # dirs can coexist after upgrades, so probe newest plugin.json first — a stale version must
38
+ # never shadow the current install. Deliberately ordered BEFORE the plugins/* glob: that glob
39
+ # can match the marketplace *clone* (~/.claude/plugins/marketplaces/<mp>/ when the marketplace
40
+ # repo root is itself a plugin root), which may sit at a different commit than the installed
41
+ # skills — the versioned cache install must always win to prevent that version skew.
42
+ while IFS= read -r manifest; do
43
+ candidate="${manifest%/.claude-plugin/plugin.json}"
44
+ if is_root "$candidate"; then
45
+ printf '%s\n' "$candidate"
46
+ exit 0
47
+ fi
48
+ done < <(ls -t "$HOME"/.claude/plugins/cache/*/feature-forge/*/.claude-plugin/plugin.json 2>/dev/null || true)
49
+
34
50
  # ── Step 2: candidate-root probe (authoritative multi-agent root list; extend here first). ─
35
51
  # Globs that match nothing expand to themselves; the is_root test rejects such literals. Covers
36
52
  # every supported agent's install destination under BOTH global ($HOME) and project ($PWD) scope,
37
53
  # matching the installer's per-agent layout: claude .claude/skills, codex .agents/skills, copilot
38
- # .github/feature-forge, cursor .cursor/rules, gemini .gemini/extensions.
54
+ # .github/feature-forge, cursor .cursor/rules, gemini .gemini/extensions. The cache glob repeats
55
+ # step 2a's path for a cache install that carries only the neutral bundle sentinel (no
56
+ # plugin.json for ls -t to key on).
39
57
  for candidate in \
40
58
  "$HOME/.claude/skills/feature-forge" \
41
59
  "$PWD/.claude/skills/feature-forge" \
60
+ "$HOME"/.claude/plugins/cache/*/feature-forge/* \
42
61
  "$HOME"/.claude/plugins/*/feature-forge \
43
62
  "$HOME/.agents/skills/feature-forge" \
44
63
  "$PWD/.agents/skills/feature-forge" \