@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.
- package/README.md +6 -1
- package/adapters/GENERATION-REPORT.md +5 -1
- package/adapters/claude/references/forge-config-schema.json +43 -4
- package/adapters/claude/references/pipeline-state-schema.json +3 -2
- package/adapters/claude/references/portable-root.md +2 -2
- package/adapters/claude/references/process-overview.md +10 -0
- package/adapters/claude/references/shared-conventions.md +17 -9
- package/adapters/claude/references/stage-exit-protocol.md +99 -0
- package/adapters/claude/scripts/epic-manifest.py +10 -0
- package/adapters/claude/scripts/forge-bootstrap.py +94 -16
- package/adapters/claude/scripts/forge-init.sh +13 -1
- package/adapters/claude/scripts/forge-session.py +636 -0
- package/adapters/claude/skills/forge/SKILL.md +60 -4
- package/adapters/claude/skills/forge-0-epic/SKILL.md +20 -15
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +6 -4
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/claude/skills/forge-1-prd/SKILL.md +14 -4
- package/adapters/claude/skills/forge-2-tech/SKILL.md +14 -3
- package/adapters/claude/skills/forge-3-specs/SKILL.md +14 -3
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +16 -5
- package/adapters/claude/skills/forge-5-loop/SKILL.md +20 -21
- package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +41 -15
- package/adapters/claude/skills/forge-6-docs/SKILL.md +6 -6
- package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
- package/adapters/claude/skills/forge-fix/SKILL.md +27 -6
- package/adapters/claude/skills/forge-guide/SKILL.md +179 -0
- package/adapters/claude/skills/forge-init/SKILL.md +31 -1
- package/adapters/claude/skills/forge-verify/SKILL.md +46 -15
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/codex/references/forge-config-schema.json +43 -4
- package/adapters/codex/references/pipeline-state-schema.json +3 -2
- package/adapters/codex/references/portable-root.md +2 -2
- package/adapters/codex/references/process-overview.md +10 -0
- package/adapters/codex/references/shared-conventions.md +17 -9
- package/adapters/codex/references/stage-exit-protocol.md +99 -0
- package/adapters/codex/scripts/epic-manifest.py +10 -0
- package/adapters/codex/scripts/forge-bootstrap.py +94 -16
- package/adapters/codex/scripts/forge-init.sh +13 -1
- package/adapters/codex/scripts/forge-session.py +636 -0
- package/adapters/codex/skills/forge/SKILL.md +60 -4
- package/adapters/codex/skills/forge-0-epic/SKILL.md +21 -16
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +6 -4
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/codex/skills/forge-1-prd/SKILL.md +14 -4
- package/adapters/codex/skills/forge-2-tech/SKILL.md +15 -4
- package/adapters/codex/skills/forge-3-specs/SKILL.md +14 -3
- package/adapters/codex/skills/forge-4-backlog/SKILL.md +16 -5
- package/adapters/codex/skills/forge-5-loop/SKILL.md +22 -23
- package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +41 -15
- package/adapters/codex/skills/forge-6-docs/SKILL.md +6 -6
- package/adapters/codex/skills/forge-bootstrap/SKILL.md +4 -4
- package/adapters/codex/skills/forge-fix/SKILL.md +27 -6
- package/adapters/codex/skills/forge-guide/SKILL.md +188 -0
- package/adapters/codex/skills/forge-init/SKILL.md +31 -1
- package/adapters/codex/skills/forge-verify/SKILL.md +45 -14
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/copilot/references/forge-config-schema.json +43 -4
- package/adapters/copilot/references/pipeline-state-schema.json +3 -2
- package/adapters/copilot/references/portable-root.md +2 -2
- package/adapters/copilot/references/process-overview.md +10 -0
- package/adapters/copilot/references/shared-conventions.md +17 -9
- package/adapters/copilot/references/stage-exit-protocol.md +99 -0
- package/adapters/copilot/scripts/epic-manifest.py +10 -0
- package/adapters/copilot/scripts/forge-bootstrap.py +94 -16
- package/adapters/copilot/scripts/forge-init.sh +13 -1
- package/adapters/copilot/scripts/forge-session.py +636 -0
- package/adapters/copilot/skills/forge/forge.md +60 -4
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +21 -16
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +6 -4
- package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +14 -4
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +15 -4
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +14 -3
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +16 -5
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +22 -23
- package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +41 -15
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +6 -6
- package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +4 -4
- package/adapters/copilot/skills/forge-fix/forge-fix.md +27 -6
- package/adapters/copilot/skills/forge-guide/forge-guide.md +188 -0
- package/adapters/copilot/skills/forge-init/forge-init.md +31 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +45 -14
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/cursor/references/forge-config-schema.json +43 -4
- package/adapters/cursor/references/pipeline-state-schema.json +3 -2
- package/adapters/cursor/references/portable-root.md +2 -2
- package/adapters/cursor/references/process-overview.md +10 -0
- package/adapters/cursor/references/shared-conventions.md +17 -9
- package/adapters/cursor/references/stage-exit-protocol.md +99 -0
- package/adapters/cursor/scripts/epic-manifest.py +10 -0
- package/adapters/cursor/scripts/forge-bootstrap.py +94 -16
- package/adapters/cursor/scripts/forge-init.sh +13 -1
- package/adapters/cursor/scripts/forge-session.py +636 -0
- package/adapters/cursor/skills/forge/forge.mdc +60 -4
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +21 -16
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +6 -4
- package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +14 -4
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +15 -4
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +14 -3
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +16 -5
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +22 -23
- package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +41 -15
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +6 -6
- package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +4 -4
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +27 -6
- package/adapters/cursor/skills/forge-guide/forge-guide.mdc +189 -0
- package/adapters/cursor/skills/forge-init/forge-init.mdc +31 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +45 -14
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/gemini/gemini-extension.json +4 -0
- package/adapters/gemini/references/forge-config-schema.json +43 -4
- package/adapters/gemini/references/pipeline-state-schema.json +3 -2
- package/adapters/gemini/references/portable-root.md +2 -2
- package/adapters/gemini/references/process-overview.md +10 -0
- package/adapters/gemini/references/shared-conventions.md +17 -9
- package/adapters/gemini/references/stage-exit-protocol.md +99 -0
- package/adapters/gemini/scripts/epic-manifest.py +10 -0
- package/adapters/gemini/scripts/forge-bootstrap.py +94 -16
- package/adapters/gemini/scripts/forge-init.sh +13 -1
- package/adapters/gemini/scripts/forge-session.py +636 -0
- package/adapters/gemini/skills/forge/forge.md +60 -4
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +21 -16
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +6 -4
- package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +14 -4
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +15 -4
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +14 -3
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +16 -5
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +22 -23
- package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +10 -5
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +41 -15
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +6 -6
- package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +4 -4
- package/adapters/gemini/skills/forge-fix/forge-fix.md +27 -6
- package/adapters/gemini/skills/forge-guide/forge-guide.md +188 -0
- package/adapters/gemini/skills/forge-init/forge-init.md +31 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +45 -14
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
- package/dist/apply.js +34 -8
- package/dist/cli.js +40 -4
- package/dist/fsutil.d.ts +0 -12
- package/dist/fsutil.js +10 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/plan.js +22 -2
- package/dist/rauf.d.ts +4 -4
- package/dist/rauf.js +3 -3
- package/dist/report.js +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
|
428
|
-
* file also counts as drift. Pure read of the manifest's per-file sha256 against a fresh
|
|
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
|
-
|
|
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);
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
224
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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",
|