@garygentry/feature-forge 0.1.5 → 0.2.1
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 +19 -1
- package/adapters/GENERATION-REPORT.md +12 -12
- package/adapters/claude/.feature-forge-bundle.json +6 -0
- package/adapters/claude/references/forge-config-schema.json +2 -2
- package/adapters/claude/references/portable-root.md +8 -5
- package/adapters/claude/references/process-overview.md +1 -1
- package/adapters/claude/references/shared-conventions.md +24 -5
- package/adapters/claude/references/stack-resolution.md +4 -1
- package/adapters/claude/references/stacks/go.md +1 -1
- package/adapters/claude/references/stacks/python.md +1 -1
- package/adapters/claude/references/stacks/rust.md +1 -1
- package/adapters/claude/references/stacks/typescript.md +1 -1
- package/adapters/claude/scripts/epic-manifest.py +1379 -0
- package/adapters/claude/scripts/forge-bootstrap.py +991 -0
- package/adapters/claude/scripts/forge-init.sh +44 -0
- package/adapters/claude/scripts/forge-root.sh +30 -8
- package/adapters/claude/scripts/validate-traceability.py +150 -0
- package/adapters/claude/skills/forge/SKILL.md +5 -5
- package/adapters/claude/skills/forge-0-epic/SKILL.md +6 -10
- 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 +2 -2
- package/adapters/claude/skills/forge-2-tech/SKILL.md +8 -7
- package/adapters/claude/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/claude/skills/forge-3-specs/SKILL.md +1 -1
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +2 -2
- package/adapters/claude/skills/forge-5-loop/SKILL.md +2 -2
- package/adapters/claude/skills/forge-6-docs/SKILL.md +2 -2
- package/adapters/claude/skills/forge-bootstrap/SKILL.md +4 -4
- package/adapters/claude/skills/forge-fix/SKILL.md +1 -1
- package/adapters/claude/skills/forge-init/SKILL.md +1 -1
- package/adapters/claude/skills/forge-verify/SKILL.md +7 -2
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/codex/.feature-forge-bundle.json +6 -0
- package/adapters/codex/agents/{forge-researcher.md → forge-researcher.toml} +4 -4
- package/adapters/codex/agents/{forge-spec-writer.md → forge-spec-writer.toml} +4 -4
- package/adapters/codex/agents/{forge-verifier.md → forge-verifier.toml} +4 -4
- package/adapters/codex/references/forge-config-schema.json +2 -2
- package/adapters/codex/references/portable-root.md +8 -5
- package/adapters/codex/references/process-overview.md +1 -1
- package/adapters/codex/references/shared-conventions.md +24 -5
- package/adapters/codex/references/stack-resolution.md +4 -1
- package/adapters/codex/references/stacks/go.md +1 -1
- package/adapters/codex/references/stacks/python.md +1 -1
- package/adapters/codex/references/stacks/rust.md +1 -1
- package/adapters/codex/references/stacks/typescript.md +1 -1
- package/adapters/codex/scripts/epic-manifest.py +1379 -0
- package/adapters/codex/scripts/forge-bootstrap.py +991 -0
- package/adapters/codex/scripts/forge-init.sh +44 -0
- package/adapters/codex/scripts/forge-root.sh +30 -8
- package/adapters/codex/scripts/validate-traceability.py +150 -0
- package/adapters/codex/skills/forge/{forge.md → SKILL.md} +16 -6
- package/adapters/codex/skills/forge-0-epic/{forge-0-epic.md → SKILL.md} +26 -20
- 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/{forge-1-prd.md → SKILL.md} +18 -8
- package/adapters/codex/skills/forge-2-tech/{forge-2-tech.md → SKILL.md} +26 -15
- package/adapters/codex/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/codex/skills/forge-3-specs/{forge-3-specs.md → SKILL.md} +16 -6
- package/adapters/codex/skills/forge-4-backlog/{forge-4-backlog.md → SKILL.md} +15 -5
- package/adapters/codex/skills/forge-5-loop/{forge-5-loop.md → SKILL.md} +27 -17
- package/adapters/codex/skills/forge-6-docs/{forge-6-docs.md → SKILL.md} +17 -7
- package/adapters/codex/skills/forge-bootstrap/{forge-bootstrap.md → SKILL.md} +17 -7
- package/adapters/codex/skills/forge-fix/{forge-fix.md → SKILL.md} +12 -2
- package/adapters/codex/skills/forge-init/{forge-init.md → SKILL.md} +11 -1
- package/adapters/codex/skills/forge-verify/{forge-verify.md → SKILL.md} +24 -9
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/copilot/.feature-forge-bundle.json +6 -0
- package/adapters/copilot/references/forge-config-schema.json +2 -2
- package/adapters/copilot/references/portable-root.md +8 -5
- package/adapters/copilot/references/process-overview.md +1 -1
- package/adapters/copilot/references/shared-conventions.md +24 -5
- package/adapters/copilot/references/stack-resolution.md +4 -1
- package/adapters/copilot/references/stacks/go.md +1 -1
- package/adapters/copilot/references/stacks/python.md +1 -1
- package/adapters/copilot/references/stacks/rust.md +1 -1
- package/adapters/copilot/references/stacks/typescript.md +1 -1
- package/adapters/copilot/scripts/epic-manifest.py +1379 -0
- package/adapters/copilot/scripts/forge-bootstrap.py +991 -0
- package/adapters/copilot/scripts/forge-init.sh +44 -0
- package/adapters/copilot/scripts/forge-root.sh +30 -8
- package/adapters/copilot/scripts/validate-traceability.py +150 -0
- package/adapters/copilot/skills/forge/forge.md +16 -6
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +26 -20
- 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 +18 -8
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +26 -15
- package/adapters/copilot/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +16 -6
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +15 -5
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +27 -17
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +17 -7
- package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +17 -7
- package/adapters/copilot/skills/forge-fix/forge-fix.md +12 -2
- package/adapters/copilot/skills/forge-init/forge-init.md +11 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +24 -9
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/cursor/.feature-forge-bundle.json +6 -0
- package/adapters/cursor/references/forge-config-schema.json +2 -2
- package/adapters/cursor/references/portable-root.md +8 -5
- package/adapters/cursor/references/process-overview.md +1 -1
- package/adapters/cursor/references/shared-conventions.md +24 -5
- package/adapters/cursor/references/stack-resolution.md +4 -1
- package/adapters/cursor/references/stacks/go.md +1 -1
- package/adapters/cursor/references/stacks/python.md +1 -1
- package/adapters/cursor/references/stacks/rust.md +1 -1
- package/adapters/cursor/references/stacks/typescript.md +1 -1
- package/adapters/cursor/scripts/epic-manifest.py +1379 -0
- package/adapters/cursor/scripts/forge-bootstrap.py +991 -0
- package/adapters/cursor/scripts/forge-init.sh +44 -0
- package/adapters/cursor/scripts/forge-root.sh +30 -8
- package/adapters/cursor/scripts/validate-traceability.py +150 -0
- package/adapters/cursor/skills/forge/forge.mdc +16 -6
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +26 -20
- 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 +18 -8
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +26 -15
- package/adapters/cursor/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +16 -6
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +15 -5
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +27 -17
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +17 -7
- package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +17 -7
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +12 -2
- package/adapters/cursor/skills/forge-init/forge-init.mdc +11 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +24 -9
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/gemini/.feature-forge-bundle.json +6 -0
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/forge-config-schema.json +2 -2
- package/adapters/gemini/references/portable-root.md +8 -5
- package/adapters/gemini/references/process-overview.md +1 -1
- package/adapters/gemini/references/shared-conventions.md +24 -5
- package/adapters/gemini/references/stack-resolution.md +4 -1
- package/adapters/gemini/references/stacks/go.md +1 -1
- package/adapters/gemini/references/stacks/python.md +1 -1
- package/adapters/gemini/references/stacks/rust.md +1 -1
- package/adapters/gemini/references/stacks/typescript.md +1 -1
- package/adapters/gemini/scripts/epic-manifest.py +1379 -0
- package/adapters/gemini/scripts/forge-bootstrap.py +991 -0
- package/adapters/gemini/scripts/forge-init.sh +44 -0
- package/adapters/gemini/scripts/forge-root.sh +30 -8
- package/adapters/gemini/scripts/validate-traceability.py +150 -0
- package/adapters/gemini/skills/forge/forge.md +16 -6
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +26 -20
- 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 +18 -8
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +26 -15
- package/adapters/gemini/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +16 -6
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +15 -5
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +27 -17
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +17 -7
- package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +17 -7
- package/adapters/gemini/skills/forge-fix/forge-fix.md +12 -2
- package/adapters/gemini/skills/forge-init/forge-init.md +11 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +24 -9
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
- package/dist/agent-targets.d.ts +20 -4
- package/dist/agent-targets.js +29 -4
- package/dist/apply.js +245 -18
- package/dist/cli.js +12 -6
- package/dist/hash.d.ts +5 -0
- package/dist/hash.js +7 -0
- package/dist/manifest.d.ts +4 -2
- package/dist/manifest.js +58 -2
- package/dist/placements.d.ts +69 -0
- package/dist/placements.js +116 -0
- package/dist/plan.d.ts +7 -0
- package/dist/plan.js +87 -1
- package/dist/rauf.d.ts +4 -4
- package/dist/rauf.js +3 -3
- package/dist/report.js +21 -0
- package/dist/source.d.ts +4 -3
- package/dist/source.js +4 -3
- package/dist/types.d.ts +163 -19
- package/dist/types.js +42 -11
- package/package.json +1 -1
- package/adapters/codex/agents/openai.yaml +0 -10
package/dist/hash.d.ts
CHANGED
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* for an already-located, integrity-checked bundle, caught at the operation boundary.
|
|
16
16
|
*/
|
|
17
17
|
export declare function sha256File(filePath: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* SHA-256 of a UTF-8 string, hex-encoded. Used to fingerprint a managed-block region (A4b) so the
|
|
20
|
+
* planner can tell a clean prior block (recorded hash matches) from a user-edited one. Pure.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sha256String(s: string): string;
|
|
18
23
|
/**
|
|
19
24
|
* Deterministic SHA-256 over a directory tree's file set (OQ-4). The digest is a function of the
|
|
20
25
|
* set of `{ relativePosixPath, fileContentHash }` pairs ONLY — never of mtime, inode, or
|
package/dist/hash.js
CHANGED
|
@@ -21,6 +21,13 @@ export function sha256File(filePath) {
|
|
|
21
21
|
const buf = fs.readFileSync(filePath);
|
|
22
22
|
return createHash("sha256").update(buf).digest("hex");
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* SHA-256 of a UTF-8 string, hex-encoded. Used to fingerprint a managed-block region (A4b) so the
|
|
26
|
+
* planner can tell a clean prior block (recorded hash matches) from a user-edited one. Pure.
|
|
27
|
+
*/
|
|
28
|
+
export function sha256String(s) {
|
|
29
|
+
return createHash("sha256").update(Buffer.from(s, "utf8")).digest("hex");
|
|
30
|
+
}
|
|
24
31
|
/**
|
|
25
32
|
* Deterministic SHA-256 over a directory tree's file set (OQ-4). The digest is a function of the
|
|
26
33
|
* set of `{ relativePosixPath, fileContentHash }` pairs ONLY — never of mtime, inode, or
|
package/dist/manifest.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Zero runtime dependencies; only `node:` built-ins. Named exports only. Core functions return
|
|
9
9
|
* `Result<T, E>` and never throw for expected errors; `JSON.parse` is wrapped in `try/catch`.
|
|
10
10
|
*/
|
|
11
|
-
import { type AgentId, type InstallManifest, type ManifestFile, type Mode, type PlannedAction, type ResolveOpts, type Result, type Scope } from "./types.js";
|
|
11
|
+
import { type AgentId, type InstallManifest, type ManifestFile, type Mode, type Placement, type PlannedAction, type ResolveOpts, type Result, type Scope } from "./types.js";
|
|
12
12
|
/**
|
|
13
13
|
* Inputs to {@link buildManifest}. The caller (apply.ts, spec 04) assembles this from the resolved
|
|
14
14
|
* detection target, the chosen scope/mode, and the apply result's per-file inventory.
|
|
@@ -28,12 +28,14 @@ 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.8.
|
|
31
|
+
/** Recorded pinned rauf coordinate (e.g. "@garygentry/rauf@0.8.1"); `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?: {
|
|
35
35
|
readonly target: string;
|
|
36
36
|
};
|
|
37
|
+
/** Secondary placement inventory written this run (A4b); omit/empty when the agent has none. */
|
|
38
|
+
readonly placements?: readonly Placement[];
|
|
37
39
|
/** Prior manifest, if any. When present, its `installedAt` is preserved (this is an update). */
|
|
38
40
|
readonly previous?: InstallManifest | null;
|
|
39
41
|
/** Injectable clock for deterministic tests. Default: `() => new Date()`. */
|
package/dist/manifest.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
-
import { AGENT_TARGETS, MANIFEST_PREFIX, SCHEMA_VERSION, ok, err, } from "./types.js";
|
|
13
|
+
import { AGENT_TARGETS, MANIFEST_PREFIX, SCHEMA_VERSION, ok, err, READABLE_SCHEMA_VERSIONS, } from "./types.js";
|
|
14
14
|
import { destinationFor } from "./agent-targets.js";
|
|
15
15
|
/**
|
|
16
16
|
* Assemble an {@link InstallManifest} from an apply result (REQ-SAFE-01/03). Pure — no I/O.
|
|
@@ -40,8 +40,18 @@ export function buildManifest(args) {
|
|
|
40
40
|
skills,
|
|
41
41
|
files,
|
|
42
42
|
...(args.link !== undefined ? { link: args.link } : {}),
|
|
43
|
+
...(args.placements && args.placements.length > 0
|
|
44
|
+
? { placements: args.placements.map(normalizePlacement) }
|
|
45
|
+
: {}),
|
|
43
46
|
};
|
|
44
47
|
}
|
|
48
|
+
/** Canonicalize a placement for persistence: sort its file inventory by path (byte-wise). */
|
|
49
|
+
function normalizePlacement(p) {
|
|
50
|
+
const files = [...p.files]
|
|
51
|
+
.map((f) => ({ path: f.path, ...(f.sha256 !== undefined ? { sha256: f.sha256 } : {}) }))
|
|
52
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
53
|
+
return { kind: p.kind, root: p.root, destination: p.destination, files };
|
|
54
|
+
}
|
|
45
55
|
// ---------------------------------------------------------------------------
|
|
46
56
|
// manifestPath
|
|
47
57
|
// ---------------------------------------------------------------------------
|
|
@@ -142,7 +152,7 @@ function validateManifest(x) {
|
|
|
142
152
|
if (typeof x !== "object" || x === null)
|
|
143
153
|
return { ok: false, reason: "not an object" };
|
|
144
154
|
const o = x;
|
|
145
|
-
if (o.schemaVersion
|
|
155
|
+
if (!READABLE_SCHEMA_VERSIONS.includes(o.schemaVersion)) {
|
|
146
156
|
return { ok: false, reason: `unsupported schemaVersion ${String(o.schemaVersion)}` };
|
|
147
157
|
}
|
|
148
158
|
if (typeof o.agent !== "string" || !AGENT_IDS_SET.has(o.agent)) {
|
|
@@ -196,8 +206,45 @@ function validateManifest(x) {
|
|
|
196
206
|
if (o.mode === "copy" && o.link !== undefined) {
|
|
197
207
|
return { ok: false, reason: "copy mode manifest must not carry link" };
|
|
198
208
|
}
|
|
209
|
+
// Optional secondary placements (manifest v2, A4b). Absent on v1 and on agents without a rule.
|
|
210
|
+
if (o.placements !== undefined) {
|
|
211
|
+
if (!Array.isArray(o.placements))
|
|
212
|
+
return { ok: false, reason: "invalid placements[]" };
|
|
213
|
+
for (const p of o.placements) {
|
|
214
|
+
const reason = validatePlacement(p);
|
|
215
|
+
if (reason !== null)
|
|
216
|
+
return { ok: false, reason };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
199
219
|
return { ok: true, value: x };
|
|
200
220
|
}
|
|
221
|
+
/** Structural validation of one placement record; returns a reason string, or null if valid. */
|
|
222
|
+
function validatePlacement(p) {
|
|
223
|
+
if (typeof p !== "object" || p === null)
|
|
224
|
+
return "placements[] entry not an object";
|
|
225
|
+
const o = p;
|
|
226
|
+
if (o.kind !== "mirror" && o.kind !== "managed-block") {
|
|
227
|
+
return `invalid placement kind ${String(o.kind)}`;
|
|
228
|
+
}
|
|
229
|
+
if (typeof o.root !== "string" || o.root.length === 0)
|
|
230
|
+
return "placement missing root";
|
|
231
|
+
if (typeof o.destination !== "string" || o.destination.length === 0) {
|
|
232
|
+
return "placement missing destination";
|
|
233
|
+
}
|
|
234
|
+
if (!Array.isArray(o.files))
|
|
235
|
+
return "invalid placement files[]";
|
|
236
|
+
for (const f of o.files) {
|
|
237
|
+
if (typeof f !== "object" || f === null)
|
|
238
|
+
return "invalid placement files[] entry";
|
|
239
|
+
const ff = f;
|
|
240
|
+
if (typeof ff.path !== "string")
|
|
241
|
+
return "placement files[].path not a string";
|
|
242
|
+
if (ff.sha256 !== undefined && typeof ff.sha256 !== "string") {
|
|
243
|
+
return "placement files[].sha256 not a string";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
201
248
|
// ---------------------------------------------------------------------------
|
|
202
249
|
// planUninstall — the uninstall removal POLICY
|
|
203
250
|
// ---------------------------------------------------------------------------
|
|
@@ -212,11 +259,20 @@ export function planUninstall(manifest) {
|
|
|
212
259
|
const files = isSymlink
|
|
213
260
|
? [{ relpath: ".", action: "remove" }]
|
|
214
261
|
: manifest.files.map((f) => ({ relpath: f.path, action: "remove" }));
|
|
262
|
+
// Secondary placements (A4b) are removed too: a "mirror" deletes each recorded file; a
|
|
263
|
+
// "managed-block" strips only the sentinel region (apply interprets a "remove" action per kind).
|
|
264
|
+
const placements = (manifest.placements ?? []).map((p) => ({
|
|
265
|
+
kind: p.kind,
|
|
266
|
+
root: p.root,
|
|
267
|
+
destination: p.destination,
|
|
268
|
+
files: p.files.map((f) => ({ relpath: f.path, action: "remove" })),
|
|
269
|
+
}));
|
|
215
270
|
return ok({
|
|
216
271
|
agent: manifest.agent,
|
|
217
272
|
scope: manifest.scope,
|
|
218
273
|
mode: manifest.mode,
|
|
219
274
|
destination: manifest.destination,
|
|
220
275
|
files,
|
|
276
|
+
...(placements.length > 0 ? { placements } : {}),
|
|
221
277
|
});
|
|
222
278
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secondary install placements (A4b) — the second-root generalization the single-`destination`
|
|
3
|
+
* install model can't express. Two kinds (see {@link PlacementKind}):
|
|
4
|
+
* - "mirror" — codex copies the bundle's `agents/*.toml` FLAT into `.codex/agents/`, where
|
|
5
|
+
* Codex loads custom agents (it does NOT read them from `.agents/skills`).
|
|
6
|
+
* - "managed-block" — copilot writes a sentinel-delimited pointer block into the (possibly
|
|
7
|
+
* user-owned) `.github/copilot-instructions.md`, preserving the rest of it.
|
|
8
|
+
*
|
|
9
|
+
* This module is PURE: it resolves declarative {@link PlacementSpec}s to absolute roots, selects the
|
|
10
|
+
* mirror source files, and provides the managed-block string transforms (render/upsert/remove/read).
|
|
11
|
+
* The planner (plan.ts) decides actions and the apply engine (apply.ts) executes them; neither knows
|
|
12
|
+
* the per-kind string mechanics — those live here. Zero runtime dependencies; only `node:` built-ins.
|
|
13
|
+
*/
|
|
14
|
+
import { type AgentTarget, type PlacementKind, type PlacementSpec, type ResolveOpts, type Scope } from "./types.js";
|
|
15
|
+
import type { LocatedSource } from "./source.js";
|
|
16
|
+
/** A {@link PlacementSpec} resolved to absolute paths under a scope. Pure derivation; nothing stored. */
|
|
17
|
+
export interface ResolvedPlacement {
|
|
18
|
+
readonly kind: PlacementKind;
|
|
19
|
+
/** Absolute containment boundary (REQ-SEC-02): `<scopeRoot>/<spec.baseDir>`. */
|
|
20
|
+
readonly root: string;
|
|
21
|
+
/** Absolute destination: a DIR ("mirror") or a FILE ("managed-block"): `<root>/<spec.subpath>`. */
|
|
22
|
+
readonly destination: string;
|
|
23
|
+
readonly spec: PlacementSpec;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve every secondary placement declared on `target` to absolute roots under `scope` (A4b).
|
|
27
|
+
* Returns `[]` for agents with no placements (claude/cursor/gemini). Pure; the single derivation
|
|
28
|
+
* point so a new rule stays one `AGENT_TARGETS` edit (REQ-SCALE-01).
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolvePlacements(target: AgentTarget, scope: Scope, opts?: ResolveOpts): ResolvedPlacement[];
|
|
31
|
+
/** One selected mirror source: the bundle-relative source and its FLAT destination basename. */
|
|
32
|
+
export interface MirrorFile {
|
|
33
|
+
readonly srcRelpath: string;
|
|
34
|
+
readonly destRelpath: string;
|
|
35
|
+
readonly srcHash: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Select the bundle files a "mirror" placement copies (A4b): every `source.files` entry whose
|
|
39
|
+
* POSIX relpath starts with `spec.sourcePrefix`, copied FLAT (basename only) into the destination.
|
|
40
|
+
* Sorted by destination basename for deterministic plans. Pure.
|
|
41
|
+
*/
|
|
42
|
+
export declare function selectMirrorFiles(source: LocatedSource, spec: PlacementSpec): MirrorFile[];
|
|
43
|
+
/**
|
|
44
|
+
* Render the managed-block BODY (without sentinels) for copilot (A4b). Points Copilot — which has no
|
|
45
|
+
* skills loader — at the staged bundle under `.github/feature-forge/` and lists the available skills.
|
|
46
|
+
* Deterministic given the bundle's skill ids. Pure.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderCopilotBlock(skills: readonly string[]): string;
|
|
49
|
+
/** Wrap a rendered block body in the managed sentinels — the exact region written on disk. */
|
|
50
|
+
export declare function wrapBlock(body: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Extract the full managed region (sentinels INCLUDED) currently present in `content`, or `null` if
|
|
53
|
+
* no well-formed `start…end` region exists. The region is what `wrapBlock` produces, so its hash is
|
|
54
|
+
* directly comparable to a freshly rendered block. Pure.
|
|
55
|
+
*/
|
|
56
|
+
export declare function extractManagedRegion(content: string): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Insert or replace the managed block in `existing`, preserving all user content outside the
|
|
59
|
+
* sentinels (A4b). If a region exists it is replaced in place; otherwise the block is appended after
|
|
60
|
+
* the existing content (separated by a blank line). `existing` is `""` for a not-yet-created file.
|
|
61
|
+
* The result always ends with a single trailing newline. Pure.
|
|
62
|
+
*/
|
|
63
|
+
export declare function upsertBlock(existing: string, body: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Remove the managed block from `existing`, preserving the rest (A4b uninstall). Returns the
|
|
66
|
+
* remaining content (trailing whitespace trimmed to a single newline), or `""` if nothing but the
|
|
67
|
+
* block (and whitespace) remains — the caller deletes the file in that case. Pure.
|
|
68
|
+
*/
|
|
69
|
+
export declare function removeBlock(existing: string): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secondary install placements (A4b) — the second-root generalization the single-`destination`
|
|
3
|
+
* install model can't express. Two kinds (see {@link PlacementKind}):
|
|
4
|
+
* - "mirror" — codex copies the bundle's `agents/*.toml` FLAT into `.codex/agents/`, where
|
|
5
|
+
* Codex loads custom agents (it does NOT read them from `.agents/skills`).
|
|
6
|
+
* - "managed-block" — copilot writes a sentinel-delimited pointer block into the (possibly
|
|
7
|
+
* user-owned) `.github/copilot-instructions.md`, preserving the rest of it.
|
|
8
|
+
*
|
|
9
|
+
* This module is PURE: it resolves declarative {@link PlacementSpec}s to absolute roots, selects the
|
|
10
|
+
* mirror source files, and provides the managed-block string transforms (render/upsert/remove/read).
|
|
11
|
+
* The planner (plan.ts) decides actions and the apply engine (apply.ts) executes them; neither knows
|
|
12
|
+
* the per-kind string mechanics — those live here. Zero runtime dependencies; only `node:` built-ins.
|
|
13
|
+
*/
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { MANAGED_BLOCK_END, MANAGED_BLOCK_START, } from "./types.js";
|
|
16
|
+
import { resolveRoots } from "./agent-targets.js";
|
|
17
|
+
/**
|
|
18
|
+
* Resolve every secondary placement declared on `target` to absolute roots under `scope` (A4b).
|
|
19
|
+
* Returns `[]` for agents with no placements (claude/cursor/gemini). Pure; the single derivation
|
|
20
|
+
* point so a new rule stays one `AGENT_TARGETS` edit (REQ-SCALE-01).
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePlacements(target, scope, opts) {
|
|
23
|
+
const roots = resolveRoots(opts);
|
|
24
|
+
const scopeRoot = scope === "global" ? roots.home : roots.cwd;
|
|
25
|
+
return (target.placements ?? []).map((spec) => {
|
|
26
|
+
const root = path.resolve(scopeRoot, spec.baseDir);
|
|
27
|
+
return { kind: spec.kind, root, destination: path.resolve(root, spec.subpath), spec };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Select the bundle files a "mirror" placement copies (A4b): every `source.files` entry whose
|
|
32
|
+
* POSIX relpath starts with `spec.sourcePrefix`, copied FLAT (basename only) into the destination.
|
|
33
|
+
* Sorted by destination basename for deterministic plans. Pure.
|
|
34
|
+
*/
|
|
35
|
+
export function selectMirrorFiles(source, spec) {
|
|
36
|
+
const prefix = spec.sourcePrefix ?? "";
|
|
37
|
+
return source.files
|
|
38
|
+
.filter((f) => f.relpath.startsWith(prefix))
|
|
39
|
+
.map((f) => ({ srcRelpath: f.relpath, destRelpath: path.posix.basename(f.relpath), srcHash: f.sha256 }))
|
|
40
|
+
.sort((a, b) => (a.destRelpath < b.destRelpath ? -1 : a.destRelpath > b.destRelpath ? 1 : 0));
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Managed-block string transforms (pure)
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
/**
|
|
46
|
+
* Render the managed-block BODY (without sentinels) for copilot (A4b). Points Copilot — which has no
|
|
47
|
+
* skills loader — at the staged bundle under `.github/feature-forge/` and lists the available skills.
|
|
48
|
+
* Deterministic given the bundle's skill ids. Pure.
|
|
49
|
+
*/
|
|
50
|
+
export function renderCopilotBlock(skills) {
|
|
51
|
+
const lines = [
|
|
52
|
+
"# feature-forge",
|
|
53
|
+
"",
|
|
54
|
+
"The feature-forge skill suite is installed in this repository under",
|
|
55
|
+
"`.github/feature-forge/`. GitHub Copilot has no skills loader, so consult those files",
|
|
56
|
+
"directly when a feature-forge workflow is requested.",
|
|
57
|
+
"",
|
|
58
|
+
"Each skill lives at `.github/feature-forge/skills/<name>/SKILL.md`. Available skills:",
|
|
59
|
+
"",
|
|
60
|
+
...[...skills].sort().map((s) => `- ${s}`),
|
|
61
|
+
"",
|
|
62
|
+
"Shared references are under `.github/feature-forge/references/`; helper scripts under",
|
|
63
|
+
"`.github/feature-forge/scripts/`.",
|
|
64
|
+
];
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
/** Wrap a rendered block body in the managed sentinels — the exact region written on disk. */
|
|
68
|
+
export function wrapBlock(body) {
|
|
69
|
+
return `${MANAGED_BLOCK_START}\n${body}\n${MANAGED_BLOCK_END}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract the full managed region (sentinels INCLUDED) currently present in `content`, or `null` if
|
|
73
|
+
* no well-formed `start…end` region exists. The region is what `wrapBlock` produces, so its hash is
|
|
74
|
+
* directly comparable to a freshly rendered block. Pure.
|
|
75
|
+
*/
|
|
76
|
+
export function extractManagedRegion(content) {
|
|
77
|
+
const startIdx = content.indexOf(MANAGED_BLOCK_START);
|
|
78
|
+
if (startIdx === -1)
|
|
79
|
+
return null;
|
|
80
|
+
const endIdx = content.indexOf(MANAGED_BLOCK_END, startIdx + MANAGED_BLOCK_START.length);
|
|
81
|
+
if (endIdx === -1)
|
|
82
|
+
return null;
|
|
83
|
+
return content.slice(startIdx, endIdx + MANAGED_BLOCK_END.length);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Insert or replace the managed block in `existing`, preserving all user content outside the
|
|
87
|
+
* sentinels (A4b). If a region exists it is replaced in place; otherwise the block is appended after
|
|
88
|
+
* the existing content (separated by a blank line). `existing` is `""` for a not-yet-created file.
|
|
89
|
+
* The result always ends with a single trailing newline. Pure.
|
|
90
|
+
*/
|
|
91
|
+
export function upsertBlock(existing, body) {
|
|
92
|
+
const region = wrapBlock(body);
|
|
93
|
+
const current = extractManagedRegion(existing);
|
|
94
|
+
if (current !== null) {
|
|
95
|
+
return ensureTrailingNewline(existing.replace(current, region));
|
|
96
|
+
}
|
|
97
|
+
if (existing.trim() === "")
|
|
98
|
+
return ensureTrailingNewline(region);
|
|
99
|
+
return ensureTrailingNewline(`${existing.replace(/\n+$/, "")}\n\n${region}`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Remove the managed block from `existing`, preserving the rest (A4b uninstall). Returns the
|
|
103
|
+
* remaining content (trailing whitespace trimmed to a single newline), or `""` if nothing but the
|
|
104
|
+
* block (and whitespace) remains — the caller deletes the file in that case. Pure.
|
|
105
|
+
*/
|
|
106
|
+
export function removeBlock(existing) {
|
|
107
|
+
const region = extractManagedRegion(existing);
|
|
108
|
+
if (region === null)
|
|
109
|
+
return existing;
|
|
110
|
+
const without = existing.replace(region, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
111
|
+
return without === "" ? "" : `${without}\n`;
|
|
112
|
+
}
|
|
113
|
+
/** Ensure exactly one trailing newline. */
|
|
114
|
+
function ensureTrailingNewline(s) {
|
|
115
|
+
return `${s.replace(/\n+$/, "")}\n`;
|
|
116
|
+
}
|
package/dist/plan.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { AgentId, FileActionKind, Mode, PlannedAction, Result, Scope, InstallManifest } from "./types.js";
|
|
11
11
|
import { type LocatedSource } from "./source.js";
|
|
12
|
+
import { type ResolvedPlacement } from "./placements.js";
|
|
12
13
|
/**
|
|
13
14
|
* Everything the pure planner needs to diff source ⇆ destination ⇆ manifest for ONE agent
|
|
14
15
|
* (spec 04 §4). Built by cli.ts (07); the planner reads these and writes nothing.
|
|
@@ -30,6 +31,12 @@ export interface PlanContext {
|
|
|
30
31
|
readonly force: boolean;
|
|
31
32
|
/** The pinned rauf coordinate to surface on the plan (06); the planner only echoes it. */
|
|
32
33
|
readonly raufPin?: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Resolved secondary placements for this agent (A4b), or absent/empty when it has none. Supplied by
|
|
36
|
+
* cli.ts (which holds the scope roots); the planner diffs each against its destination and the prior
|
|
37
|
+
* manifest's matching placement inventory.
|
|
38
|
+
*/
|
|
39
|
+
readonly placements?: ResolvedPlacement[];
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* Classify one bundle-relative path (spec 04 §6 table). PURE: hashes are read, nothing is written.
|
package/dist/plan.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { ok, err } from "./types.js";
|
|
13
|
-
import { sha256File } from "./hash.js";
|
|
13
|
+
import { sha256File, sha256String } from "./hash.js";
|
|
14
|
+
import { selectMirrorFiles, renderCopilotBlock, wrapBlock, extractManagedRegion, } from "./placements.js";
|
|
14
15
|
import { planUninstall } from "./manifest.js";
|
|
15
16
|
import { isWindows } from "./fsutil.js";
|
|
16
17
|
/**
|
|
@@ -98,15 +99,100 @@ function buildPlan(ctx, withOrphans) {
|
|
|
98
99
|
const files = ctx.mode === "symlink"
|
|
99
100
|
? planSymlink(ctx)
|
|
100
101
|
: planCopy(ctx, withOrphans);
|
|
102
|
+
// Secondary placements (A4b) are always copy-style regardless of the primary mode: a mirror is a
|
|
103
|
+
// few flat files and a managed-block is a merge, neither of which a whole-dir symlink expresses.
|
|
104
|
+
const placements = planPlacements(ctx, withOrphans);
|
|
101
105
|
const action = {
|
|
102
106
|
agent: ctx.agent,
|
|
103
107
|
scope: ctx.scope,
|
|
104
108
|
mode: ctx.mode,
|
|
105
109
|
files,
|
|
106
110
|
...(ctx.raufPin !== undefined ? { raufPin: ctx.raufPin } : {}),
|
|
111
|
+
...(placements.length > 0 ? { placements } : {}),
|
|
107
112
|
};
|
|
108
113
|
return ok(action);
|
|
109
114
|
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Secondary placements (A4b)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
/** Plan every resolved secondary placement (A4b). `ctx.source` is non-null here. */
|
|
119
|
+
function planPlacements(ctx, withOrphans) {
|
|
120
|
+
const resolved = ctx.placements ?? [];
|
|
121
|
+
if (resolved.length === 0)
|
|
122
|
+
return [];
|
|
123
|
+
const source = ctx.source;
|
|
124
|
+
const priorByDest = priorPlacementIndex(ctx.priorManifest);
|
|
125
|
+
return resolved.map((rp) => rp.kind === "mirror"
|
|
126
|
+
? planMirror(ctx, rp, source, priorByDest.get(rp.destination) ?? null, withOrphans)
|
|
127
|
+
: planManagedBlock(ctx, rp, source, priorByDest.get(rp.destination) ?? null));
|
|
128
|
+
}
|
|
129
|
+
/** Index prior-manifest placements by their absolute destination, for clean/orphan reconciliation. */
|
|
130
|
+
function priorPlacementIndex(prior) {
|
|
131
|
+
const m = new Map();
|
|
132
|
+
for (const p of prior?.placements ?? [])
|
|
133
|
+
m.set(p.destination, p);
|
|
134
|
+
return m;
|
|
135
|
+
}
|
|
136
|
+
/** Diff a "mirror" placement: each selected bundle file vs its flat destination + recorded hash. */
|
|
137
|
+
function planMirror(ctx, rp, source, prior, withOrphans) {
|
|
138
|
+
const recorded = new Map();
|
|
139
|
+
for (const f of prior?.files ?? [])
|
|
140
|
+
recorded.set(f.path, f);
|
|
141
|
+
const mirror = selectMirrorFiles(source, rp.spec);
|
|
142
|
+
const files = mirror.map((mf) => {
|
|
143
|
+
const destAbs = path.join(rp.destination, mf.destRelpath);
|
|
144
|
+
const destHash = hashIfExists(destAbs);
|
|
145
|
+
const manifestHash = recorded.get(mf.destRelpath)?.sha256;
|
|
146
|
+
const action = classifyFile(mf.destRelpath, mf.srcHash, destHash, manifestHash, ctx.force);
|
|
147
|
+
return { relpath: mf.destRelpath, action, srcRelpath: mf.srcRelpath };
|
|
148
|
+
});
|
|
149
|
+
if (withOrphans && prior !== null) {
|
|
150
|
+
const live = new Set(mirror.map((mf) => mf.destRelpath));
|
|
151
|
+
for (const f of prior.files) {
|
|
152
|
+
if (!live.has(f.path))
|
|
153
|
+
files.push({ relpath: f.path, action: "remove" });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { kind: "mirror", root: rp.root, destination: rp.destination, files };
|
|
157
|
+
}
|
|
158
|
+
/** Diff a "managed-block" placement: render the block, compare its region to the on-disk region. */
|
|
159
|
+
function planManagedBlock(ctx, rp, source, prior) {
|
|
160
|
+
const blockContent = renderCopilotBlock(source.skills);
|
|
161
|
+
const newHash = sha256String(wrapBlock(blockContent));
|
|
162
|
+
const basename = path.basename(rp.destination);
|
|
163
|
+
const current = readManagedRegionHash(rp.destination);
|
|
164
|
+
const recordedHash = prior?.files.find((f) => f.path === basename)?.sha256;
|
|
165
|
+
let action;
|
|
166
|
+
if (current === undefined) {
|
|
167
|
+
action = "create"; // no managed region present yet
|
|
168
|
+
}
|
|
169
|
+
else if (current === newHash) {
|
|
170
|
+
action = "unchanged";
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const clean = recordedHash !== undefined && current === recordedHash;
|
|
174
|
+
action = clean ? "overwrite" : ctx.force ? "overwrite" : "skip-modified";
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
kind: "managed-block",
|
|
178
|
+
root: rp.root,
|
|
179
|
+
destination: rp.destination,
|
|
180
|
+
files: [{ relpath: basename, action }],
|
|
181
|
+
blockContent,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** Hash of the managed region currently in the target file, or undefined if absent/unreadable. */
|
|
185
|
+
function readManagedRegionHash(file) {
|
|
186
|
+
let content;
|
|
187
|
+
try {
|
|
188
|
+
content = fs.readFileSync(file, "utf8");
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const region = extractManagedRegion(content);
|
|
194
|
+
return region === null ? undefined : sha256String(region);
|
|
195
|
+
}
|
|
110
196
|
/** Copy-mode per-file diff (spec 04 §6). `ctx.source` is non-null here. */
|
|
111
197
|
function planCopy(ctx, withOrphans) {
|
|
112
198
|
const source = ctx.source;
|
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.8.
|
|
22
|
+
* known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.8.1.
|
|
23
23
|
*
|
|
24
|
-
* rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.8.
|
|
24
|
+
* rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.8.1` 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.8.
|
|
28
|
+
export declare const RAUF_PIN = "@garygentry/rauf@0.8.1";
|
|
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.8.0";
|
|
|
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.8.
|
|
40
|
+
* @param coordinate - the `name@version` to resolve, e.g. "@garygentry/rauf@0.8.1"
|
|
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.8.
|
|
23
|
+
* known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.8.1.
|
|
24
24
|
*
|
|
25
|
-
* rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.8.
|
|
25
|
+
* rauf is now PUBLISHED (rauf#28): `@garygentry/rauf@0.8.1` 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.8.
|
|
29
|
+
export const RAUF_PIN = "@garygentry/rauf@0.8.1";
|
|
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
|
@@ -47,6 +47,9 @@ function renderAgent(subcommand, a) {
|
|
|
47
47
|
// Decode the synthetic status rows (§3.3) into one human line.
|
|
48
48
|
const status = a.actions.map((f) => f.relpath).join(" ");
|
|
49
49
|
lines.push(`${a.agent}: ${a.detected ? "detected" : "not detected"} ${status}`);
|
|
50
|
+
const note = confidenceNote(a);
|
|
51
|
+
if (note)
|
|
52
|
+
lines.push(` ${note}`);
|
|
50
53
|
return lines;
|
|
51
54
|
}
|
|
52
55
|
if (!a.ok && a.error) {
|
|
@@ -58,10 +61,28 @@ function renderAgent(subcommand, a) {
|
|
|
58
61
|
for (const f of a.actions) {
|
|
59
62
|
lines.push(` ${actionVerb(f.action)} ${f.relpath}`);
|
|
60
63
|
}
|
|
64
|
+
const note = confidenceNote(a);
|
|
65
|
+
if (note)
|
|
66
|
+
lines.push(` ${note}`);
|
|
61
67
|
if (a.raufPin)
|
|
62
68
|
lines.push(` rauf default runner pinned: ${a.raufPin}`);
|
|
63
69
|
return lines;
|
|
64
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Honest per-agent confidence note (A4 / Finding 6): silent for fully-trusted targets
|
|
73
|
+
* (`confirmed`/`verified-current`), explicit for `best-known`/`unsupported` so a user knows
|
|
74
|
+
* an install path may not be auto-loaded by that agent and where to check current docs. Pure.
|
|
75
|
+
*/
|
|
76
|
+
function confidenceNote(a) {
|
|
77
|
+
if (!a.confidence || a.confidence === "confirmed" || a.confidence === "verified-current") {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const where = a.docsUrl ? ` — see ${a.docsUrl}` : "";
|
|
81
|
+
if (a.confidence === "unsupported") {
|
|
82
|
+
return `note: ${a.agent} has no confirmed install surface (unsupported)${where}`;
|
|
83
|
+
}
|
|
84
|
+
return `note: ${a.agent} install path is best-known, not vendor-confirmed${where}`;
|
|
85
|
+
}
|
|
65
86
|
/** Map a FileActionKind to its human verb (REQ-OBS-01 vocabulary). */
|
|
66
87
|
export function actionVerb(kind) {
|
|
67
88
|
switch (kind) {
|
package/dist/source.d.ts
CHANGED
|
@@ -44,9 +44,10 @@ export interface LocatedSource {
|
|
|
44
44
|
export declare function locateBundle(agent: AgentId, opts?: LocateBundleOpts): Result<string>;
|
|
45
45
|
/**
|
|
46
46
|
* Minimal integrity check for a located bundle (REQ-OPS-06). Valid iff the `BUNDLE_REQUIRED_PATHS`
|
|
47
|
-
* are present: `skills/` is a non-empty dir,
|
|
48
|
-
* `
|
|
49
|
-
* `
|
|
47
|
+
* are present: `skills/` is a non-empty dir, the neutral `.feature-forge-bundle.json` sentinel and
|
|
48
|
+
* every bundled runtime helper (`forge-root.sh`, `forge-init.sh`, `epic-manifest.py`,
|
|
49
|
+
* `validate-traceability.py`, `forge-bootstrap.py`) exist, and (gemini) `gemini-extension.json`
|
|
50
|
+
* exists. Keys on the neutral sentinel, NOT the Claude-only `.claude-plugin/plugin.json`.
|
|
50
51
|
*
|
|
51
52
|
* @returns ok(undefined) when every required path is present; err(SOURCE_INVALID) naming the
|
|
52
53
|
* first missing/invalid required path otherwise.
|
package/dist/source.js
CHANGED
|
@@ -38,9 +38,10 @@ export function locateBundle(agent, opts = {}) {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Minimal integrity check for a located bundle (REQ-OPS-06). Valid iff the `BUNDLE_REQUIRED_PATHS`
|
|
41
|
-
* are present: `skills/` is a non-empty dir,
|
|
42
|
-
* `
|
|
43
|
-
* `
|
|
41
|
+
* are present: `skills/` is a non-empty dir, the neutral `.feature-forge-bundle.json` sentinel and
|
|
42
|
+
* every bundled runtime helper (`forge-root.sh`, `forge-init.sh`, `epic-manifest.py`,
|
|
43
|
+
* `validate-traceability.py`, `forge-bootstrap.py`) exist, and (gemini) `gemini-extension.json`
|
|
44
|
+
* exists. Keys on the neutral sentinel, NOT the Claude-only `.claude-plugin/plugin.json`.
|
|
44
45
|
*
|
|
45
46
|
* @returns ok(undefined) when every required path is present; err(SOURCE_INVALID) naming the
|
|
46
47
|
* first missing/invalid required path otherwise.
|