@garygentry/feature-forge 0.1.0

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/adapters/GENERATION-REPORT.md +128 -0
  3. package/adapters/claude/agents/forge-researcher.md +137 -0
  4. package/adapters/claude/agents/forge-spec-writer.md +115 -0
  5. package/adapters/claude/agents/forge-verifier.md +121 -0
  6. package/adapters/claude/references/epic-manifest-schema.json +120 -0
  7. package/adapters/claude/references/forge-config-schema.json +166 -0
  8. package/adapters/claude/references/pipeline-state-schema.json +110 -0
  9. package/adapters/claude/references/portable-root.md +56 -0
  10. package/adapters/claude/references/process-overview.md +123 -0
  11. package/adapters/claude/references/ralph-loop-contract.md +221 -0
  12. package/adapters/claude/references/shared-conventions.md +144 -0
  13. package/adapters/claude/references/skill-frontmatter.schema.json +17 -0
  14. package/adapters/claude/references/stack-resolution.md +51 -0
  15. package/adapters/claude/references/stacks/_generic.md +90 -0
  16. package/adapters/claude/references/stacks/go.md +138 -0
  17. package/adapters/claude/references/stacks/python.md +163 -0
  18. package/adapters/claude/references/stacks/rust.md +151 -0
  19. package/adapters/claude/references/stacks/typescript.md +111 -0
  20. package/adapters/claude/references/vendor-construct-inventory.md +49 -0
  21. package/adapters/claude/scripts/forge-root.sh +50 -0
  22. package/adapters/claude/skills/forge/SKILL.md +165 -0
  23. package/adapters/claude/skills/forge-0-epic/SKILL.md +303 -0
  24. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +222 -0
  25. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  26. package/adapters/claude/skills/forge-1-prd/SKILL.md +121 -0
  27. package/adapters/claude/skills/forge-1-prd/references/prd-template.md +106 -0
  28. package/adapters/claude/skills/forge-2-tech/SKILL.md +198 -0
  29. package/adapters/claude/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  30. package/adapters/claude/skills/forge-3-specs/SKILL.md +154 -0
  31. package/adapters/claude/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  32. package/adapters/claude/skills/forge-3-specs/references/spec-examples.md +71 -0
  33. package/adapters/claude/skills/forge-4-backlog/SKILL.md +146 -0
  34. package/adapters/claude/skills/forge-5-loop/SKILL.md +303 -0
  35. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +63 -0
  36. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +214 -0
  37. package/adapters/claude/skills/forge-6-docs/SKILL.md +179 -0
  38. package/adapters/claude/skills/forge-6-docs/references/doc-conventions.md +126 -0
  39. package/adapters/claude/skills/forge-fix/SKILL.md +65 -0
  40. package/adapters/claude/skills/forge-init/SKILL.md +29 -0
  41. package/adapters/claude/skills/forge-verify/SKILL.md +219 -0
  42. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +379 -0
  43. package/adapters/codex/agents/forge-researcher.md +133 -0
  44. package/adapters/codex/agents/forge-spec-writer.md +112 -0
  45. package/adapters/codex/agents/forge-verifier.md +115 -0
  46. package/adapters/codex/agents/openai.yaml +10 -0
  47. package/adapters/codex/references/epic-manifest-schema.json +120 -0
  48. package/adapters/codex/references/forge-config-schema.json +166 -0
  49. package/adapters/codex/references/pipeline-state-schema.json +110 -0
  50. package/adapters/codex/references/portable-root.md +56 -0
  51. package/adapters/codex/references/process-overview.md +123 -0
  52. package/adapters/codex/references/ralph-loop-contract.md +221 -0
  53. package/adapters/codex/references/shared-conventions.md +144 -0
  54. package/adapters/codex/references/skill-frontmatter.schema.json +17 -0
  55. package/adapters/codex/references/stack-resolution.md +51 -0
  56. package/adapters/codex/references/stacks/_generic.md +90 -0
  57. package/adapters/codex/references/stacks/go.md +138 -0
  58. package/adapters/codex/references/stacks/python.md +163 -0
  59. package/adapters/codex/references/stacks/rust.md +151 -0
  60. package/adapters/codex/references/stacks/typescript.md +111 -0
  61. package/adapters/codex/references/vendor-construct-inventory.md +49 -0
  62. package/adapters/codex/scripts/forge-root.sh +50 -0
  63. package/adapters/codex/skills/forge/forge.md +164 -0
  64. package/adapters/codex/skills/forge-0-epic/forge-0-epic.md +302 -0
  65. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +222 -0
  66. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  67. package/adapters/codex/skills/forge-1-prd/forge-1-prd.md +120 -0
  68. package/adapters/codex/skills/forge-1-prd/references/prd-template.md +106 -0
  69. package/adapters/codex/skills/forge-2-tech/forge-2-tech.md +197 -0
  70. package/adapters/codex/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  71. package/adapters/codex/skills/forge-3-specs/forge-3-specs.md +153 -0
  72. package/adapters/codex/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  73. package/adapters/codex/skills/forge-3-specs/references/spec-examples.md +71 -0
  74. package/adapters/codex/skills/forge-4-backlog/forge-4-backlog.md +145 -0
  75. package/adapters/codex/skills/forge-5-loop/forge-5-loop.md +302 -0
  76. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +63 -0
  77. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +214 -0
  78. package/adapters/codex/skills/forge-6-docs/forge-6-docs.md +178 -0
  79. package/adapters/codex/skills/forge-6-docs/references/doc-conventions.md +126 -0
  80. package/adapters/codex/skills/forge-fix/forge-fix.md +64 -0
  81. package/adapters/codex/skills/forge-init/forge-init.md +29 -0
  82. package/adapters/codex/skills/forge-verify/forge-verify.md +218 -0
  83. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +379 -0
  84. package/adapters/copilot/agents/forge-researcher.md +133 -0
  85. package/adapters/copilot/agents/forge-spec-writer.md +112 -0
  86. package/adapters/copilot/agents/forge-verifier.md +115 -0
  87. package/adapters/copilot/references/epic-manifest-schema.json +120 -0
  88. package/adapters/copilot/references/forge-config-schema.json +166 -0
  89. package/adapters/copilot/references/pipeline-state-schema.json +110 -0
  90. package/adapters/copilot/references/portable-root.md +56 -0
  91. package/adapters/copilot/references/process-overview.md +123 -0
  92. package/adapters/copilot/references/ralph-loop-contract.md +221 -0
  93. package/adapters/copilot/references/shared-conventions.md +144 -0
  94. package/adapters/copilot/references/skill-frontmatter.schema.json +17 -0
  95. package/adapters/copilot/references/stack-resolution.md +51 -0
  96. package/adapters/copilot/references/stacks/_generic.md +90 -0
  97. package/adapters/copilot/references/stacks/go.md +138 -0
  98. package/adapters/copilot/references/stacks/python.md +163 -0
  99. package/adapters/copilot/references/stacks/rust.md +151 -0
  100. package/adapters/copilot/references/stacks/typescript.md +111 -0
  101. package/adapters/copilot/references/vendor-construct-inventory.md +49 -0
  102. package/adapters/copilot/scripts/forge-root.sh +50 -0
  103. package/adapters/copilot/skills/forge/forge.md +164 -0
  104. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +302 -0
  105. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +222 -0
  106. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  107. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +120 -0
  108. package/adapters/copilot/skills/forge-1-prd/references/prd-template.md +106 -0
  109. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +197 -0
  110. package/adapters/copilot/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  111. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +153 -0
  112. package/adapters/copilot/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  113. package/adapters/copilot/skills/forge-3-specs/references/spec-examples.md +71 -0
  114. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +145 -0
  115. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +302 -0
  116. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +63 -0
  117. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +214 -0
  118. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +178 -0
  119. package/adapters/copilot/skills/forge-6-docs/references/doc-conventions.md +126 -0
  120. package/adapters/copilot/skills/forge-fix/forge-fix.md +64 -0
  121. package/adapters/copilot/skills/forge-init/forge-init.md +29 -0
  122. package/adapters/copilot/skills/forge-verify/forge-verify.md +218 -0
  123. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +379 -0
  124. package/adapters/cursor/agents/forge-researcher.mdc +134 -0
  125. package/adapters/cursor/agents/forge-spec-writer.mdc +113 -0
  126. package/adapters/cursor/agents/forge-verifier.mdc +116 -0
  127. package/adapters/cursor/references/epic-manifest-schema.json +120 -0
  128. package/adapters/cursor/references/forge-config-schema.json +166 -0
  129. package/adapters/cursor/references/pipeline-state-schema.json +110 -0
  130. package/adapters/cursor/references/portable-root.md +56 -0
  131. package/adapters/cursor/references/process-overview.md +123 -0
  132. package/adapters/cursor/references/ralph-loop-contract.md +221 -0
  133. package/adapters/cursor/references/shared-conventions.md +144 -0
  134. package/adapters/cursor/references/skill-frontmatter.schema.json +17 -0
  135. package/adapters/cursor/references/stack-resolution.md +51 -0
  136. package/adapters/cursor/references/stacks/_generic.md +90 -0
  137. package/adapters/cursor/references/stacks/go.md +138 -0
  138. package/adapters/cursor/references/stacks/python.md +163 -0
  139. package/adapters/cursor/references/stacks/rust.md +151 -0
  140. package/adapters/cursor/references/stacks/typescript.md +111 -0
  141. package/adapters/cursor/references/vendor-construct-inventory.md +49 -0
  142. package/adapters/cursor/scripts/forge-root.sh +50 -0
  143. package/adapters/cursor/skills/forge/forge.mdc +165 -0
  144. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +303 -0
  145. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +222 -0
  146. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  147. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +121 -0
  148. package/adapters/cursor/skills/forge-1-prd/references/prd-template.md +106 -0
  149. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +198 -0
  150. package/adapters/cursor/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  151. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +154 -0
  152. package/adapters/cursor/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  153. package/adapters/cursor/skills/forge-3-specs/references/spec-examples.md +71 -0
  154. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +146 -0
  155. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +303 -0
  156. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +63 -0
  157. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +214 -0
  158. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +179 -0
  159. package/adapters/cursor/skills/forge-6-docs/references/doc-conventions.md +126 -0
  160. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +65 -0
  161. package/adapters/cursor/skills/forge-init/forge-init.mdc +30 -0
  162. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +219 -0
  163. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +379 -0
  164. package/adapters/gemini/agents/forge-researcher.md +133 -0
  165. package/adapters/gemini/agents/forge-spec-writer.md +112 -0
  166. package/adapters/gemini/agents/forge-verifier.md +115 -0
  167. package/adapters/gemini/gemini-extension.json +54 -0
  168. package/adapters/gemini/references/epic-manifest-schema.json +120 -0
  169. package/adapters/gemini/references/forge-config-schema.json +166 -0
  170. package/adapters/gemini/references/pipeline-state-schema.json +110 -0
  171. package/adapters/gemini/references/portable-root.md +56 -0
  172. package/adapters/gemini/references/process-overview.md +123 -0
  173. package/adapters/gemini/references/ralph-loop-contract.md +221 -0
  174. package/adapters/gemini/references/shared-conventions.md +144 -0
  175. package/adapters/gemini/references/skill-frontmatter.schema.json +17 -0
  176. package/adapters/gemini/references/stack-resolution.md +51 -0
  177. package/adapters/gemini/references/stacks/_generic.md +90 -0
  178. package/adapters/gemini/references/stacks/go.md +138 -0
  179. package/adapters/gemini/references/stacks/python.md +163 -0
  180. package/adapters/gemini/references/stacks/rust.md +151 -0
  181. package/adapters/gemini/references/stacks/typescript.md +111 -0
  182. package/adapters/gemini/references/vendor-construct-inventory.md +49 -0
  183. package/adapters/gemini/scripts/forge-root.sh +50 -0
  184. package/adapters/gemini/skills/forge/forge.md +164 -0
  185. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +302 -0
  186. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +222 -0
  187. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  188. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +120 -0
  189. package/adapters/gemini/skills/forge-1-prd/references/prd-template.md +106 -0
  190. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +197 -0
  191. package/adapters/gemini/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  192. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +153 -0
  193. package/adapters/gemini/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  194. package/adapters/gemini/skills/forge-3-specs/references/spec-examples.md +71 -0
  195. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +145 -0
  196. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +302 -0
  197. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +63 -0
  198. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +214 -0
  199. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +178 -0
  200. package/adapters/gemini/skills/forge-6-docs/references/doc-conventions.md +126 -0
  201. package/adapters/gemini/skills/forge-fix/forge-fix.md +64 -0
  202. package/adapters/gemini/skills/forge-init/forge-init.md +29 -0
  203. package/adapters/gemini/skills/forge-verify/forge-verify.md +218 -0
  204. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +379 -0
  205. package/dist/agent-targets.d.ts +70 -0
  206. package/dist/agent-targets.js +111 -0
  207. package/dist/apply.d.ts +49 -0
  208. package/dist/apply.js +246 -0
  209. package/dist/cli.d.ts +94 -0
  210. package/dist/cli.js +508 -0
  211. package/dist/detect.d.ts +45 -0
  212. package/dist/detect.js +72 -0
  213. package/dist/fsutil.d.ts +56 -0
  214. package/dist/fsutil.js +175 -0
  215. package/dist/hash.d.ts +50 -0
  216. package/dist/hash.js +107 -0
  217. package/dist/index.d.ts +8 -0
  218. package/dist/index.js +9 -0
  219. package/dist/manifest.d.ts +72 -0
  220. package/dist/manifest.js +222 -0
  221. package/dist/plan.d.ts +66 -0
  222. package/dist/plan.js +166 -0
  223. package/dist/rauf.d.ts +83 -0
  224. package/dist/rauf.js +118 -0
  225. package/dist/report.d.ts +35 -0
  226. package/dist/report.js +110 -0
  227. package/dist/source.d.ts +69 -0
  228. package/dist/source.js +164 -0
  229. package/dist/types.d.ts +264 -0
  230. package/dist/types.js +57 -0
  231. package/package.json +42 -0
@@ -0,0 +1,56 @@
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
+ export declare function resolveWithin(root: string, ...segs: string[]): Result<string>;
15
+ /**
16
+ * Recursively copy `src` → `dest` (copy mode). The CALLER is responsible for having
17
+ * containment-checked `dest` via resolveWithin first (REQ-SEC-02).
18
+ *
19
+ * @returns ok(undefined) on success; err(WRITE_DENIED) on EACCES/EPERM, naming `dest`.
20
+ */
21
+ export declare function copyDir(src: string, dest: string): Promise<Result<void>>;
22
+ /**
23
+ * Link the whole namespace dir `linkPath` → `target` (REQ-FLAG-03). On Windows, OR if the symlink
24
+ * syscall fails for any reason, FALL BACK to copyDir and report it via the `mode` so apply records
25
+ * the truthful mode.
26
+ *
27
+ * @returns ok({ mode }) where mode is "symlink" (link created) or "copy" (fallback fired);
28
+ * err(WRITE_DENIED) if even the copy fallback fails.
29
+ */
30
+ export declare function symlinkDir(target: string, linkPath: string): Promise<Result<{
31
+ mode: "symlink" | "copy";
32
+ }>>;
33
+ /**
34
+ * Remove `p` safely (REQ-SEC-03/REQ-SAFE-02). NEVER follows a symlink to delete its target — uses
35
+ * `lstat` (which does not dereference) to distinguish a symbolic link (unlink the link only), a real
36
+ * directory (recursive rm), and a real file (unlink). ENOENT → ok (idempotent removal).
37
+ *
38
+ * The CALLER must have containment-checked `p` via resolveWithin first (REQ-SEC-02).
39
+ *
40
+ * @returns ok(undefined) on success or already-absent; err(WRITE_DENIED) on EACCES/EPERM.
41
+ */
42
+ export declare function removePath(p: string): Promise<Result<void>>;
43
+ /**
44
+ * Prune now-empty directories from `startDir` UPWARD, stopping before `stopRoot` (exclusive). Used by
45
+ * `apply`'s copy-mode uninstall (§5.3) after the recorded files are removed. NEVER removes a
46
+ * non-empty dir nor `stopRoot` itself (REQ-SAFE-01). A non-existent `cur` is treated as
47
+ * already-removed and the ascent continues.
48
+ *
49
+ * @param startDir - the deepest dir to consider pruning (e.g. the namespace dir).
50
+ * @param stopRoot - the boundary; pruning never removes `stopRoot` itself nor anything outside it.
51
+ * @returns ok(undefined) when the upward prune completes (or halts on a non-empty dir);
52
+ * err(PATH_ESCAPE) if `startDir` is not within `stopRoot`; err(WRITE_DENIED) on EACCES/EPERM.
53
+ */
54
+ export declare function removeEmptyDirsWithin(startDir: string, stopRoot: string): Promise<Result<void>>;
55
+ /** Platform check (REQ-FLAG-03, C-6). Centralized so resolveMode/symlinkDir share one decision. */
56
+ export declare function isWindows(): boolean;
package/dist/fsutil.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Sandboxed filesystem primitives (spec 04 §7). Every filesystem mutation in `apply` routes through
3
+ * these — they are the single place REQ-SEC-01/02/03 are enforced. Cross-platform: node:fs/promises,
4
+ * node:path, node:os only (no shelling out). No throw for expected errors — all return `Result`.
5
+ */
6
+ import * as fsp from "node:fs/promises";
7
+ import * as path from "node:path";
8
+ import * as os from "node:os";
9
+ import { ok, err } from "./types.js";
10
+ /**
11
+ * Resolve `segs` against `root` and assert the result lies WITHIN `root` (REQ-SEC-02). Returns the
12
+ * resolved absolute path on success, or a PATH_ESCAPE InstallerError if a `..` segment or a
13
+ * malformed agent id would escape the agent config root. MUST be called before ANY write/delete.
14
+ *
15
+ * Uses `path.relative` + the `..` prefix test (the robust boundary check — a bare string startsWith
16
+ * would false-pass `/root-evil`).
17
+ *
18
+ * @param root - the containment boundary (the agent config root, e.g. <home>/.claude)
19
+ * @param segs - path segments to join under root (destination, then a bundle-relative path)
20
+ * @returns ok(absolutePath) if inside; err(PATH_ESCAPE) otherwise.
21
+ */
22
+ export function resolveWithin(root, ...segs) {
23
+ const base = path.resolve(root);
24
+ const target = path.resolve(base, ...segs);
25
+ const rel = path.relative(base, target);
26
+ const inside = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
27
+ if (!inside) {
28
+ return err({
29
+ code: "PATH_ESCAPE",
30
+ message: `refusing to write outside the agent config root: resolved "${target}" escapes "${base}"`,
31
+ path: target,
32
+ remedy: "this indicates a malformed agent id or path segment; report it as a bug",
33
+ });
34
+ }
35
+ return ok(target);
36
+ }
37
+ /**
38
+ * Recursively copy `src` → `dest` (copy mode). The CALLER is responsible for having
39
+ * containment-checked `dest` via resolveWithin first (REQ-SEC-02).
40
+ *
41
+ * @returns ok(undefined) on success; err(WRITE_DENIED) on EACCES/EPERM, naming `dest`.
42
+ */
43
+ export async function copyDir(src, dest) {
44
+ try {
45
+ await fsp.mkdir(path.dirname(dest), { recursive: true });
46
+ await fsp.cp(src, dest, { recursive: true, force: true, dereference: false });
47
+ return ok(undefined);
48
+ }
49
+ catch (e) {
50
+ return err(toWriteError(e, dest));
51
+ }
52
+ }
53
+ /**
54
+ * Link the whole namespace dir `linkPath` → `target` (REQ-FLAG-03). On Windows, OR if the symlink
55
+ * syscall fails for any reason, FALL BACK to copyDir and report it via the `mode` so apply records
56
+ * the truthful mode.
57
+ *
58
+ * @returns ok({ mode }) where mode is "symlink" (link created) or "copy" (fallback fired);
59
+ * err(WRITE_DENIED) if even the copy fallback fails.
60
+ */
61
+ export async function symlinkDir(target, linkPath) {
62
+ if (isWindows()) {
63
+ const copied = await copyDir(target, linkPath);
64
+ return copied.ok ? ok({ mode: "copy" }) : err(copied.error);
65
+ }
66
+ try {
67
+ await fsp.mkdir(path.dirname(linkPath), { recursive: true });
68
+ await fsp.symlink(target, linkPath, "dir");
69
+ return ok({ mode: "symlink" });
70
+ }
71
+ catch {
72
+ const copied = await copyDir(target, linkPath);
73
+ return copied.ok ? ok({ mode: "copy" }) : err(copied.error);
74
+ }
75
+ }
76
+ /**
77
+ * Remove `p` safely (REQ-SEC-03/REQ-SAFE-02). NEVER follows a symlink to delete its target — uses
78
+ * `lstat` (which does not dereference) to distinguish a symbolic link (unlink the link only), a real
79
+ * directory (recursive rm), and a real file (unlink). ENOENT → ok (idempotent removal).
80
+ *
81
+ * The CALLER must have containment-checked `p` via resolveWithin first (REQ-SEC-02).
82
+ *
83
+ * @returns ok(undefined) on success or already-absent; err(WRITE_DENIED) on EACCES/EPERM.
84
+ */
85
+ export async function removePath(p) {
86
+ let st;
87
+ try {
88
+ st = await fsp.lstat(p); // lstat: does NOT dereference the link
89
+ }
90
+ catch (e) {
91
+ if (e.code === "ENOENT")
92
+ return ok(undefined);
93
+ return err(toWriteError(e, p));
94
+ }
95
+ try {
96
+ if (st.isSymbolicLink()) {
97
+ await fsp.unlink(p); // remove the LINK only — never the target (REQ-SAFE-02)
98
+ }
99
+ else if (st.isDirectory()) {
100
+ await fsp.rm(p, { recursive: true, force: true });
101
+ }
102
+ else {
103
+ await fsp.unlink(p);
104
+ }
105
+ return ok(undefined);
106
+ }
107
+ catch (e) {
108
+ return err(toWriteError(e, p));
109
+ }
110
+ }
111
+ /**
112
+ * Prune now-empty directories from `startDir` UPWARD, stopping before `stopRoot` (exclusive). Used by
113
+ * `apply`'s copy-mode uninstall (§5.3) after the recorded files are removed. NEVER removes a
114
+ * non-empty dir nor `stopRoot` itself (REQ-SAFE-01). A non-existent `cur` is treated as
115
+ * already-removed and the ascent continues.
116
+ *
117
+ * @param startDir - the deepest dir to consider pruning (e.g. the namespace dir).
118
+ * @param stopRoot - the boundary; pruning never removes `stopRoot` itself nor anything outside it.
119
+ * @returns ok(undefined) when the upward prune completes (or halts on a non-empty dir);
120
+ * err(PATH_ESCAPE) if `startDir` is not within `stopRoot`; err(WRITE_DENIED) on EACCES/EPERM.
121
+ */
122
+ export async function removeEmptyDirsWithin(startDir, stopRoot) {
123
+ const contained = resolveWithin(stopRoot, startDir);
124
+ if (!contained.ok)
125
+ return contained;
126
+ const stop = path.resolve(stopRoot);
127
+ let cur = contained.value;
128
+ while (cur !== stop && cur.startsWith(stop)) {
129
+ let entries;
130
+ try {
131
+ entries = await fsp.readdir(cur);
132
+ }
133
+ catch (e) {
134
+ if (e.code === "ENOENT") {
135
+ cur = path.dirname(cur);
136
+ continue;
137
+ }
138
+ return err(toWriteError(e, cur));
139
+ }
140
+ if (entries.length > 0)
141
+ break; // non-empty ⇒ MUST NOT remove (REQ-SAFE-01) — halt
142
+ try {
143
+ await fsp.rmdir(cur); // remove this now-empty dir only
144
+ }
145
+ catch (e) {
146
+ return err(toWriteError(e, cur));
147
+ }
148
+ cur = path.dirname(cur); // ascend
149
+ }
150
+ return ok(undefined);
151
+ }
152
+ /** Platform check (REQ-FLAG-03, C-6). Centralized so resolveMode/symlinkDir share one decision. */
153
+ export function isWindows() {
154
+ return os.platform() === "win32";
155
+ }
156
+ /**
157
+ * Map a caught fs exception to an actionable InstallerError (REQ-OBS-02). EACCES/EPERM → WRITE_DENIED;
158
+ * anything else → UNEXPECTED carrying the message. Internal helper (not exported as public surface).
159
+ */
160
+ function toWriteError(e, p) {
161
+ const code = e?.code;
162
+ if (code === "EACCES" || code === "EPERM") {
163
+ return {
164
+ code: "WRITE_DENIED",
165
+ message: `no write permission to ${p}`,
166
+ path: p,
167
+ remedy: "check directory permissions, or choose a different scope (--global vs project)",
168
+ };
169
+ }
170
+ return {
171
+ code: "UNEXPECTED",
172
+ message: `filesystem error at ${p}: ${e?.message ?? String(e)}`,
173
+ path: p,
174
+ };
175
+ }
package/dist/hash.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Content hashing for the cross-agent installer (spec 03 §3.3-§3.6, OQ-4).
3
+ *
4
+ * Drift detection is decided by SHA-256 *content* hashing, NEVER mtime. The tree digest is a
5
+ * function of the set of `{ relativePosixPath, fileContentHash }` pairs only, so two
6
+ * materializations of the same bundle at different paths/times hash identically. Zero runtime
7
+ * dependencies — only `node:` built-ins.
8
+ */
9
+ /**
10
+ * SHA-256 of a single file's bytes, hex-encoded (OQ-4 — content hash, never mtime).
11
+ *
12
+ * @param filePath - Absolute path to a regular file.
13
+ * @returns 64-char lowercase hex digest of the file's bytes.
14
+ * @throws Propagates the underlying node:fs error (ENOENT/EACCES) — an *unexpected* IO failure
15
+ * for an already-located, integrity-checked bundle, caught at the operation boundary.
16
+ */
17
+ export declare function sha256File(filePath: string): string;
18
+ /**
19
+ * Deterministic SHA-256 over a directory tree's file set (OQ-4). The digest is a function of the
20
+ * set of `{ relativePosixPath, fileContentHash }` pairs ONLY — never of mtime, inode, or
21
+ * traversal order.
22
+ *
23
+ * Canonical form: walk regular files, compute POSIX-relative paths, sort byte-wise, then fold a
24
+ * single hash over `update(rel); update("\0"); update(contentHash); update("\n")` per file.
25
+ *
26
+ * @param dir - Absolute path to the directory whose tree to hash.
27
+ * @returns 64-char lowercase hex digest, invariant under relocation and traversal order.
28
+ */
29
+ export declare function sha256Tree(dir: string): string;
30
+ /**
31
+ * Compute the `sourceHash` stored in InstallManifest (spec 00 §3, spec 03 §3.4). It is exactly
32
+ * `sha256Tree(bundlePath)` — the sorted-path canonical digest over the bundle's file set, so two
33
+ * materializations of the same bundle produce the SAME hash (REQ-IDEM-01 basis).
34
+ *
35
+ * @param bundlePath - Absolute path to a *located, integrity-checked* bundle dir.
36
+ * @returns 64-char lowercase hex digest — store verbatim in `InstallManifest.sourceHash`.
37
+ */
38
+ export declare function computeSourceHash(bundlePath: string): string;
39
+ /**
40
+ * The bundle's per-file inventory: every regular file under `bundlePath`, each as its
41
+ * bundle-relative POSIX path plus the content `sha256`. Sorted by relative POSIX path — the SAME
42
+ * sorted walk `sha256Tree` folds over, so the inventory and `sourceHash` always agree.
43
+ *
44
+ * @param bundlePath - Absolute path to a located, integrity-checked bundle dir.
45
+ * @returns Array of `{ relpath, sha256 }`, sorted by `relpath` (POSIX `/` separators).
46
+ */
47
+ export declare function listBundleFiles(bundlePath: string): Array<{
48
+ relpath: string;
49
+ sha256: string;
50
+ }>;
package/dist/hash.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Content hashing for the cross-agent installer (spec 03 §3.3-§3.6, OQ-4).
3
+ *
4
+ * Drift detection is decided by SHA-256 *content* hashing, NEVER mtime. The tree digest is a
5
+ * function of the set of `{ relativePosixPath, fileContentHash }` pairs only, so two
6
+ * materializations of the same bundle at different paths/times hash identically. Zero runtime
7
+ * dependencies — only `node:` built-ins.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ /**
13
+ * SHA-256 of a single file's bytes, hex-encoded (OQ-4 — content hash, never mtime).
14
+ *
15
+ * @param filePath - Absolute path to a regular file.
16
+ * @returns 64-char lowercase hex digest of the file's bytes.
17
+ * @throws Propagates the underlying node:fs error (ENOENT/EACCES) — an *unexpected* IO failure
18
+ * for an already-located, integrity-checked bundle, caught at the operation boundary.
19
+ */
20
+ export function sha256File(filePath) {
21
+ const buf = fs.readFileSync(filePath);
22
+ return createHash("sha256").update(buf).digest("hex");
23
+ }
24
+ /**
25
+ * Deterministic SHA-256 over a directory tree's file set (OQ-4). The digest is a function of the
26
+ * set of `{ relativePosixPath, fileContentHash }` pairs ONLY — never of mtime, inode, or
27
+ * traversal order.
28
+ *
29
+ * Canonical form: walk regular files, compute POSIX-relative paths, sort byte-wise, then fold a
30
+ * single hash over `update(rel); update("\0"); update(contentHash); update("\n")` per file.
31
+ *
32
+ * @param dir - Absolute path to the directory whose tree to hash.
33
+ * @returns 64-char lowercase hex digest, invariant under relocation and traversal order.
34
+ */
35
+ export function sha256Tree(dir) {
36
+ const files = walkFiles(dir);
37
+ const entries = files
38
+ .map((abs) => ({
39
+ rel: toPosix(path.relative(dir, abs)),
40
+ contentHash: sha256File(abs),
41
+ }))
42
+ .sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
43
+ const h = createHash("sha256");
44
+ for (const e of entries) {
45
+ h.update(e.rel);
46
+ h.update("\0");
47
+ h.update(e.contentHash);
48
+ h.update("\n");
49
+ }
50
+ return h.digest("hex");
51
+ }
52
+ /**
53
+ * Compute the `sourceHash` stored in InstallManifest (spec 00 §3, spec 03 §3.4). It is exactly
54
+ * `sha256Tree(bundlePath)` — the sorted-path canonical digest over the bundle's file set, so two
55
+ * materializations of the same bundle produce the SAME hash (REQ-IDEM-01 basis).
56
+ *
57
+ * @param bundlePath - Absolute path to a *located, integrity-checked* bundle dir.
58
+ * @returns 64-char lowercase hex digest — store verbatim in `InstallManifest.sourceHash`.
59
+ */
60
+ export function computeSourceHash(bundlePath) {
61
+ return sha256Tree(bundlePath);
62
+ }
63
+ /**
64
+ * The bundle's per-file inventory: every regular file under `bundlePath`, each as its
65
+ * bundle-relative POSIX path plus the content `sha256`. Sorted by relative POSIX path — the SAME
66
+ * sorted walk `sha256Tree` folds over, so the inventory and `sourceHash` always agree.
67
+ *
68
+ * @param bundlePath - Absolute path to a located, integrity-checked bundle dir.
69
+ * @returns Array of `{ relpath, sha256 }`, sorted by `relpath` (POSIX `/` separators).
70
+ */
71
+ export function listBundleFiles(bundlePath) {
72
+ const files = walkFiles(bundlePath);
73
+ return files
74
+ .map((abs) => ({
75
+ relpath: toPosix(path.relative(bundlePath, abs)),
76
+ sha256: sha256File(abs),
77
+ }))
78
+ .sort((a, b) => (a.relpath < b.relpath ? -1 : a.relpath > b.relpath ? 1 : 0));
79
+ }
80
+ // ---------------------------------------------------------------------------
81
+ // Internal helpers (module-private — spec 03 §4.2)
82
+ // ---------------------------------------------------------------------------
83
+ /**
84
+ * Recursively collect every REGULAR FILE under `dir` (absolute paths). Directories contribute only
85
+ * via their files; symlink entries are NOT followed and NOT included. Order is unspecified —
86
+ * callers sort by relative path so traversal order never affects the digest.
87
+ */
88
+ function walkFiles(dir) {
89
+ const out = [];
90
+ const stack = [dir];
91
+ while (stack.length > 0) {
92
+ const cur = stack.pop();
93
+ for (const ent of fs.readdirSync(cur, { withFileTypes: true })) {
94
+ const abs = path.join(cur, ent.name);
95
+ if (ent.isDirectory())
96
+ stack.push(abs);
97
+ else if (ent.isFile())
98
+ out.push(abs);
99
+ // symlinks / sockets / fifos: ignored (not expected in a copied bundle)
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+ /** Normalize an OS-relative path to POSIX separators so hashes match across Windows and POSIX. */
105
+ function toPosix(rel) {
106
+ return rel.split(path.sep).join("/");
107
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Library barrel (spec 01 §4): the public surface of the cross-agent installer as a Node
3
+ * library. Re-exports the agent-detection-map surface, the rauf pin, and the shared spec-00
4
+ * types. Named exports only; no runtime logic of its own.
5
+ */
6
+ export { AGENT_TARGETS, resolveRoots, destinationFor, detectAgent, detectAgents, formatZeroDetection, } from "./agent-targets.js";
7
+ export { RAUF_PIN } from "./rauf.js";
8
+ export type { AgentId, AgentTarget, DetectionResult, ResolveOpts, Scope, Mode, InstallManifest, PlannedAction, RunReport, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Library barrel (spec 01 §4): the public surface of the cross-agent installer as a Node
3
+ * library. Re-exports the agent-detection-map surface, the rauf pin, and the shared spec-00
4
+ * types. Named exports only; no runtime logic of its own.
5
+ */
6
+ // The agent-detection-map surface (spec 02).
7
+ export { AGENT_TARGETS, resolveRoots, destinationFor, detectAgent, detectAgents, formatZeroDetection, } from "./agent-targets.js";
8
+ // The pinned default loop-runner coordinate (spec 06).
9
+ export { RAUF_PIN } from "./rauf.js";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The persisted install manifest (read/write/build) and the manifest-driven uninstall-exactness
3
+ * policy (spec 05). This module locates the hidden parent-sibling manifest, reads/validates and
4
+ * atomically writes it, builds an {@link InstallManifest} from an apply result, and owns the
5
+ * uninstall removal POLICY (`planUninstall`). The safe EXECUTION of that plan is `apply()` in
6
+ * spec 04 — there is no `applyUninstall` here.
7
+ *
8
+ * Zero runtime dependencies; only `node:` built-ins. Named exports only. Core functions return
9
+ * `Result<T, E>` and never throw for expected errors; `JSON.parse` is wrapped in `try/catch`.
10
+ */
11
+ import { type AgentId, type InstallManifest, type ManifestFile, type Mode, type PlannedAction, type ResolveOpts, type Result, type Scope } from "./types.js";
12
+ /**
13
+ * Inputs to {@link buildManifest}. The caller (apply.ts, spec 04) assembles this from the resolved
14
+ * detection target, the chosen scope/mode, and the apply result's per-file inventory.
15
+ */
16
+ export interface BuildManifestArgs {
17
+ readonly agent: AgentId;
18
+ readonly scope: Scope;
19
+ readonly mode: Mode;
20
+ /** Absolute path of the `feature-forge/` namespace dir this manifest governs. */
21
+ readonly destination: string;
22
+ /**
23
+ * Per-file inventory of what was written, paths relative to `destination`. In `"symlink"` mode
24
+ * this is `[]` (no per-file copy exists). In `"copy"` mode each entry carries its `sha256`.
25
+ */
26
+ readonly files: readonly ManifestFile[];
27
+ /** Installed skill ids (the bundle's `skills/*` dir names). */
28
+ readonly skills: readonly string[];
29
+ /** SHA-256 over the source bundle's canonical (sorted-path) file set — drift anchor (spec 03). */
30
+ readonly sourceHash: string;
31
+ /** Recorded pinned rauf coordinate (e.g. "rauf@0.6.0"); `null` when `--skip-rauf` (spec 06). */
32
+ readonly raufPin: string | null;
33
+ /** Symlink mode only: the source bundle the namespace dir links to (REQ-SAFE-02). */
34
+ readonly link?: {
35
+ readonly target: string;
36
+ };
37
+ /** Prior manifest, if any. When present, its `installedAt` is preserved (this is an update). */
38
+ readonly previous?: InstallManifest | null;
39
+ /** Injectable clock for deterministic tests. Default: `() => new Date()`. */
40
+ readonly now?: () => Date;
41
+ }
42
+ /**
43
+ * Assemble an {@link InstallManifest} from an apply result (REQ-SAFE-01/03). Pure — no I/O.
44
+ *
45
+ * Timestamp policy: `updatedAt` is always "now"; `installedAt` is `previous.installedAt` when
46
+ * reconciling an existing install, else "now". `featureForgeVersion` is always `null` today
47
+ * (OQ-A/IR-1; C-3 forbids synthesizing one).
48
+ */
49
+ export declare function buildManifest(args: BuildManifestArgs): InstallManifest;
50
+ /**
51
+ * Absolute path of the hidden parent-sibling manifest for an agent + scope (D6/D8):
52
+ * `<scopeRoot>/<configDirName>/<installSubdir>/.feature-forge.<scope>.json`
53
+ * e.g. `~/.claude/skills/.feature-forge.global.json`. Identical for copy and symlink mode.
54
+ */
55
+ export declare function manifestPath(agent: AgentId, scope: Scope, opts?: Omit<ResolveOpts, "scope">): string;
56
+ /**
57
+ * Read and validate the manifest at `p`. Absent (`ENOENT`) → `ok(null)`; present + valid →
58
+ * `ok(manifest)`; unreadable / invalid JSON / failed shape validation → `err(MANIFEST_CORRUPT)`.
59
+ */
60
+ export declare function readManifest(p: string): Result<InstallManifest | null>;
61
+ /**
62
+ * Atomically write the manifest to `p` (write `<p>.tmp` → `rename`). Creates the parent dir if
63
+ * missing. Returns `err(WRITE_DENIED)` on a permission failure, cleaning up the temp file.
64
+ */
65
+ export declare function writeManifest(p: string, m: InstallManifest): Result<void>;
66
+ /**
67
+ * Compute the uninstall plan from a manifest (REQ-OPS-03, REQ-SAFE-01/02). PURE — no I/O,
68
+ * manifest only. Returns an all-`"remove"` {@link PlannedAction}: copy mode one
69
+ * `{ relpath, action: "remove" }` per `manifest.files[].path` in recorded order; symlink mode the
70
+ * single `{ relpath: ".", action: "remove" }`. The safe EXECUTION is `apply()` in spec 04.
71
+ */
72
+ export declare function planUninstall(manifest: InstallManifest): Result<PlannedAction>;