@garygentry/feature-forge 0.2.2 → 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.
Files changed (154) hide show
  1. package/README.md +6 -1
  2. package/adapters/GENERATION-REPORT.md +5 -1
  3. package/adapters/claude/references/forge-config-schema.json +43 -4
  4. package/adapters/claude/references/pipeline-state-schema.json +3 -2
  5. package/adapters/claude/references/portable-root.md +2 -2
  6. package/adapters/claude/references/process-overview.md +10 -0
  7. package/adapters/claude/references/shared-conventions.md +17 -9
  8. package/adapters/claude/references/stage-exit-protocol.md +99 -0
  9. package/adapters/claude/scripts/epic-manifest.py +10 -0
  10. package/adapters/claude/scripts/forge-bootstrap.py +94 -16
  11. package/adapters/claude/scripts/forge-init.sh +13 -1
  12. package/adapters/claude/scripts/forge-session.py +636 -0
  13. package/adapters/claude/skills/forge/SKILL.md +60 -4
  14. package/adapters/claude/skills/forge-0-epic/SKILL.md +20 -15
  15. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +6 -4
  16. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  17. package/adapters/claude/skills/forge-1-prd/SKILL.md +14 -4
  18. package/adapters/claude/skills/forge-2-tech/SKILL.md +14 -3
  19. package/adapters/claude/skills/forge-3-specs/SKILL.md +14 -3
  20. package/adapters/claude/skills/forge-4-backlog/SKILL.md +16 -5
  21. package/adapters/claude/skills/forge-5-loop/SKILL.md +20 -21
  22. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +10 -5
  23. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +41 -15
  24. package/adapters/claude/skills/forge-6-docs/SKILL.md +6 -6
  25. package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
  26. package/adapters/claude/skills/forge-fix/SKILL.md +27 -6
  27. package/adapters/claude/skills/forge-guide/SKILL.md +179 -0
  28. package/adapters/claude/skills/forge-init/SKILL.md +31 -1
  29. package/adapters/claude/skills/forge-verify/SKILL.md +46 -15
  30. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
  31. package/adapters/codex/references/forge-config-schema.json +43 -4
  32. package/adapters/codex/references/pipeline-state-schema.json +3 -2
  33. package/adapters/codex/references/portable-root.md +2 -2
  34. package/adapters/codex/references/process-overview.md +10 -0
  35. package/adapters/codex/references/shared-conventions.md +17 -9
  36. package/adapters/codex/references/stage-exit-protocol.md +99 -0
  37. package/adapters/codex/scripts/epic-manifest.py +10 -0
  38. package/adapters/codex/scripts/forge-bootstrap.py +94 -16
  39. package/adapters/codex/scripts/forge-init.sh +13 -1
  40. package/adapters/codex/scripts/forge-session.py +636 -0
  41. package/adapters/codex/skills/forge/SKILL.md +60 -4
  42. package/adapters/codex/skills/forge-0-epic/SKILL.md +21 -16
  43. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +6 -4
  44. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  45. package/adapters/codex/skills/forge-1-prd/SKILL.md +14 -4
  46. package/adapters/codex/skills/forge-2-tech/SKILL.md +15 -4
  47. package/adapters/codex/skills/forge-3-specs/SKILL.md +14 -3
  48. package/adapters/codex/skills/forge-4-backlog/SKILL.md +16 -5
  49. package/adapters/codex/skills/forge-5-loop/SKILL.md +22 -23
  50. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +10 -5
  51. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +41 -15
  52. package/adapters/codex/skills/forge-6-docs/SKILL.md +6 -6
  53. package/adapters/codex/skills/forge-bootstrap/SKILL.md +4 -4
  54. package/adapters/codex/skills/forge-fix/SKILL.md +27 -6
  55. package/adapters/codex/skills/forge-guide/SKILL.md +188 -0
  56. package/adapters/codex/skills/forge-init/SKILL.md +31 -1
  57. package/adapters/codex/skills/forge-verify/SKILL.md +45 -14
  58. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
  59. package/adapters/copilot/references/forge-config-schema.json +43 -4
  60. package/adapters/copilot/references/pipeline-state-schema.json +3 -2
  61. package/adapters/copilot/references/portable-root.md +2 -2
  62. package/adapters/copilot/references/process-overview.md +10 -0
  63. package/adapters/copilot/references/shared-conventions.md +17 -9
  64. package/adapters/copilot/references/stage-exit-protocol.md +99 -0
  65. package/adapters/copilot/scripts/epic-manifest.py +10 -0
  66. package/adapters/copilot/scripts/forge-bootstrap.py +94 -16
  67. package/adapters/copilot/scripts/forge-init.sh +13 -1
  68. package/adapters/copilot/scripts/forge-session.py +636 -0
  69. package/adapters/copilot/skills/forge/forge.md +60 -4
  70. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +21 -16
  71. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +6 -4
  72. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  73. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +14 -4
  74. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +15 -4
  75. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +14 -3
  76. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +16 -5
  77. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +22 -23
  78. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +10 -5
  79. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +41 -15
  80. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +6 -6
  81. package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +4 -4
  82. package/adapters/copilot/skills/forge-fix/forge-fix.md +27 -6
  83. package/adapters/copilot/skills/forge-guide/forge-guide.md +188 -0
  84. package/adapters/copilot/skills/forge-init/forge-init.md +31 -1
  85. package/adapters/copilot/skills/forge-verify/forge-verify.md +45 -14
  86. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
  87. package/adapters/cursor/references/forge-config-schema.json +43 -4
  88. package/adapters/cursor/references/pipeline-state-schema.json +3 -2
  89. package/adapters/cursor/references/portable-root.md +2 -2
  90. package/adapters/cursor/references/process-overview.md +10 -0
  91. package/adapters/cursor/references/shared-conventions.md +17 -9
  92. package/adapters/cursor/references/stage-exit-protocol.md +99 -0
  93. package/adapters/cursor/scripts/epic-manifest.py +10 -0
  94. package/adapters/cursor/scripts/forge-bootstrap.py +94 -16
  95. package/adapters/cursor/scripts/forge-init.sh +13 -1
  96. package/adapters/cursor/scripts/forge-session.py +636 -0
  97. package/adapters/cursor/skills/forge/forge.mdc +60 -4
  98. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +21 -16
  99. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +6 -4
  100. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  101. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +14 -4
  102. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +15 -4
  103. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +14 -3
  104. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +16 -5
  105. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +22 -23
  106. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +10 -5
  107. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +41 -15
  108. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +6 -6
  109. package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +4 -4
  110. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +27 -6
  111. package/adapters/cursor/skills/forge-guide/forge-guide.mdc +189 -0
  112. package/adapters/cursor/skills/forge-init/forge-init.mdc +31 -1
  113. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +45 -14
  114. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
  115. package/adapters/gemini/gemini-extension.json +4 -0
  116. package/adapters/gemini/references/forge-config-schema.json +43 -4
  117. package/adapters/gemini/references/pipeline-state-schema.json +3 -2
  118. package/adapters/gemini/references/portable-root.md +2 -2
  119. package/adapters/gemini/references/process-overview.md +10 -0
  120. package/adapters/gemini/references/shared-conventions.md +17 -9
  121. package/adapters/gemini/references/stage-exit-protocol.md +99 -0
  122. package/adapters/gemini/scripts/epic-manifest.py +10 -0
  123. package/adapters/gemini/scripts/forge-bootstrap.py +94 -16
  124. package/adapters/gemini/scripts/forge-init.sh +13 -1
  125. package/adapters/gemini/scripts/forge-session.py +636 -0
  126. package/adapters/gemini/skills/forge/forge.md +60 -4
  127. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +21 -16
  128. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +6 -4
  129. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  130. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +14 -4
  131. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +15 -4
  132. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +14 -3
  133. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +16 -5
  134. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +22 -23
  135. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +10 -5
  136. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +41 -15
  137. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +6 -6
  138. package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +4 -4
  139. package/adapters/gemini/skills/forge-fix/forge-fix.md +27 -6
  140. package/adapters/gemini/skills/forge-guide/forge-guide.md +188 -0
  141. package/adapters/gemini/skills/forge-init/forge-init.md +31 -1
  142. package/adapters/gemini/skills/forge-verify/forge-verify.md +45 -14
  143. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
  144. package/dist/apply.js +34 -8
  145. package/dist/cli.js +40 -4
  146. package/dist/fsutil.d.ts +0 -12
  147. package/dist/fsutil.js +10 -1
  148. package/dist/manifest.d.ts +1 -1
  149. package/dist/plan.js +22 -2
  150. package/dist/rauf.d.ts +4 -4
  151. package/dist/rauf.js +3 -3
  152. package/dist/report.js +1 -1
  153. package/dist/types.d.ts +1 -1
  154. package/package.json +1 -1
package/dist/apply.js CHANGED
@@ -91,8 +91,9 @@ async function applyCopyInstall(planned, ctx) {
91
91
  if (!placementResult.ok)
92
92
  return fail(ctx, planned, placementResult.error);
93
93
  // No-op short-circuit (REQ-IDEM-01): every action — primary AND placement — unchanged ⇒ zero
94
- // writes, manifest untouched.
95
- if (allUnchanged(planned)) {
94
+ // writes, manifest untouched — UNLESS manifest metadata (raufPin) drifted, which must persist
95
+ // even with no file change (F1) so report / on-disk manifest / a later `list` agree.
96
+ if (allUnchanged(planned) && !manifestNeedsRewrite(ctx)) {
96
97
  return success(ctx, planned);
97
98
  }
98
99
  const manifest = buildManifest({
@@ -113,6 +114,18 @@ async function applyCopyInstall(planned, ctx) {
113
114
  return fail(ctx, planned, wrote.error);
114
115
  return success(ctx, planned);
115
116
  }
117
+ /**
118
+ * True when manifest metadata diverges from the prior manifest, so the manifest must be rewritten
119
+ * even when every file action is "unchanged" (F1). Today the only such field is `raufPin`; a null
120
+ * prior manifest also counts (there is nothing recorded yet). `updatedAt` is deliberately NOT a
121
+ * trigger — it is a consequence of writing, so keying on it would defeat the idempotency short-circuit.
122
+ */
123
+ function manifestNeedsRewrite(ctx) {
124
+ const prior = ctx.priorManifest;
125
+ if (prior === null)
126
+ return true;
127
+ return prior.raufPin !== ctx.raufPin;
128
+ }
116
129
  /** True iff every planned action — primary files and all placement files — is "unchanged". */
117
130
  function allUnchanged(planned) {
118
131
  return (planned.files.every((f) => f.action === "unchanged") &&
@@ -164,8 +177,8 @@ async function applySymlinkInstall(planned, ctx) {
164
177
  const primary = planned.files;
165
178
  const primaryUntouched = primary.every((f) => f.action === "unchanged") ||
166
179
  primary.every((f) => f.action === "skip-modified");
167
- // Nothing changed anywhere ⇒ zero writes, manifest untouched.
168
- if (allUnchanged(planned)) {
180
+ // Nothing changed anywhere ⇒ zero writes, manifest untouched — unless raufPin drifted (F1).
181
+ if (allUnchanged(planned) && !manifestNeedsRewrite(ctx)) {
169
182
  return success(ctx, planned);
170
183
  }
171
184
  // The recorded mode/link reflect the primary namespace dir: only (re)link when it actually changed
@@ -285,12 +298,25 @@ async function applyMirror(pl, ctx, source, prior) {
285
298
  case "skip-modified": {
286
299
  // Carry the prior record forward; if none exists (e.g. a v1→v2 manifest migration where the
287
300
  // file is already on disk), reconstruct it by hashing the destination so the inventory stays
288
- // faithful rather than silently dropping an unrecorded-but-present file.
301
+ // faithful rather than silently dropping an unrecorded-but-present file. Guard the hash: a
302
+ // TOCTOU vanish (file removed after planning) must yield an err Result, never throw ENOENT
303
+ // out of apply (REQ-OBS-03 — a throw would abort every sibling agent at the CLI boundary).
289
304
  const p = priorByPath.get(fa.relpath);
290
- if (p !== undefined)
305
+ if (p !== undefined) {
291
306
  inventory.push(p);
292
- else
293
- inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
307
+ }
308
+ else {
309
+ try {
310
+ inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
311
+ }
312
+ catch {
313
+ return err({
314
+ code: "UNEXPECTED",
315
+ agent: ctx.agent,
316
+ message: `mirror file "${fa.relpath}" vanished before it could be recorded (${destAbs})`,
317
+ });
318
+ }
319
+ }
294
320
  break;
295
321
  }
296
322
  }
package/dist/cli.js CHANGED
@@ -23,7 +23,8 @@ import { plan, resolveMode } from "./plan.js"; // 04
23
23
  import { apply } from "./apply.js"; // 04
24
24
  import { manifestPath, readManifest, planUninstall } from "./manifest.js"; // 05
25
25
  import { preflightRauf, RAUF_PIN } from "./rauf.js"; // 06
26
- import { sha256File } from "./hash.js"; // 03 (list destination-drift hashing)
26
+ import { sha256File, sha256String } from "./hash.js"; // 03 (list destination-drift hashing)
27
+ import { extractManagedRegion } from "./placements.js"; // A4b managed-block drift
27
28
  import { renderReport } from "./report.js"; // 09
28
29
  /** Canonical subcommand table (REQ-DIST-03, §1.2). */
29
30
  export const SUBCOMMANDS = [
@@ -424,12 +425,18 @@ function listOneAgent(agent, detection, flags, scope, env) {
424
425
  /**
425
426
  * Return `"true"` if any manifest-recorded file's on-disk bytes differ from its recorded sha256
426
427
  * (a locally-modified destination, REQ-SAFE-03), `"false"` if every recorded file matches, or
427
- * `"n/a(symlink)"` for a symlink-mode install (no per-file sha256 to compare). A missing recorded
428
- * file also counts as drift. Pure read of the manifest's per-file sha256 against a fresh local
429
- * hash — no network, no source bundle needed (REQ-PERF-01). Hash errors are swallowed as drift
428
+ * `"n/a(symlink)"` for a symlink-mode primary install (no per-file sha256 to compare). A missing
429
+ * recorded file also counts as drift. Pure read of the manifest's per-file sha256 against a fresh
430
+ * local hash — no network, no source bundle needed (REQ-PERF-01). Hash errors are swallowed as drift
430
431
  * (an unreadable recorded file is itself a deviation from the clean install).
432
+ *
433
+ * Secondary placements (A4b) are checked in EVERY mode — they are always copy-written with a recorded
434
+ * hash even when the primary namespace is a symlink, so ignoring them (the prior behavior) left codex
435
+ * `.toml` mirrors and copilot managed blocks unwatched for drift.
431
436
  */
432
437
  function detectDestinationDrift(manifest) {
438
+ if (detectPlacementDrift(manifest))
439
+ return "true";
433
440
  if (manifest.mode === "symlink")
434
441
  return "n/a(symlink)";
435
442
  for (const f of manifest.files) {
@@ -445,6 +452,35 @@ function detectDestinationDrift(manifest) {
445
452
  }
446
453
  return "false";
447
454
  }
455
+ /**
456
+ * True iff any secondary placement diverges from its recorded hash. A "mirror" file drifts when its
457
+ * on-disk bytes differ from the recorded sha256 (destination is a DIR, paths relative to it). A
458
+ * "managed-block" drifts when the sentinel region extracted from the (user-owned) target FILE no
459
+ * longer hashes to the recorded region hash — or the region is gone entirely. Read errors count as
460
+ * drift, matching the primary check.
461
+ */
462
+ function detectPlacementDrift(manifest) {
463
+ for (const p of manifest.placements ?? []) {
464
+ for (const f of p.files) {
465
+ if (f.sha256 === undefined)
466
+ continue;
467
+ try {
468
+ if (p.kind === "managed-block") {
469
+ const region = extractManagedRegion(readFileSync(p.destination, "utf8"));
470
+ if (region === null || sha256String(region) !== f.sha256)
471
+ return true;
472
+ }
473
+ else if (sha256File(path.join(p.destination, f.path)) !== f.sha256) {
474
+ return true;
475
+ }
476
+ }
477
+ catch {
478
+ return true; // unreadable/absent recorded placement ⇒ drift
479
+ }
480
+ }
481
+ }
482
+ return false;
483
+ }
448
484
  // ---------------------------------------------------------------------------
449
485
  // 7. helpText and --version (§3.4)
450
486
  // ---------------------------------------------------------------------------
package/dist/fsutil.d.ts CHANGED
@@ -1,16 +1,4 @@
1
1
  import type { Result } from "./types.js";
2
- /**
3
- * Resolve `segs` against `root` and assert the result lies WITHIN `root` (REQ-SEC-02). Returns the
4
- * resolved absolute path on success, or a PATH_ESCAPE InstallerError if a `..` segment or a
5
- * malformed agent id would escape the agent config root. MUST be called before ANY write/delete.
6
- *
7
- * Uses `path.relative` + the `..` prefix test (the robust boundary check — a bare string startsWith
8
- * would false-pass `/root-evil`).
9
- *
10
- * @param root - the containment boundary (the agent config root, e.g. <home>/.claude)
11
- * @param segs - path segments to join under root (destination, then a bundle-relative path)
12
- * @returns ok(absolutePath) if inside; err(PATH_ESCAPE) otherwise.
13
- */
14
2
  export declare function resolveWithin(root: string, ...segs: string[]): Result<string>;
15
3
  /**
16
4
  * Recursively copy `src` → `dest` (copy mode). The CALLER is responsible for having
package/dist/fsutil.js CHANGED
@@ -19,6 +19,12 @@ import { ok, err } from "./types.js";
19
19
  * @param segs - path segments to join under root (destination, then a bundle-relative path)
20
20
  * @returns ok(absolutePath) if inside; err(PATH_ESCAPE) otherwise.
21
21
  */
22
+ /** True iff `child` is strictly within `parent` (path.relative + `..`-prefix test, never a bare
23
+ * `startsWith`). Shares the boundary logic of {@link resolveWithin} for a plain boolean check. */
24
+ function isWithin(parent, child) {
25
+ const rel = path.relative(parent, child);
26
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
27
+ }
22
28
  export function resolveWithin(root, ...segs) {
23
29
  const base = path.resolve(root);
24
30
  const target = path.resolve(base, ...segs);
@@ -125,7 +131,10 @@ export async function removeEmptyDirsWithin(startDir, stopRoot) {
125
131
  return contained;
126
132
  const stop = path.resolve(stopRoot);
127
133
  let cur = contained.value;
128
- while (cur !== stop && cur.startsWith(stop)) {
134
+ // Robust boundary test (path.relative + `..` prefix), not a bare `cur.startsWith(stop)` — the
135
+ // exact anti-pattern this module warns against at the top: `startsWith` false-passes a sibling
136
+ // like `<stop>-evil`. Safe here by the ancestor invariant today, but don't rely on it.
137
+ while (cur !== stop && isWithin(stop, cur)) {
129
138
  let entries;
130
139
  try {
131
140
  entries = await fsp.readdir(cur);
@@ -28,7 +28,7 @@ export interface BuildManifestArgs {
28
28
  readonly skills: readonly string[];
29
29
  /** SHA-256 over the source bundle's canonical (sorted-path) file set — drift anchor (spec 03). */
30
30
  readonly sourceHash: string;
31
- /** Recorded pinned rauf coordinate (e.g. "@garygentry/rauf@0.10.1"); `null` when `--skip-rauf` (spec 06). */
31
+ /** Recorded pinned rauf coordinate (e.g. "@garygentry/rauf@0.12.0"); `null` when `--skip-rauf` (spec 06). */
32
32
  readonly raufPin: string | null;
33
33
  /** Symlink mode only: the source bundle the namespace dir links to (REQ-SAFE-02). */
34
34
  readonly link?: {
package/dist/plan.js CHANGED
@@ -220,8 +220,16 @@ function planSymlink(ctx) {
220
220
  const source = ctx.source;
221
221
  const prior = ctx.priorManifest;
222
222
  const priorExists = prior !== null;
223
- const priorIsLiveSymlinkToSameTarget = priorExists && prior.link?.target === source.root;
224
- const action = priorIsLiveSymlinkToSameTarget
223
+ const manifestSaysLive = priorExists && prior.link?.target === source.root;
224
+ // The manifest can claim a live symlink that no longer exists on disk (deleted, or clobbered by a
225
+ // regular file/dir). Copy mode self-heals via per-file hashing; symlink mode had no on-disk check
226
+ // and would classify a broken link "unchanged" forever. lstat the link and force a relink when the
227
+ // manifest says live but the link is absent/not-a-symlink (F2) — never "skip-modified", which apply
228
+ // treats as untouched and would leave the install broken.
229
+ if (manifestSaysLive && !isLiveSymlink(ctx.destination)) {
230
+ return [{ relpath: ".", action: "create" }];
231
+ }
232
+ const action = manifestSaysLive
225
233
  ? "unchanged"
226
234
  : priorExists
227
235
  ? ctx.force
@@ -230,6 +238,18 @@ function planSymlink(ctx) {
230
238
  : "create";
231
239
  return [{ relpath: ".", action }];
232
240
  }
241
+ /**
242
+ * True iff `absPath` exists and is currently a symbolic link. `lstat`, not `readlink` — we only need
243
+ * the node type to decide whether a manifest-recorded symlink is still live (F2). Absent ⇒ false.
244
+ */
245
+ function isLiveSymlink(absPath) {
246
+ try {
247
+ return fs.lstatSync(absPath).isSymbolicLink();
248
+ }
249
+ catch {
250
+ return false;
251
+ }
252
+ }
233
253
  /** Paths in the prior manifest that the current source no longer contains → `remove` (row 6). */
234
254
  function orphanRemovals(priorFiles, sourceFiles) {
235
255
  const inSource = new Set(sourceFiles.map((f) => f.relpath));
package/dist/rauf.d.ts CHANGED
@@ -19,13 +19,13 @@ import { type Result } from "./types.js";
19
19
  *
20
20
  * Shape: `<name>@<version>` — the SCOPED package `@garygentry/rauf` (the unscoped `rauf` name is
21
21
  * blocked by npm's similarity filter). Advanced on each feature-forge release to a new
22
- * known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.10.1.
22
+ * known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.12.0.
23
23
  *
24
- * rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.10.1` resolves from the npm registry, so the
24
+ * rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.12.0` resolves from the npm registry, so the
25
25
  * preflight below passes by default. (Historically this pin pointed at an unpublished package and
26
26
  * the preflight was a designed-to-fail check — see the `--skip-rauf` escape hatch.)
27
27
  */
28
- export declare const RAUF_PIN = "@garygentry/rauf@0.10.1";
28
+ export declare const RAUF_PIN = "@garygentry/rauf@0.12.0";
29
29
  /**
30
30
  * An injectable, READ-ONLY registry query (D1). Given a coordinate `name@version`, returns the
31
31
  * resolved version string on success, or an `InstallerError` if it is not resolvable.
@@ -37,7 +37,7 @@ export declare const RAUF_PIN = "@garygentry/rauf@0.10.1";
37
37
  * Contract: the query MUST be read-only — it MUST NOT install, MUST NOT mutate global npm
38
38
  * state, and MUST NOT execute rauf. `npm view` satisfies this (it only reads registry metadata).
39
39
  *
40
- * @param coordinate - the `name@version` to resolve, e.g. "@garygentry/rauf@0.10.1"
40
+ * @param coordinate - the `name@version` to resolve, e.g. "@garygentry/rauf@0.12.0"
41
41
  * @returns Result<string> — the resolved version on success; RAUF_UNRESOLVABLE on failure.
42
42
  */
43
43
  export type RegistryQuery = (coordinate: string) => Result<string>;
package/dist/rauf.js CHANGED
@@ -20,13 +20,13 @@ import { err, ok } from "./types.js";
20
20
  *
21
21
  * Shape: `<name>@<version>` — the SCOPED package `@garygentry/rauf` (the unscoped `rauf` name is
22
22
  * blocked by npm's similarity filter). Advanced on each feature-forge release to a new
23
- * known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.10.1.
23
+ * known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.12.0.
24
24
  *
25
- * rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.10.1` resolves from the npm registry, so the
25
+ * rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.12.0` resolves from the npm registry, so the
26
26
  * preflight below passes by default. (Historically this pin pointed at an unpublished package and
27
27
  * the preflight was a designed-to-fail check — see the `--skip-rauf` escape hatch.)
28
28
  */
29
- export const RAUF_PIN = "@garygentry/rauf@0.10.1";
29
+ export const RAUF_PIN = "@garygentry/rauf@0.12.0";
30
30
  /**
31
31
  * Resolvability preflight for the pinned default loop runner (D1; REQ-RAUF-01/02/03, OQ-1).
32
32
  *
package/dist/report.js CHANGED
@@ -126,6 +126,6 @@ const DEFAULT_REMEDY = {
126
126
  LOCALLY_MODIFIED: "re-run with --force to overwrite local changes",
127
127
  WRITE_DENIED: "check write permission to the path, or choose --global vs project scope",
128
128
  PATH_ESCAPE: "report this — a destination resolved outside the agent config dir",
129
- RAUF_UNRESOLVABLE: "the default loop will be unavailable until rauf publishes (see packaging-docs-ci); skills were still installed",
129
+ RAUF_UNRESOLVABLE: "could not resolve the rauf loop runner (a network/registry hiccup or an unmet version pin) — skills were still installed; retry, or install rauf manually",
130
130
  MANIFEST_CORRUPT: "remove the corrupt .feature-forge.<scope>.json and re-run install",
131
131
  };
package/dist/types.d.ts CHANGED
@@ -216,7 +216,7 @@ export interface InstallManifest {
216
216
  readonly featureForgeVersion: string | null;
217
217
  /** SHA-256 over the source bundle's canonical (sorted-path) file set — drift anchor (OQ-4, spec 03). */
218
218
  readonly sourceHash: string;
219
- /** Pinned rauf coordinate recorded at install, e.g. "@garygentry/rauf@0.10.1"; `null` if `--skip-rauf` (spec 06). */
219
+ /** Pinned rauf coordinate recorded at install, e.g. "@garygentry/rauf@0.12.0"; `null` if `--skip-rauf` (spec 06). */
220
220
  readonly raufPin: string | null;
221
221
  /** ISO-8601 timestamps. */
222
222
  readonly installedAt: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garygentry/feature-forge",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Cross-agent installer for the feature-forge skill suite — installs the canonical forge pipeline into Claude, Codex, Copilot, Cursor, or Gemini.",
5
5
  "license": "MIT",
6
6
  "type": "module",