@garygentry/feature-forge 0.1.4 → 0.2.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 (322) hide show
  1. package/README.md +19 -1
  2. package/adapters/GENERATION-REPORT.md +17 -13
  3. package/adapters/claude/.feature-forge-bundle.json +6 -0
  4. package/adapters/claude/references/forge-config-schema.json +36 -10
  5. package/adapters/claude/references/pipeline-state-schema.json +4 -0
  6. package/adapters/claude/references/portable-root.md +8 -5
  7. package/adapters/claude/references/process-overview.md +15 -5
  8. package/adapters/claude/references/shared-conventions.md +69 -4
  9. package/adapters/claude/references/stack-resolution.md +4 -1
  10. package/adapters/claude/references/stacks/go.md +1 -1
  11. package/adapters/claude/references/stacks/python.md +1 -1
  12. package/adapters/claude/references/stacks/rust.md +1 -1
  13. package/adapters/claude/references/stacks/typescript.md +1 -1
  14. package/adapters/claude/references/templates/specs-hygiene/AGENTS.md +23 -0
  15. package/adapters/claude/references/templates/specs-hygiene/CLAUDE.md +22 -0
  16. package/adapters/claude/scripts/epic-manifest.py +1379 -0
  17. package/adapters/claude/scripts/forge-bootstrap.py +991 -0
  18. package/adapters/claude/scripts/forge-init.sh +44 -0
  19. package/adapters/claude/scripts/forge-root.sh +30 -8
  20. package/adapters/claude/scripts/validate-traceability.py +150 -0
  21. package/adapters/claude/skills/forge/SKILL.md +5 -5
  22. package/adapters/claude/skills/forge-0-epic/SKILL.md +13 -15
  23. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +2 -2
  24. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  25. package/adapters/claude/skills/forge-1-prd/SKILL.md +6 -4
  26. package/adapters/claude/skills/forge-2-tech/SKILL.md +8 -7
  27. package/adapters/claude/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  28. package/adapters/claude/skills/forge-3-specs/SKILL.md +1 -1
  29. package/adapters/claude/skills/forge-4-backlog/SKILL.md +2 -2
  30. package/adapters/claude/skills/forge-5-loop/SKILL.md +20 -18
  31. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +13 -0
  32. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +40 -0
  33. package/adapters/claude/skills/forge-6-docs/SKILL.md +11 -1
  34. package/adapters/claude/skills/forge-bootstrap/SKILL.md +240 -0
  35. package/adapters/claude/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
  36. package/adapters/claude/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
  37. package/adapters/claude/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
  38. package/adapters/claude/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
  39. package/adapters/claude/skills/forge-bootstrap/references/templates/go/main.go +12 -0
  40. package/adapters/claude/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
  41. package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
  42. package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
  43. package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
  44. package/adapters/claude/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
  45. package/adapters/claude/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
  46. package/adapters/claude/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
  47. package/adapters/claude/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
  48. package/adapters/claude/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
  49. package/adapters/claude/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
  50. package/adapters/claude/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
  51. package/adapters/claude/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
  52. package/adapters/claude/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
  53. package/adapters/claude/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
  54. package/adapters/claude/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
  55. package/adapters/claude/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
  56. package/adapters/claude/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
  57. package/adapters/claude/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
  58. package/adapters/claude/skills/forge-fix/SKILL.md +1 -1
  59. package/adapters/claude/skills/forge-init/SKILL.md +1 -1
  60. package/adapters/claude/skills/forge-verify/SKILL.md +7 -2
  61. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
  62. package/adapters/codex/.feature-forge-bundle.json +6 -0
  63. package/adapters/codex/agents/{forge-researcher.md → forge-researcher.toml} +4 -4
  64. package/adapters/codex/agents/{forge-spec-writer.md → forge-spec-writer.toml} +4 -4
  65. package/adapters/codex/agents/{forge-verifier.md → forge-verifier.toml} +4 -4
  66. package/adapters/codex/references/forge-config-schema.json +36 -10
  67. package/adapters/codex/references/pipeline-state-schema.json +4 -0
  68. package/adapters/codex/references/portable-root.md +8 -5
  69. package/adapters/codex/references/process-overview.md +15 -5
  70. package/adapters/codex/references/shared-conventions.md +69 -4
  71. package/adapters/codex/references/stack-resolution.md +4 -1
  72. package/adapters/codex/references/stacks/go.md +1 -1
  73. package/adapters/codex/references/stacks/python.md +1 -1
  74. package/adapters/codex/references/stacks/rust.md +1 -1
  75. package/adapters/codex/references/stacks/typescript.md +1 -1
  76. package/adapters/codex/references/templates/specs-hygiene/AGENTS.md +23 -0
  77. package/adapters/codex/references/templates/specs-hygiene/CLAUDE.md +22 -0
  78. package/adapters/codex/scripts/epic-manifest.py +1379 -0
  79. package/adapters/codex/scripts/forge-bootstrap.py +991 -0
  80. package/adapters/codex/scripts/forge-init.sh +44 -0
  81. package/adapters/codex/scripts/forge-root.sh +30 -8
  82. package/adapters/codex/scripts/validate-traceability.py +150 -0
  83. package/adapters/codex/skills/forge/{forge.md → SKILL.md} +16 -6
  84. package/adapters/codex/skills/forge-0-epic/{forge-0-epic.md → SKILL.md} +33 -25
  85. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +2 -2
  86. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  87. package/adapters/codex/skills/forge-1-prd/{forge-1-prd.md → SKILL.md} +22 -10
  88. package/adapters/codex/skills/forge-2-tech/{forge-2-tech.md → SKILL.md} +26 -15
  89. package/adapters/codex/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  90. package/adapters/codex/skills/forge-3-specs/{forge-3-specs.md → SKILL.md} +16 -6
  91. package/adapters/codex/skills/forge-4-backlog/{forge-4-backlog.md → SKILL.md} +15 -5
  92. package/adapters/codex/skills/forge-5-loop/{forge-5-loop.md → SKILL.md} +40 -28
  93. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +13 -0
  94. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +40 -0
  95. package/adapters/codex/skills/forge-6-docs/{forge-6-docs.md → SKILL.md} +26 -6
  96. package/adapters/codex/skills/forge-bootstrap/SKILL.md +249 -0
  97. package/adapters/codex/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
  98. package/adapters/codex/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
  99. package/adapters/codex/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
  100. package/adapters/codex/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
  101. package/adapters/codex/skills/forge-bootstrap/references/templates/go/main.go +12 -0
  102. package/adapters/codex/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
  103. package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
  104. package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
  105. package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
  106. package/adapters/codex/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
  107. package/adapters/codex/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
  108. package/adapters/codex/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
  109. package/adapters/codex/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
  110. package/adapters/codex/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
  111. package/adapters/codex/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
  112. package/adapters/codex/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
  113. package/adapters/codex/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
  114. package/adapters/codex/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
  115. package/adapters/codex/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
  116. package/adapters/codex/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
  117. package/adapters/codex/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
  118. package/adapters/codex/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
  119. package/adapters/codex/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
  120. package/adapters/codex/skills/forge-fix/{forge-fix.md → SKILL.md} +12 -2
  121. package/adapters/codex/skills/forge-init/{forge-init.md → SKILL.md} +11 -1
  122. package/adapters/codex/skills/forge-verify/{forge-verify.md → SKILL.md} +24 -9
  123. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
  124. package/adapters/copilot/.feature-forge-bundle.json +6 -0
  125. package/adapters/copilot/references/forge-config-schema.json +36 -10
  126. package/adapters/copilot/references/pipeline-state-schema.json +4 -0
  127. package/adapters/copilot/references/portable-root.md +8 -5
  128. package/adapters/copilot/references/process-overview.md +15 -5
  129. package/adapters/copilot/references/shared-conventions.md +69 -4
  130. package/adapters/copilot/references/stack-resolution.md +4 -1
  131. package/adapters/copilot/references/stacks/go.md +1 -1
  132. package/adapters/copilot/references/stacks/python.md +1 -1
  133. package/adapters/copilot/references/stacks/rust.md +1 -1
  134. package/adapters/copilot/references/stacks/typescript.md +1 -1
  135. package/adapters/copilot/references/templates/specs-hygiene/AGENTS.md +23 -0
  136. package/adapters/copilot/references/templates/specs-hygiene/CLAUDE.md +22 -0
  137. package/adapters/copilot/scripts/epic-manifest.py +1379 -0
  138. package/adapters/copilot/scripts/forge-bootstrap.py +991 -0
  139. package/adapters/copilot/scripts/forge-init.sh +44 -0
  140. package/adapters/copilot/scripts/forge-root.sh +30 -8
  141. package/adapters/copilot/scripts/validate-traceability.py +150 -0
  142. package/adapters/copilot/skills/forge/forge.md +16 -6
  143. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +33 -25
  144. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +2 -2
  145. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  146. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +22 -10
  147. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +26 -15
  148. package/adapters/copilot/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  149. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +16 -6
  150. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +15 -5
  151. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +40 -28
  152. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +13 -0
  153. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +40 -0
  154. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +26 -6
  155. package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +249 -0
  156. package/adapters/copilot/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
  157. package/adapters/copilot/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
  158. package/adapters/copilot/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
  159. package/adapters/copilot/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
  160. package/adapters/copilot/skills/forge-bootstrap/references/templates/go/main.go +12 -0
  161. package/adapters/copilot/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
  162. package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
  163. package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
  164. package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
  165. package/adapters/copilot/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
  166. package/adapters/copilot/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
  167. package/adapters/copilot/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
  168. package/adapters/copilot/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
  169. package/adapters/copilot/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
  170. package/adapters/copilot/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
  171. package/adapters/copilot/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
  172. package/adapters/copilot/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
  173. package/adapters/copilot/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
  174. package/adapters/copilot/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
  175. package/adapters/copilot/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
  176. package/adapters/copilot/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
  177. package/adapters/copilot/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
  178. package/adapters/copilot/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
  179. package/adapters/copilot/skills/forge-fix/forge-fix.md +12 -2
  180. package/adapters/copilot/skills/forge-init/forge-init.md +11 -1
  181. package/adapters/copilot/skills/forge-verify/forge-verify.md +24 -9
  182. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
  183. package/adapters/cursor/.feature-forge-bundle.json +6 -0
  184. package/adapters/cursor/references/forge-config-schema.json +36 -10
  185. package/adapters/cursor/references/pipeline-state-schema.json +4 -0
  186. package/adapters/cursor/references/portable-root.md +8 -5
  187. package/adapters/cursor/references/process-overview.md +15 -5
  188. package/adapters/cursor/references/shared-conventions.md +69 -4
  189. package/adapters/cursor/references/stack-resolution.md +4 -1
  190. package/adapters/cursor/references/stacks/go.md +1 -1
  191. package/adapters/cursor/references/stacks/python.md +1 -1
  192. package/adapters/cursor/references/stacks/rust.md +1 -1
  193. package/adapters/cursor/references/stacks/typescript.md +1 -1
  194. package/adapters/cursor/references/templates/specs-hygiene/AGENTS.md +23 -0
  195. package/adapters/cursor/references/templates/specs-hygiene/CLAUDE.md +22 -0
  196. package/adapters/cursor/scripts/epic-manifest.py +1379 -0
  197. package/adapters/cursor/scripts/forge-bootstrap.py +991 -0
  198. package/adapters/cursor/scripts/forge-init.sh +44 -0
  199. package/adapters/cursor/scripts/forge-root.sh +30 -8
  200. package/adapters/cursor/scripts/validate-traceability.py +150 -0
  201. package/adapters/cursor/skills/forge/forge.mdc +16 -6
  202. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +33 -25
  203. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +2 -2
  204. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  205. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +22 -10
  206. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +26 -15
  207. package/adapters/cursor/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  208. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +16 -6
  209. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +15 -5
  210. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +40 -28
  211. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +13 -0
  212. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +40 -0
  213. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +26 -6
  214. package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +250 -0
  215. package/adapters/cursor/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
  216. package/adapters/cursor/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
  217. package/adapters/cursor/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
  218. package/adapters/cursor/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
  219. package/adapters/cursor/skills/forge-bootstrap/references/templates/go/main.go +12 -0
  220. package/adapters/cursor/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
  221. package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
  222. package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
  223. package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
  224. package/adapters/cursor/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
  225. package/adapters/cursor/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
  226. package/adapters/cursor/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
  227. package/adapters/cursor/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
  228. package/adapters/cursor/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
  229. package/adapters/cursor/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
  230. package/adapters/cursor/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
  231. package/adapters/cursor/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
  232. package/adapters/cursor/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
  233. package/adapters/cursor/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
  234. package/adapters/cursor/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
  235. package/adapters/cursor/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
  236. package/adapters/cursor/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
  237. package/adapters/cursor/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
  238. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +12 -2
  239. package/adapters/cursor/skills/forge-init/forge-init.mdc +11 -1
  240. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +24 -9
  241. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
  242. package/adapters/gemini/.feature-forge-bundle.json +6 -0
  243. package/adapters/gemini/gemini-extension.json +5 -1
  244. package/adapters/gemini/references/forge-config-schema.json +36 -10
  245. package/adapters/gemini/references/pipeline-state-schema.json +4 -0
  246. package/adapters/gemini/references/portable-root.md +8 -5
  247. package/adapters/gemini/references/process-overview.md +15 -5
  248. package/adapters/gemini/references/shared-conventions.md +69 -4
  249. package/adapters/gemini/references/stack-resolution.md +4 -1
  250. package/adapters/gemini/references/stacks/go.md +1 -1
  251. package/adapters/gemini/references/stacks/python.md +1 -1
  252. package/adapters/gemini/references/stacks/rust.md +1 -1
  253. package/adapters/gemini/references/stacks/typescript.md +1 -1
  254. package/adapters/gemini/references/templates/specs-hygiene/AGENTS.md +23 -0
  255. package/adapters/gemini/references/templates/specs-hygiene/CLAUDE.md +22 -0
  256. package/adapters/gemini/scripts/epic-manifest.py +1379 -0
  257. package/adapters/gemini/scripts/forge-bootstrap.py +991 -0
  258. package/adapters/gemini/scripts/forge-init.sh +44 -0
  259. package/adapters/gemini/scripts/forge-root.sh +30 -8
  260. package/adapters/gemini/scripts/validate-traceability.py +150 -0
  261. package/adapters/gemini/skills/forge/forge.md +16 -6
  262. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +33 -25
  263. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +2 -2
  264. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
  265. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +22 -10
  266. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +26 -15
  267. package/adapters/gemini/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
  268. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +16 -6
  269. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +15 -5
  270. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +40 -28
  271. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +13 -0
  272. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +40 -0
  273. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +26 -6
  274. package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +249 -0
  275. package/adapters/gemini/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
  276. package/adapters/gemini/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
  277. package/adapters/gemini/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
  278. package/adapters/gemini/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
  279. package/adapters/gemini/skills/forge-bootstrap/references/templates/go/main.go +12 -0
  280. package/adapters/gemini/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
  281. package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
  282. package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
  283. package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
  284. package/adapters/gemini/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
  285. package/adapters/gemini/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
  286. package/adapters/gemini/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
  287. package/adapters/gemini/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
  288. package/adapters/gemini/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
  289. package/adapters/gemini/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
  290. package/adapters/gemini/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
  291. package/adapters/gemini/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
  292. package/adapters/gemini/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
  293. package/adapters/gemini/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
  294. package/adapters/gemini/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
  295. package/adapters/gemini/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
  296. package/adapters/gemini/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
  297. package/adapters/gemini/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
  298. package/adapters/gemini/skills/forge-fix/forge-fix.md +12 -2
  299. package/adapters/gemini/skills/forge-init/forge-init.md +11 -1
  300. package/adapters/gemini/skills/forge-verify/forge-verify.md +24 -9
  301. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
  302. package/dist/agent-targets.d.ts +20 -4
  303. package/dist/agent-targets.js +29 -4
  304. package/dist/apply.js +245 -18
  305. package/dist/cli.js +12 -6
  306. package/dist/hash.d.ts +5 -0
  307. package/dist/hash.js +7 -0
  308. package/dist/manifest.d.ts +4 -2
  309. package/dist/manifest.js +58 -2
  310. package/dist/placements.d.ts +69 -0
  311. package/dist/placements.js +116 -0
  312. package/dist/plan.d.ts +7 -0
  313. package/dist/plan.js +87 -1
  314. package/dist/rauf.d.ts +4 -4
  315. package/dist/rauf.js +3 -3
  316. package/dist/report.js +21 -0
  317. package/dist/source.d.ts +4 -3
  318. package/dist/source.js +4 -3
  319. package/dist/types.d.ts +163 -19
  320. package/dist/types.js +42 -11
  321. package/package.json +1 -1
  322. package/adapters/codex/agents/openai.yaml +0 -10
package/dist/apply.js CHANGED
@@ -12,11 +12,13 @@
12
12
  * Zero runtime dependencies; only `node:` built-ins.
13
13
  */
14
14
  import * as fsp from "node:fs/promises";
15
+ import * as fs from "node:fs";
15
16
  import * as path from "node:path";
16
17
  import { ok, err } from "./types.js";
17
- import { sha256File } from "./hash.js";
18
+ import { sha256File, sha256String } from "./hash.js";
18
19
  import { resolveWithin, symlinkDir, removePath, removeEmptyDirsWithin, } from "./fsutil.js";
19
20
  import { buildManifest, writeManifest } from "./manifest.js";
21
+ import { wrapBlock, upsertBlock, removeBlock } from "./placements.js";
20
22
  /**
21
23
  * Execute one agent's `PlannedAction` against the filesystem, then write/delete the manifest.
22
24
  * Returns an `AgentReport` instead of throwing (REQ-OBS-03). See spec 04 §5.
@@ -84,8 +86,13 @@ async function applyCopyInstall(planned, ctx) {
84
86
  }
85
87
  }
86
88
  }
87
- // No-op short-circuit (REQ-IDEM-01): every action unchanged zero writes, manifest untouched.
88
- if (planned.files.every((f) => f.action === "unchanged")) {
89
+ // Secondary placements (A4b) execute regardless of the primary mode; collect their inventory.
90
+ const placementResult = await applyPlacements(planned.placements ?? [], ctx, source);
91
+ if (!placementResult.ok)
92
+ return fail(ctx, planned, placementResult.error);
93
+ // No-op short-circuit (REQ-IDEM-01): every action — primary AND placement — unchanged ⇒ zero
94
+ // writes, manifest untouched.
95
+ if (allUnchanged(planned)) {
89
96
  return success(ctx, planned);
90
97
  }
91
98
  const manifest = buildManifest({
@@ -97,6 +104,7 @@ async function applyCopyInstall(planned, ctx) {
97
104
  skills: source.skills,
98
105
  sourceHash: source.sourceHash,
99
106
  raufPin: ctx.raufPin,
107
+ placements: placementResult.value,
100
108
  previous: ctx.priorManifest,
101
109
  now: () => new Date(ctx.now),
102
110
  });
@@ -105,6 +113,11 @@ async function applyCopyInstall(planned, ctx) {
105
113
  return fail(ctx, planned, wrote.error);
106
114
  return success(ctx, planned);
107
115
  }
116
+ /** True iff every planned action — primary files and all placement files — is "unchanged". */
117
+ function allUnchanged(planned) {
118
+ return (planned.files.every((f) => f.action === "unchanged") &&
119
+ (planned.placements ?? []).every((p) => p.files.every((f) => f.action === "unchanged")));
120
+ }
108
121
  /** §5.3 copy-mode uninstall: remove recorded files, prune empty dirs, delete the manifest LAST. */
109
122
  async function applyCopyUninstall(planned, ctx) {
110
123
  for (const fa of planned.files) {
@@ -118,6 +131,9 @@ async function applyCopyUninstall(planned, ctx) {
118
131
  const pruned = await removeEmptyDirsWithin(ctx.destination, ctx.agentRoot);
119
132
  if (!pruned.ok)
120
133
  return fail(ctx, planned, pruned.error);
134
+ const placementsRemoved = await removePlacements(planned.placements ?? []);
135
+ if (!placementsRemoved.ok)
136
+ return fail(ctx, planned, placementsRemoved.error);
121
137
  const deleted = await deleteManifest(ctx);
122
138
  if (!deleted.ok)
123
139
  return fail(ctx, planned, deleted.error);
@@ -140,23 +156,35 @@ async function applySymlinkInstall(planned, ctx) {
140
156
  if (!resolved.ok)
141
157
  return fail(ctx, planned, resolved.error);
142
158
  const linkPath = resolved.value;
143
- // Unchanged (live link already points at the same target) zero writes, manifest untouched.
144
- if (planned.files.every((f) => f.action === "unchanged")) {
159
+ // Secondary placements (A4b) apply even in symlink mode they live under a different root than the
160
+ // symlinked namespace dir. Run them first so a placement-only change still rewrites the manifest.
161
+ const placementResult = await applyPlacements(planned.placements ?? [], ctx, source);
162
+ if (!placementResult.ok)
163
+ return fail(ctx, planned, placementResult.error);
164
+ const primary = planned.files;
165
+ const primaryUntouched = primary.every((f) => f.action === "unchanged") ||
166
+ primary.every((f) => f.action === "skip-modified");
167
+ // Nothing changed anywhere ⇒ zero writes, manifest untouched.
168
+ if (allUnchanged(planned)) {
145
169
  return success(ctx, planned);
146
170
  }
147
- // skip-modified (prior exists, no --force) leave it; report, write nothing.
148
- if (planned.files.every((f) => f.action === "skip-modified")) {
149
- return success(ctx, planned);
171
+ // The recorded mode/link reflect the primary namespace dir: only (re)link when it actually changed
172
+ // (a placement-only change leaves a live link and its prior manifest mode/link intact).
173
+ let effectiveMode = ctx.priorManifest?.mode ?? "symlink";
174
+ let linkTarget = ctx.priorManifest?.link?.target ?? (effectiveMode === "symlink" ? source.root : undefined);
175
+ let files = ctx.priorManifest?.files ?? source.files.map((f) => ({ path: f.relpath }));
176
+ if (!primaryUntouched) {
177
+ const removed = await removePath(linkPath);
178
+ if (!removed.ok)
179
+ return fail(ctx, planned, removed.error);
180
+ const linked = await symlinkDir(source.root, linkPath);
181
+ if (!linked.ok)
182
+ return fail(ctx, planned, linked.error);
183
+ effectiveMode = linked.value.mode;
184
+ linkTarget = effectiveMode === "symlink" ? source.root : undefined;
185
+ // files[] lists the bundle-relative paths with sha256 OMITTED (no per-file copy exists, 00 §3).
186
+ files = source.files.map((f) => ({ path: f.relpath }));
150
187
  }
151
- const removed = await removePath(linkPath);
152
- if (!removed.ok)
153
- return fail(ctx, planned, removed.error);
154
- const linked = await symlinkDir(source.root, linkPath);
155
- if (!linked.ok)
156
- return fail(ctx, planned, linked.error);
157
- const effectiveMode = linked.value.mode;
158
- // files[] lists the bundle-relative paths with sha256 OMITTED (no per-file copy exists, 00 §3).
159
- const files = source.files.map((f) => ({ path: f.relpath }));
160
188
  const manifest = buildManifest({
161
189
  agent: ctx.agent,
162
190
  scope: ctx.scope,
@@ -166,8 +194,9 @@ async function applySymlinkInstall(planned, ctx) {
166
194
  skills: source.skills,
167
195
  sourceHash: source.sourceHash,
168
196
  raufPin: ctx.raufPin,
197
+ placements: placementResult.value,
169
198
  // Truthful record: a copy fallback must NOT carry link (copy-mode manifest invariant, 05).
170
- ...(effectiveMode === "symlink" ? { link: { target: source.root } } : {}),
199
+ ...(linkTarget !== undefined ? { link: { target: linkTarget } } : {}),
171
200
  previous: ctx.priorManifest,
172
201
  now: () => new Date(ctx.now),
173
202
  });
@@ -184,12 +213,210 @@ async function applySymlinkUninstall(planned, ctx) {
184
213
  const removed = await removePath(resolved.value);
185
214
  if (!removed.ok)
186
215
  return fail(ctx, planned, removed.error);
216
+ const placementsRemoved = await removePlacements(planned.placements ?? []);
217
+ if (!placementsRemoved.ok)
218
+ return fail(ctx, planned, placementsRemoved.error);
187
219
  const deleted = await deleteManifest(ctx);
188
220
  if (!deleted.ok)
189
221
  return fail(ctx, planned, deleted.error);
190
222
  return success(ctx, planned);
191
223
  }
192
224
  // ---------------------------------------------------------------------------
225
+ // Secondary placements (A4b)
226
+ // ---------------------------------------------------------------------------
227
+ /**
228
+ * Execute every secondary placement for an install/update and return the inventory to record in the
229
+ * manifest. Each placement is contained to ITS OWN root (`resolveWithin(placement.root, …)`), so a
230
+ * mirror under `.codex` and a managed block under `.github` never escape their boundary (REQ-SEC-02).
231
+ * Unchanged/skip-modified entries carry their prior recorded hash forward so the manifest stays
232
+ * faithful. Never throws for expected errors.
233
+ */
234
+ async function applyPlacements(placements, ctx, source) {
235
+ const priorByDest = new Map();
236
+ for (const p of ctx.priorManifest?.placements ?? [])
237
+ priorByDest.set(p.destination, p);
238
+ const out = [];
239
+ for (const pl of placements) {
240
+ const prior = priorByDest.get(pl.destination) ?? null;
241
+ const res = pl.kind === "mirror"
242
+ ? await applyMirror(pl, ctx, source, prior)
243
+ : await applyManagedBlock(pl, prior);
244
+ if (!res.ok)
245
+ return res;
246
+ out.push(res.value);
247
+ }
248
+ return ok(out);
249
+ }
250
+ /** §A4b mirror: copy/refresh/remove flat files under the second root; record per-file sha256. */
251
+ async function applyMirror(pl, ctx, source, prior) {
252
+ const priorByPath = new Map();
253
+ for (const f of prior?.files ?? [])
254
+ priorByPath.set(f.path, f);
255
+ const writeFile = ctx.writeFileSeam ?? defaultCopyFile;
256
+ const inventory = [];
257
+ for (const fa of pl.files) {
258
+ const resolved = resolveWithin(pl.root, pl.destination, fa.relpath);
259
+ if (!resolved.ok)
260
+ return resolved;
261
+ const destAbs = resolved.value;
262
+ switch (fa.action) {
263
+ case "create":
264
+ case "overwrite": {
265
+ if (fa.srcRelpath === undefined) {
266
+ return err({
267
+ code: "UNEXPECTED",
268
+ agent: ctx.agent,
269
+ message: `mirror action for "${fa.relpath}" is missing its source path`,
270
+ });
271
+ }
272
+ const wrote = await writeFile(path.join(source.root, fa.srcRelpath), destAbs);
273
+ if (!wrote.ok)
274
+ return wrote;
275
+ inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
276
+ break;
277
+ }
278
+ case "remove": {
279
+ const removed = await removePath(destAbs);
280
+ if (!removed.ok)
281
+ return removed;
282
+ break;
283
+ }
284
+ case "unchanged":
285
+ case "skip-modified": {
286
+ // Carry the prior record forward; if none exists (e.g. a v1→v2 manifest migration where the
287
+ // 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.
289
+ const p = priorByPath.get(fa.relpath);
290
+ if (p !== undefined)
291
+ inventory.push(p);
292
+ else
293
+ inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
294
+ break;
295
+ }
296
+ }
297
+ }
298
+ return ok({ kind: "mirror", root: pl.root, destination: pl.destination, files: inventory });
299
+ }
300
+ /**
301
+ * §A4b managed-block: merge/refresh the sentinel block into the (possibly user-owned) target file,
302
+ * preserving everything outside the sentinels. Records a single inventory entry whose sha256 is the
303
+ * written region's hash. skip-modified/unchanged carry the prior record forward (no write).
304
+ */
305
+ async function applyManagedBlock(pl, prior) {
306
+ const fa = pl.files[0];
307
+ const basename = fa?.relpath ?? path.basename(pl.destination);
308
+ const resolved = resolveWithin(pl.root, pl.destination);
309
+ if (!resolved.ok)
310
+ return resolved;
311
+ const fileAbs = resolved.value;
312
+ const carry = () => {
313
+ const p = prior?.files.find((f) => f.path === basename);
314
+ return {
315
+ kind: "managed-block",
316
+ root: pl.root,
317
+ destination: pl.destination,
318
+ files: p !== undefined ? [p] : [],
319
+ };
320
+ };
321
+ if (fa === undefined || fa.action === "unchanged" || fa.action === "skip-modified") {
322
+ return ok(carry());
323
+ }
324
+ if (fa.action === "remove") {
325
+ // Uninstall is handled by removePlacements; an install-plan never yields "remove" here.
326
+ return ok(carry());
327
+ }
328
+ // create | overwrite — read existing (or treat as empty), upsert the block, write back.
329
+ const body = pl.blockContent ?? "";
330
+ let existing = "";
331
+ try {
332
+ existing = fs.readFileSync(fileAbs, "utf8");
333
+ }
334
+ catch {
335
+ existing = "";
336
+ }
337
+ const next = upsertBlock(existing, body);
338
+ const wrote = await writeText(fileAbs, next);
339
+ if (!wrote.ok)
340
+ return wrote;
341
+ return ok({
342
+ kind: "managed-block",
343
+ root: pl.root,
344
+ destination: pl.destination,
345
+ files: [{ path: basename, sha256: sha256String(wrapBlock(body)) }],
346
+ });
347
+ }
348
+ /**
349
+ * Remove every secondary placement during uninstall (A4b): a "mirror" deletes each recorded file
350
+ * (and prunes its now-empty dir); a "managed-block" strips ONLY the sentinel region, deleting the
351
+ * file only if nothing else remains. User content outside the block is always preserved.
352
+ */
353
+ async function removePlacements(placements) {
354
+ for (const pl of placements) {
355
+ if (pl.kind === "managed-block") {
356
+ const resolved = resolveWithin(pl.root, pl.destination);
357
+ if (!resolved.ok)
358
+ return resolved;
359
+ const fileAbs = resolved.value;
360
+ let existing;
361
+ try {
362
+ existing = fs.readFileSync(fileAbs, "utf8");
363
+ }
364
+ catch {
365
+ continue; // already gone — nothing to strip
366
+ }
367
+ const stripped = removeBlock(existing);
368
+ if (stripped === "") {
369
+ const removed = await removePath(fileAbs);
370
+ if (!removed.ok)
371
+ return removed;
372
+ }
373
+ else {
374
+ const wrote = await writeText(fileAbs, stripped);
375
+ if (!wrote.ok)
376
+ return wrote;
377
+ }
378
+ continue;
379
+ }
380
+ // mirror: remove each recorded file, then prune the now-empty destination dir.
381
+ for (const fa of pl.files) {
382
+ const resolved = resolveWithin(pl.root, pl.destination, fa.relpath);
383
+ if (!resolved.ok)
384
+ return resolved;
385
+ const removed = await removePath(resolved.value);
386
+ if (!removed.ok)
387
+ return removed;
388
+ }
389
+ const pruned = await removeEmptyDirsWithin(pl.destination, pl.root);
390
+ if (!pruned.ok)
391
+ return pruned;
392
+ }
393
+ return ok(undefined);
394
+ }
395
+ /** Write text content to `destAbs`, creating the parent dir; maps EACCES/EPERM to WRITE_DENIED. */
396
+ async function writeText(destAbs, content) {
397
+ try {
398
+ await fsp.mkdir(path.dirname(destAbs), { recursive: true });
399
+ await fsp.writeFile(destAbs, content, "utf8");
400
+ return ok(undefined);
401
+ }
402
+ catch (e) {
403
+ const code = e?.code;
404
+ if (code === "EACCES" || code === "EPERM") {
405
+ return err({
406
+ code: "WRITE_DENIED",
407
+ message: `no write permission to ${destAbs}`,
408
+ path: destAbs,
409
+ remedy: "check directory permissions, or choose a different scope (--global vs project)",
410
+ });
411
+ }
412
+ return err({
413
+ code: "UNEXPECTED",
414
+ message: `filesystem error at ${destAbs}: ${e?.message ?? String(e)}`,
415
+ path: destAbs,
416
+ });
417
+ }
418
+ }
419
+ // ---------------------------------------------------------------------------
193
420
  // Internal helpers
194
421
  // ---------------------------------------------------------------------------
195
422
  /** Default per-file write seam: ensure the parent dir, then copy the source bytes. */
package/dist/cli.js CHANGED
@@ -16,7 +16,8 @@ import { readFileSync, realpathSync } from "node:fs";
16
16
  import { pathToFileURL } from "node:url";
17
17
  import * as path from "node:path";
18
18
  import { AGENT_IDS, AGENT_TARGETS, EXIT, err, ok, } from "./types.js";
19
- import { detectAgent, detectAgents, resolveRoots } from "./agent-targets.js"; // 02
19
+ import { detectAgent, detectAgents, agentRootFor } from "./agent-targets.js"; // 02
20
+ import { resolvePlacements } from "./placements.js"; // 02 (A4b second-root placements)
20
21
  import { locateSource } from "./source.js"; // 03
21
22
  import { plan, resolveMode } from "./plan.js"; // 04
22
23
  import { apply } from "./apply.js"; // 04
@@ -254,7 +255,8 @@ async function runMutation(subcommand, flags, env) {
254
255
  for (const agent of targets) {
255
256
  const detection = detectAgent(agent, ropts);
256
257
  const r = await runOneAgent(subcommand, agent, detection, flags, scope, mode, raufPin, env);
257
- agentReports.push(r);
258
+ // Carry the scope-effective confidence + docs URL onto the report for honest labeling (A4).
259
+ agentReports.push({ ...r, confidence: detection.confidence, docsUrl: detection.docsUrl });
258
260
  }
259
261
  const anyAgentFailed = agentReports.some((r) => !r.ok);
260
262
  const exitCode = anyAgentFailed || raufError !== undefined ? EXIT.FAILURE : EXIT.SUCCESS;
@@ -274,9 +276,9 @@ async function runMutation(subcommand, flags, env) {
274
276
  /** Run the pipeline for a single agent, returning its AgentReport (catches every expected error). */
275
277
  async function runOneAgent(subcommand, agent, detection, flags, scope, mode, raufPin, env) {
276
278
  const mpath = manifestPath(agent, scope, { home: env.home, cwd: env.cwd });
277
- const roots = resolveRoots({ home: env.home, cwd: env.cwd, scope });
278
- const scopeRoot = scope === "global" ? roots.home : roots.cwd;
279
- const agentRoot = path.join(scopeRoot, AGENT_TARGETS[agent].configDirName);
279
+ // Containment boundary = the agent's install base dir (A4: decoupled from the detection dir,
280
+ // so codex contains under `.agents` and copilot under `.github`).
281
+ const agentRoot = agentRootFor(AGENT_TARGETS[agent], scope, { home: env.home, cwd: env.cwd });
280
282
  // uninstall path: manifest → planUninstall → apply.
281
283
  if (subcommand === "uninstall") {
282
284
  const m = readManifest(mpath);
@@ -319,6 +321,9 @@ async function runOneAgent(subcommand, agent, detection, flags, scope, mode, rau
319
321
  priorManifest: prior.value,
320
322
  force: flags.force,
321
323
  raufPin,
324
+ // A4b: resolve any second-root placements for this agent under the active scope (codex
325
+ // `.codex/agents`, copilot `.github/copilot-instructions.md`); empty for the rest.
326
+ placements: resolvePlacements(AGENT_TARGETS[agent], scope, { home: env.home, cwd: env.cwd }),
322
327
  };
323
328
  const planned = plan(subcommand, planCtx);
324
329
  if (!planned.ok)
@@ -367,7 +372,8 @@ async function runList(flags, env) {
367
372
  const agentReports = [];
368
373
  for (const agent of targets) {
369
374
  const detection = detectAgent(agent, ropts);
370
- agentReports.push(listOneAgent(agent, detection, flags, scope, env));
375
+ const base = listOneAgent(agent, detection, flags, scope, env);
376
+ agentReports.push({ ...base, confidence: detection.confidence, docsUrl: detection.docsUrl });
371
377
  }
372
378
  const anyFailed = agentReports.some((r) => !r.ok);
373
379
  return {
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
@@ -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.7.0"); `null` when `--skip-rauf` (spec 06). */
31
+ /** Recorded pinned rauf coordinate (e.g. "@garygentry/rauf@0.8.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?: {
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 !== SCHEMA_VERSION) {
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;