@warpgogol/forge 2.8.0 → 2.8.2

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 (271) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/os/session/handlers/validate.d.ts.map +1 -1
  5. package/dist/os/session/handlers/validate.js +23 -1
  6. package/dist/os/session/handlers/validate.js.map +1 -1
  7. package/dist/os/session/types.d.ts +31 -1
  8. package/dist/os/session/types.d.ts.map +1 -1
  9. package/dist/os/session/types.js +7 -0
  10. package/dist/os/session/types.js.map +1 -1
  11. package/os/adr/adr.module.ts +158 -0
  12. package/os/adr/frontmatter-io.ts +80 -0
  13. package/os/adr/handlers/archive.ts +244 -0
  14. package/os/adr/handlers/implement-stamp.ts +331 -0
  15. package/os/adr/handlers/list-create.ts +207 -0
  16. package/os/adr/handlers/validate.test.ts +232 -0
  17. package/os/adr/handlers/validate.ts +429 -0
  18. package/os/adr/index.ts +33 -0
  19. package/os/adr/types.ts +141 -0
  20. package/os/audit/audit.module.ts +48 -0
  21. package/os/audit/frontmatter-io.ts +84 -0
  22. package/os/audit/handlers/archive.ts +239 -0
  23. package/os/audit/index.ts +23 -0
  24. package/os/audit/types.ts +29 -0
  25. package/os/compass/compass.module.ts +186 -0
  26. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  27. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  28. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  29. package/os/compass/handlers/compass-inventory.ts +521 -0
  30. package/os/compass/handlers/git-revision.ts +137 -0
  31. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  32. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  33. package/os/compass/index.ts +33 -0
  34. package/os/core/core.module.ts +948 -0
  35. package/os/core/handlers/assets-check.ts +122 -0
  36. package/os/core/handlers/assets-helpers.ts +174 -0
  37. package/os/core/handlers/assets-list.ts +99 -0
  38. package/os/core/handlers/build.ts +155 -0
  39. package/os/core/handlers/determinism-check.ts +354 -0
  40. package/os/core/handlers/dev.ts +127 -0
  41. package/os/core/handlers/invariant-engine.test.ts +681 -0
  42. package/os/core/handlers/knowledge-compact.ts +248 -0
  43. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  44. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  45. package/os/core/handlers/note-link-validate.test.ts +117 -0
  46. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  47. package/os/core/handlers/package-health.test.ts +226 -0
  48. package/os/core/handlers/package-health.ts +231 -0
  49. package/os/core/handlers/pinned-check.ts +200 -0
  50. package/os/core/handlers/pinned-init.ts +333 -0
  51. package/os/core/handlers/pinned-types.ts +50 -0
  52. package/os/core/handlers/pinned-validate.ts +301 -0
  53. package/os/core/handlers/profile-resolve.ts +92 -0
  54. package/os/core/handlers/release-prepare.ts +294 -0
  55. package/os/core/handlers/release-publish.ts +211 -0
  56. package/os/core/handlers/validate.ts +262 -0
  57. package/os/core/handlers/workspace-deps.ts +55 -0
  58. package/os/core/index.ts +13 -0
  59. package/os/exploration/exploration.module.ts +90 -0
  60. package/os/exploration/frontmatter-io.ts +77 -0
  61. package/os/exploration/handlers/archive.ts +159 -0
  62. package/os/exploration/handlers/list.ts +77 -0
  63. package/os/exploration/handlers/show.ts +107 -0
  64. package/os/exploration/index.ts +25 -0
  65. package/os/exploration/types.ts +65 -0
  66. package/os/mission/handlers/archive.test.ts +391 -0
  67. package/os/mission/handlers/archive.ts +453 -0
  68. package/os/mission/index.ts +1 -0
  69. package/os/mission/mission.module.ts +48 -0
  70. package/os/mission/types.ts +41 -0
  71. package/os/naming/index.ts +14 -0
  72. package/os/naming/naming-convention.test.ts +163 -0
  73. package/os/naming/naming-convention.ts +378 -0
  74. package/os/naming/naming.module.ts +36 -0
  75. package/os/notes/index.ts +10 -0
  76. package/os/notes/notes.module.ts +109 -0
  77. package/os/plan/frontmatter-io.ts +82 -0
  78. package/os/plan/handlers/archive.ts +238 -0
  79. package/os/plan/index.ts +23 -0
  80. package/os/plan/plan.module.ts +48 -0
  81. package/os/plan/types.ts +29 -0
  82. package/os/program/discovery.ts +294 -0
  83. package/os/program/handlers/complete.ts +419 -0
  84. package/os/program/handlers/lease.ts +433 -0
  85. package/os/program/handlers/seal.ts +332 -0
  86. package/os/program/handlers/validate.ts +213 -0
  87. package/os/program/lease.ts +156 -0
  88. package/os/program/program.module.ts +212 -0
  89. package/os/program/schemas.ts +218 -0
  90. package/os/program/state.ts +264 -0
  91. package/os/rfc/acceptance.ts +344 -0
  92. package/os/rfc/decision-log.ts +310 -0
  93. package/os/rfc/dna-trace.ts +280 -0
  94. package/os/rfc/frontmatter-io.test.ts +132 -0
  95. package/os/rfc/frontmatter-io.ts +130 -0
  96. package/os/rfc/handlers/archive.ts +300 -0
  97. package/os/rfc/handlers/check.ts +238 -0
  98. package/os/rfc/handlers/implement-stamp.ts +515 -0
  99. package/os/rfc/handlers/index-graph.ts +237 -0
  100. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  101. package/os/rfc/handlers/lifecycle.ts +249 -0
  102. package/os/rfc/handlers/list-create.ts +296 -0
  103. package/os/rfc/handlers/pipeline-status.ts +173 -0
  104. package/os/rfc/handlers/shared.ts +121 -0
  105. package/os/rfc/handlers/supersede-propose.ts +241 -0
  106. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  107. package/os/rfc/handlers/validate-rules.ts +984 -0
  108. package/os/rfc/handlers/validate.ts +166 -0
  109. package/os/rfc/handlers.ts +19 -0
  110. package/os/rfc/index.ts +97 -0
  111. package/os/rfc/rfc.module.ts +416 -0
  112. package/os/rfc/types.ts +596 -0
  113. package/os/rfc/verification-evidence.ts +244 -0
  114. package/os/session/atif-parser.ts +166 -0
  115. package/os/session/frontmatter-io.ts +163 -0
  116. package/os/session/handlers/archive.ts +288 -0
  117. package/os/session/handlers/list.ts +174 -0
  118. package/os/session/handlers/save.ts +348 -0
  119. package/os/session/handlers/validate.ts +250 -0
  120. package/os/session/index.ts +50 -0
  121. package/os/session/session.module.ts +126 -0
  122. package/os/session/types.ts +203 -0
  123. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  124. package/os/spec/live-spec-list.ts +78 -0
  125. package/os/spec/live-spec-merge.test.ts +261 -0
  126. package/os/spec/live-spec-merge.ts +413 -0
  127. package/os/spec/live-spec-show.ts +117 -0
  128. package/os/spec/live-spec-types.ts +93 -0
  129. package/os/spec/live-spec-validate.ts +170 -0
  130. package/os/spec/spec-materialize.ts +390 -0
  131. package/os/spec/spec-schema.ts +174 -0
  132. package/os/spec/spec-status.ts +284 -0
  133. package/os/spec/spec-validate.ts +513 -0
  134. package/os/spec/spec.module.ts +135 -0
  135. package/os/werkstatt/handlers/lock.ts +168 -0
  136. package/os/werkstatt/handlers/schema.ts +47 -0
  137. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  138. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  139. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  140. package/os/werkstatt/index.ts +39 -0
  141. package/os/werkstatt/werkstatt.module.ts +62 -0
  142. package/os/workflow/handlers.ts +304 -0
  143. package/os/workflow/index.ts +28 -0
  144. package/os/workflow/types.ts +99 -0
  145. package/os/workflow/workflow.module.ts +54 -0
  146. package/package.json +3 -3
  147. package/profiles/godot-csharp.yaml +4 -4
  148. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  149. package/profiles/phaser-turborepo.yaml +5 -5
  150. package/skills/_shared/fo-session-summary.md +132 -16
  151. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  152. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  153. package/skills/fo/fo-handoff/SKILL.md +5 -0
  154. package/skills/fo/fo-idea/SKILL.md +1 -1
  155. package/skills/fo/fo-review/SKILL.md +1 -1
  156. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  157. package/skills/fo/fo-session-save/SKILL.md +8 -0
  158. package/src/cli-output.ts +59 -0
  159. package/src/config/__tests__/resolve-terminology.test.ts +45 -0
  160. package/src/config/forge-config.ts +524 -0
  161. package/src/forge-module.ts +39 -0
  162. package/src/index.ts +172 -0
  163. package/src/knowledge/__tests__/promote.test.ts +219 -0
  164. package/src/knowledge/__tests__/serialize.test.ts +192 -0
  165. package/src/knowledge/budgets.ts +202 -0
  166. package/src/knowledge/compact.ts +405 -0
  167. package/src/knowledge/index.ts +53 -0
  168. package/src/knowledge/parse.ts +227 -0
  169. package/src/knowledge/promote.ts +156 -0
  170. package/src/knowledge/schema.ts +112 -0
  171. package/src/knowledge/serialize.ts +72 -0
  172. package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
  173. package/src/migration-adapters/__tests__/registry.test.ts +131 -0
  174. package/src/migration-adapters/__tests__/types.test.ts +86 -0
  175. package/src/migration-adapters/git-utils.ts +71 -0
  176. package/src/migration-adapters/ignored-files.ts +221 -0
  177. package/src/migration-adapters/index.ts +21 -0
  178. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  179. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  180. package/src/migration-adapters/registry.ts +61 -0
  181. package/src/migration-adapters/types.ts +68 -0
  182. package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
  183. package/src/onboarding/agents-generate.ts +443 -0
  184. package/src/onboarding/create.ts +427 -0
  185. package/src/onboarding/doctor.ts +1297 -0
  186. package/src/onboarding/init.ts +337 -0
  187. package/src/onboarding/invariant-engine.ts +448 -0
  188. package/src/onboarding/memory-scaffold.ts +189 -0
  189. package/src/onboarding/nested-agents-generate.ts +93 -0
  190. package/src/onboarding/nested-agents-templates.ts +233 -0
  191. package/src/onboarding/profile-validate.ts +139 -0
  192. package/src/onboarding/scaffold-project.ts +299 -0
  193. package/src/onboarding/scaffold.ts +143 -0
  194. package/src/onboarding/upgrade.ts +558 -0
  195. package/src/onboarding/workspace-discovery.ts +167 -0
  196. package/src/profiles/__tests__/profile-schema.test.ts +228 -0
  197. package/src/profiles/__tests__/stack-profile.test.ts +190 -0
  198. package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +54 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +221 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
  256. package/src/utils/__tests__/fs-trash.test.ts +45 -0
  257. package/src/utils/fs-atomic.ts +93 -0
  258. package/src/utils/fs-idempotent.ts +41 -0
  259. package/src/utils/fs-trash-sync.ts +37 -0
  260. package/src/utils/fs-trash.ts +24 -0
  261. package/src/utils/fs.ts +74 -0
  262. package/src/utils/generated-marker.ts +166 -0
  263. package/src/utils/hash.ts +19 -0
  264. package/src/utils/index.ts +29 -0
  265. package/src/utils/string-utils.ts +19 -0
  266. package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
  267. package/src/validators/note-frontmatter-validate.ts +142 -0
  268. package/src/validators/note-link-validate.ts +145 -0
  269. package/src/validators/note-orphan-detect.ts +146 -0
  270. package/src/validators/port-validate.ts +97 -0
  271. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,183 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Integration tests for forge.init binding defaults (RFC-0540).</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0540: initial init binding defaults integration tests.</item>
7
+ <item>RFC-0552: add test for Forge-vs-pack skill name conflict detection.</item>
8
+ </CHANGE_SUMMARY>
9
+ */
10
+
11
+ import { test, expect, beforeEach, afterEach } from "vitest";
12
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
13
+ import { existsSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { runInit } from "../onboarding/init.ts";
17
+ import { loadForgeConfig } from "../config/forge-config.ts";
18
+
19
+ let tempDir: string;
20
+
21
+ beforeEach(async () => {
22
+ tempDir = await mkdtemp(join(tmpdir(), "forge-init-bindings-test-"));
23
+ });
24
+
25
+ afterEach(async () => {
26
+ await rm(tempDir, { recursive: true, force: true });
27
+ });
28
+
29
+ async function setupForgeSource(dir: string): Promise<void> {
30
+ await mkdir(join(dir, "packages", "forge", "skills"), { recursive: true });
31
+ await writeFile(
32
+ join(dir, "packages", "forge", "package.json"),
33
+ '{"name":"@warpgogol/forge"}',
34
+ "utf8",
35
+ );
36
+ await mkdir(join(dir, "packages", "forge", "os", "rfc"), { recursive: true });
37
+ await writeFile(
38
+ join(dir, "packages", "forge", "os", "rfc", "rfc-0000-template.md"),
39
+ "# RFC Template",
40
+ "utf8",
41
+ );
42
+ await mkdir(join(dir, "packages", "forge", "profiles"), { recursive: true });
43
+ }
44
+
45
+ test("forge.init creates forge.yaml with non-null forge-CLI bindings and null stack bindings", async () => {
46
+ await setupForgeSource(tempDir);
47
+
48
+ runInit({ flags: {} }, { workspaceRoot: tempDir });
49
+
50
+ const config = loadForgeConfig(tempDir);
51
+ expect(config.bindings).toBeDefined();
52
+ expect(config.bindings?.commands.validateRfc).toBe(
53
+ "pnpm exec forge rfc.validate --id {id} --json",
54
+ );
55
+ expect(config.bindings?.commands.validateAdr).toBe(
56
+ "pnpm exec forge adr.validate --id {id} --json",
57
+ );
58
+ expect(config.bindings?.commands.implementStamp).toBe(
59
+ "pnpm exec forge rfc.implement.stamp --id {id} --implementation-commit {commit}",
60
+ );
61
+ expect(config.bindings?.commands.specValidate).toBe(
62
+ "pnpm exec forge spec.validate --spec={id} --json",
63
+ );
64
+ expect(config.bindings?.commands.sessionSave).toBe("pnpm exec forge session.save --json");
65
+ expect(config.bindings?.commands.typecheck).toBeNull();
66
+ expect(config.bindings?.commands.test).toBeNull();
67
+ expect(config.bindings?.commands.scopedBuild).toBeNull();
68
+ });
69
+
70
+ test("forge.yaml written by forge.init contains forge-CLI binding strings", async () => {
71
+ await setupForgeSource(tempDir);
72
+
73
+ runInit({ flags: {} }, { workspaceRoot: tempDir });
74
+
75
+ const yamlContent = await readFile(join(tempDir, "forge.yaml"), "utf8");
76
+ expect(yamlContent).toContain("forge rfc.validate");
77
+ expect(yamlContent).toContain("forge adr.validate");
78
+ expect(yamlContent).toContain("forge rfc.implement.stamp");
79
+ expect(yamlContent).toContain("forge spec.validate");
80
+ expect(yamlContent).toContain("forge session.save");
81
+ });
82
+
83
+ test("forge.init does not overwrite existing forge.yaml", async () => {
84
+ await setupForgeSource(tempDir);
85
+ const existingYaml = `schema: forge/config@1
86
+ project:
87
+ name: existing
88
+ stack: []
89
+ packageManager: pnpm
90
+ paths:
91
+ rfcsDir: docs/rfcs
92
+ adrsDir: docs/adrs
93
+ plansDir: docs/plans
94
+ auditsDir: docs/audits
95
+ specsDir: docs/specs
96
+ skillsDir: .agents/skills
97
+ bindings:
98
+ schema: forge/bindings@1
99
+ commands:
100
+ validateRfc: "custom command"
101
+ validateAdr: null
102
+ implementStamp: null
103
+ typecheck: null
104
+ test: null
105
+ scopedBuild: null
106
+ specValidate: null
107
+ sessionSave: null
108
+ paths:
109
+ invariantsFile: null
110
+ compassDocs: []
111
+ reviewsDir: null
112
+ handoffsDir: null
113
+ sessionsDir: null
114
+ `;
115
+ await writeFile(join(tempDir, "forge.yaml"), existingYaml, "utf8");
116
+
117
+ const result = runInit({ flags: {} }, { workspaceRoot: tempDir });
118
+ expect(result.skipped).toContain("forge.yaml (already exists)");
119
+
120
+ const config = loadForgeConfig(tempDir);
121
+ expect(config.bindings?.commands.validateRfc).toBe("custom command");
122
+ });
123
+
124
+ test("RFC-0552: forge.init returns skippedSkills field and syncs non-conflicting pack skills", async () => {
125
+ await setupForgeSource(tempDir);
126
+
127
+ // Create a pack with prefix 'wg' containing a non-conflicting skill
128
+ await mkdir(join(tempDir, "packages", "wg-skills", "skills", "wg-custom"), { recursive: true });
129
+ await writeFile(
130
+ join(tempDir, "packages", "wg-skills", "skills", "wg-custom", "SKILL.md"),
131
+ "---\nname: wg-custom\n---\n# wg-custom (should be synced)\n",
132
+ "utf8",
133
+ );
134
+
135
+ // Write forge.yaml with skillPacks config
136
+ const yaml = `schema: forge/config@1
137
+ project:
138
+ name: test-project
139
+ stack: []
140
+ packageManager: pnpm
141
+ paths:
142
+ rfcsDir: docs/rfcs
143
+ adrsDir: docs/adrs
144
+ plansDir: docs/plans
145
+ auditsDir: docs/audits
146
+ specsDir: docs/specs
147
+ skillsDir: .agents/skills
148
+ skillPacks:
149
+ - prefix: wg
150
+ dir: packages/wg-skills/skills
151
+ bindings:
152
+ schema: forge/bindings@1
153
+ commands:
154
+ validateRfc: null
155
+ validateAdr: null
156
+ implementStamp: null
157
+ typecheck: null
158
+ test: null
159
+ scopedBuild: null
160
+ specValidate: null
161
+ sessionSave: null
162
+ paths:
163
+ invariantsFile: null
164
+ compassDocs: []
165
+ reviewsDir: null
166
+ handoffsDir: null
167
+ sessionsDir: null
168
+ `;
169
+ await writeFile(join(tempDir, "forge.yaml"), yaml, "utf8");
170
+
171
+ const result = runInit(
172
+ { flags: {} },
173
+ { workspaceRoot: tempDir, forgeRoot: join(tempDir, "packages", "forge") },
174
+ );
175
+
176
+ // skippedSkills field should exist and be empty (no conflicts)
177
+ expect(result.skippedSkills).toBeDefined();
178
+ expect(result.skippedSkills).toEqual([]);
179
+
180
+ // The non-conflicting pack skill should be synced
181
+ const wgSkillPath = join(tempDir, ".agents", "skills", "wg-custom", "SKILL.md");
182
+ expect(existsSync(wgSkillPath)).toBe(true);
183
+ });
@@ -0,0 +1,307 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { parseKnowledgeFile } from "../knowledge/parse.ts";
3
+ import { serializeKnowledgeFile } from "../knowledge/serialize.ts";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+
8
+ function writeTempFile(content: string, fileName: string): string {
9
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-knowledge-"));
10
+ const filePath = path.join(tmpDir, fileName);
11
+ fs.writeFileSync(filePath, content, "utf8");
12
+ return filePath;
13
+ }
14
+
15
+ describe("RFC-0660: knowledge file parser", () => {
16
+ test("valid L0 entry with minimal metadata parses correctly", () => {
17
+ const content = `<!-- knowledge-layer: L0 -->
18
+ # Q&A Log
19
+
20
+ ### K-0001: First question
21
+
22
+ \`\`\`knowledge-entry
23
+ id: K-0001
24
+ layer: L0
25
+ created: 2026-08-03
26
+ status: active
27
+ \`\`\`
28
+
29
+ - **Question:** Is this a test?
30
+ - **Answer:** Yes.
31
+ `;
32
+ const filePath = writeTempFile(content, "qa-log.md");
33
+ try {
34
+ const parsed = parseKnowledgeFile(filePath);
35
+ expect(parsed.isKnowledgeAdjacent).toBe(false);
36
+ expect(parsed.layer).toBe("L0");
37
+ expect(parsed.entries).toHaveLength(1);
38
+ expect(parsed.entries[0].meta.id).toBe("K-0001");
39
+ expect(parsed.entries[0].meta.layer).toBe("L0");
40
+ expect(parsed.entries[0].meta.status).toBe("active");
41
+ expect(parsed.entries[0].title).toBe("First question");
42
+ expect(parsed.entries[0].body).toContain("Is this a test?");
43
+ expect(parsed.parseIssues).toHaveLength(0);
44
+ expect(parsed.legacySections).toHaveLength(0);
45
+ } finally {
46
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ test("valid L2 entry with confirmations and lastConfirmedAt parses correctly", () => {
51
+ const content = `<!-- knowledge-layer: L2 -->
52
+ # Learned Principles
53
+
54
+ ### K-0001: Test principle
55
+
56
+ \`\`\`knowledge-entry
57
+ id: K-0001
58
+ layer: L2
59
+ created: 2026-08-03
60
+ lastConfirmedAt: 2026-08-03
61
+ confirmations: 3
62
+ status: active
63
+ \`\`\`
64
+
65
+ This is a confirmed principle.
66
+ `;
67
+ const filePath = writeTempFile(content, "learned-principles.md");
68
+ try {
69
+ const parsed = parseKnowledgeFile(filePath);
70
+ expect(parsed.entries).toHaveLength(1);
71
+ expect(parsed.entries[0].meta.confirmations).toBe(3);
72
+ expect(parsed.entries[0].meta.lastConfirmedAt).toBe("2026-08-03");
73
+ expect(parsed.parseIssues).toHaveLength(0);
74
+ } finally {
75
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ test("L0 with forbidden confirmations produces parse issue", () => {
80
+ const content = `<!-- knowledge-layer: L0 -->
81
+ # Q&A Log
82
+
83
+ ### K-0001: Bad entry
84
+
85
+ \`\`\`knowledge-entry
86
+ id: K-0001
87
+ layer: L0
88
+ created: 2026-08-03
89
+ confirmations: 5
90
+ status: active
91
+ \`\`\`
92
+
93
+ Body.
94
+ `;
95
+ const filePath = writeTempFile(content, "qa-log.md");
96
+ try {
97
+ const parsed = parseKnowledgeFile(filePath);
98
+ expect(parsed.parseIssues.length).toBeGreaterThan(0);
99
+ expect(parsed.parseIssues.some((i) => i.message.includes("confirmations"))).toBe(true);
100
+ } finally {
101
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
102
+ }
103
+ });
104
+
105
+ test("L2 without required confirmations produces parse issue", () => {
106
+ const content = `<!-- knowledge-layer: L2 -->
107
+ # Learned Principles
108
+
109
+ ### K-0001: Incomplete entry
110
+
111
+ \`\`\`knowledge-entry
112
+ id: K-0001
113
+ layer: L2
114
+ created: 2026-08-03
115
+ status: active
116
+ \`\`\`
117
+
118
+ Body.
119
+ `;
120
+ const filePath = writeTempFile(content, "learned-principles.md");
121
+ try {
122
+ const parsed = parseKnowledgeFile(filePath);
123
+ expect(parsed.parseIssues.length).toBeGreaterThan(0);
124
+ expect(parsed.parseIssues.some((i) => i.message.includes("confirmations is required"))).toBe(
125
+ true,
126
+ );
127
+ } finally {
128
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
129
+ }
130
+ });
131
+
132
+ test("malformed YAML in metadata block produces parse issue, does not throw", () => {
133
+ const content = `<!-- knowledge-layer: L0 -->
134
+ # Q&A Log
135
+
136
+ ### K-0001: Broken entry
137
+
138
+ \`\`\`knowledge-entry
139
+ id: K-0001
140
+ layer: L0
141
+ created: 2026-08-03
142
+ status: [unclosed
143
+ \`\`\`
144
+
145
+ Body.
146
+ `;
147
+ const filePath = writeTempFile(content, "qa-log.md");
148
+ try {
149
+ const parsed = parseKnowledgeFile(filePath);
150
+ expect(parsed.parseIssues.length).toBeGreaterThan(0);
151
+ expect(parsed.entries).toHaveLength(0);
152
+ } finally {
153
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
154
+ }
155
+ });
156
+
157
+ test("knowledge-adjacent file (no K-NNNN headings, no layer preamble) returns empty result", () => {
158
+ const content = `# About Forge
159
+
160
+ This is a knowledge-adjacent file with no entries and no layer declaration.
161
+ It should be exempt from SKILL-19/SKILL-20.
162
+ `;
163
+ const filePath = writeTempFile(content, "forge-about.md");
164
+ try {
165
+ const parsed = parseKnowledgeFile(filePath);
166
+ expect(parsed.isKnowledgeAdjacent).toBe(true);
167
+ expect(parsed.entries).toHaveLength(0);
168
+ expect(parsed.legacySections).toHaveLength(0);
169
+ expect(parsed.parseIssues).toHaveLength(0);
170
+ } finally {
171
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
172
+ }
173
+ });
174
+
175
+ test("file with layer but no entries is structured-empty (not knowledge-adjacent)", () => {
176
+ const content = `<!-- knowledge-layer: L0 -->
177
+ # Q&A Log
178
+
179
+ <!-- Entries are appended by the skill during runs. -->
180
+ `;
181
+ const filePath = writeTempFile(content, "qa-log.md");
182
+ try {
183
+ const parsed = parseKnowledgeFile(filePath);
184
+ expect(parsed.isKnowledgeAdjacent).toBe(false);
185
+ expect(parsed.layer).toBe("L0");
186
+ expect(parsed.entries).toHaveLength(0);
187
+ expect(parsed.legacySections).toHaveLength(0);
188
+ expect(parsed.parseIssues).toHaveLength(0);
189
+ } finally {
190
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
191
+ }
192
+ });
193
+
194
+ test("layer detected via filename convention when no preamble comment", () => {
195
+ const content = `# Q&A Log
196
+
197
+ ### K-0001: Test
198
+
199
+ \`\`\`knowledge-entry
200
+ id: K-0001
201
+ layer: L0
202
+ created: 2026-08-03
203
+ status: active
204
+ \`\`\`
205
+
206
+ Body.
207
+ `;
208
+ const filePath = writeTempFile(content, "qa-log.md");
209
+ try {
210
+ const parsed = parseKnowledgeFile(filePath);
211
+ expect(parsed.layer).toBe("L0");
212
+ } finally {
213
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
214
+ }
215
+ });
216
+
217
+ test("layer comment overrides filename convention", () => {
218
+ const content = `<!-- knowledge-layer: L2 -->
219
+ # Custom File
220
+
221
+ ### K-0001: Test
222
+
223
+ \`\`\`knowledge-entry
224
+ id: K-0001
225
+ layer: L2
226
+ created: 2026-08-03
227
+ lastConfirmedAt: 2026-08-03
228
+ confirmations: 1
229
+ status: active
230
+ \`\`\`
231
+
232
+ Body.
233
+ `;
234
+ const filePath = writeTempFile(content, "qa-log.md");
235
+ try {
236
+ const parsed = parseKnowledgeFile(filePath);
237
+ expect(parsed.layer).toBe("L2");
238
+ } finally {
239
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
240
+ }
241
+ });
242
+
243
+ test("non-existent file returns empty result without throwing", () => {
244
+ const parsed = parseKnowledgeFile("/nonexistent/path/to/file.md");
245
+ expect(parsed.isKnowledgeAdjacent).toBe(true);
246
+ expect(parsed.entries).toHaveLength(0);
247
+ });
248
+
249
+ test("directory path returns empty result without throwing", () => {
250
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-knowledge-dir-"));
251
+ try {
252
+ const parsed = parseKnowledgeFile(tmpDir);
253
+ expect(parsed.isKnowledgeAdjacent).toBe(true);
254
+ expect(parsed.entries).toHaveLength(0);
255
+ } finally {
256
+ fs.rmSync(tmpDir, { recursive: true, force: true });
257
+ }
258
+ });
259
+ });
260
+
261
+ describe("RFC-0660: knowledge file serializer round-trip", () => {
262
+ test("parse → serialize → parse produces identical metadata", () => {
263
+ const content = `<!-- knowledge-layer: L0 -->
264
+ # Q&A Log
265
+
266
+ ### K-0001: First question
267
+
268
+ \`\`\`knowledge-entry
269
+ id: K-0001
270
+ layer: L0
271
+ created: 2026-08-03
272
+ status: active
273
+ \`\`\`
274
+
275
+ - **Question:** Is this a test?
276
+ - **Answer:** Yes.
277
+
278
+ ### K-0002: Second question
279
+
280
+ \`\`\`knowledge-entry
281
+ id: K-0002
282
+ layer: L0
283
+ created: 2026-08-03
284
+ status: active
285
+ \`\`\`
286
+
287
+ - **Question:** Another question?
288
+ - **Answer:** Another answer.
289
+ `;
290
+ const filePath = writeTempFile(content, "qa-log.md");
291
+ try {
292
+ const parsed1 = parseKnowledgeFile(filePath);
293
+ const serialized = serializeKnowledgeFile(parsed1);
294
+ const filePath2 = writeTempFile(serialized, "qa-log.md");
295
+ const parsed2 = parseKnowledgeFile(filePath2);
296
+
297
+ expect(parsed2.entries).toHaveLength(parsed1.entries.length);
298
+ for (let i = 0; i < parsed1.entries.length; i++) {
299
+ expect(parsed2.entries[i].meta).toEqual(parsed1.entries[i].meta);
300
+ expect(parsed2.entries[i].title).toBe(parsed1.entries[i].title);
301
+ }
302
+ fs.rmSync(path.dirname(filePath2), { recursive: true, force: true });
303
+ } finally {
304
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
305
+ }
306
+ });
307
+ });
@@ -0,0 +1,164 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { parseKnowledgeFile } from "../knowledge/parse.ts";
3
+ import { serializeKnowledgeFile } from "../knowledge/serialize.ts";
4
+ import { knowledgeEntryMetaSchema, type KnowledgeEntryMeta } from "../knowledge/schema.ts";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import os from "node:os";
8
+ import fc from "fast-check";
9
+
10
+ function buildMarkdown(entries: KnowledgeEntryMeta[], layer: string, fileName: string): string {
11
+ const parts: string[] = [`<!-- knowledge-layer: ${layer} -->`, `# ${fileName}`, ""];
12
+ for (const meta of entries) {
13
+ parts.push(`### ${meta.id}: Test entry`);
14
+ parts.push("");
15
+ parts.push("```knowledge-entry");
16
+ const fields: [string, unknown][] = [
17
+ ["id", meta.id],
18
+ ["layer", meta.layer],
19
+ ["created", meta.created],
20
+ ];
21
+ if (meta.lastConfirmedAt !== undefined) fields.push(["lastConfirmedAt", meta.lastConfirmedAt]);
22
+ if (meta.confirmations !== undefined) fields.push(["confirmations", meta.confirmations]);
23
+ if (meta.expiresAt !== undefined) fields.push(["expiresAt", meta.expiresAt]);
24
+ if (meta.supersedes !== undefined) fields.push(["supersedes", meta.supersedes]);
25
+ if (meta.promotedTo !== undefined) fields.push(["promotedTo", meta.promotedTo]);
26
+ fields.push(["status", meta.status]);
27
+ for (const [key, value] of fields) {
28
+ if (value === null) {
29
+ parts.push(`${key}: null`);
30
+ } else if (Array.isArray(value)) {
31
+ parts.push(`${key}: [${value.join(", ")}]`);
32
+ } else {
33
+ parts.push(`${key}: ${value}`);
34
+ }
35
+ }
36
+ parts.push("```");
37
+ parts.push("");
38
+ parts.push("Body text for this entry.");
39
+ parts.push("");
40
+ }
41
+ return parts.join("\n");
42
+ }
43
+
44
+ function writeTempFile(content: string, fileName: string): string {
45
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-knowledge-pbt-"));
46
+ const filePath = path.join(tmpDir, fileName);
47
+ fs.writeFileSync(filePath, content, "utf8");
48
+ return filePath;
49
+ }
50
+
51
+ describe("RFC-0660: property-based round-trip", () => {
52
+ test("parse → serialize → parse produces identical metadata for L0 entries", () => {
53
+ const l0EntryArbitrary = fc.integer({ min: 1, max: 9999 }).map((n) => ({
54
+ id: `K-${String(n).padStart(4, "0")}`,
55
+ layer: "L0" as const,
56
+ created: "2026-08-03",
57
+ status: "active" as const,
58
+ })) as fc.Arbitrary<KnowledgeEntryMeta>;
59
+
60
+ fc.assert(
61
+ fc.property(fc.array(l0EntryArbitrary, { minLength: 1, maxLength: 5 }), (entries) => {
62
+ const markdown = buildMarkdown(entries, "L0", "qa-log.md");
63
+ const filePath1 = writeTempFile(markdown, "qa-log.md");
64
+ try {
65
+ const parsed1 = parseKnowledgeFile(filePath1);
66
+ const serialized = serializeKnowledgeFile(parsed1);
67
+ const filePath2 = writeTempFile(serialized, "qa-log.md");
68
+ try {
69
+ const parsed2 = parseKnowledgeFile(filePath2);
70
+ expect(parsed2.entries.length).toBe(parsed1.entries.length);
71
+ for (let i = 0; i < parsed1.entries.length; i++) {
72
+ expect(parsed2.entries[i].meta).toEqual(parsed1.entries[i].meta);
73
+ }
74
+ } finally {
75
+ fs.rmSync(path.dirname(filePath2), { recursive: true, force: true });
76
+ }
77
+ } finally {
78
+ fs.rmSync(path.dirname(filePath1), { recursive: true, force: true });
79
+ }
80
+ }),
81
+ { numRuns: 50 },
82
+ );
83
+ });
84
+
85
+ test("parse → serialize → parse produces identical metadata for L2 entries", () => {
86
+ const l2EntryArbitrary = fc.integer({ min: 1, max: 9999 }).map((n) => ({
87
+ id: `K-${String(n).padStart(4, "0")}`,
88
+ layer: "L2" as const,
89
+ created: "2026-08-03",
90
+ lastConfirmedAt: "2026-08-03",
91
+ confirmations: n % 100,
92
+ status: "active" as const,
93
+ })) as fc.Arbitrary<KnowledgeEntryMeta>;
94
+
95
+ fc.assert(
96
+ fc.property(fc.array(l2EntryArbitrary, { minLength: 1, maxLength: 5 }), (entries) => {
97
+ const markdown = buildMarkdown(entries, "L2", "learned-principles.md");
98
+ const filePath1 = writeTempFile(markdown, "learned-principles.md");
99
+ try {
100
+ const parsed1 = parseKnowledgeFile(filePath1);
101
+ const serialized = serializeKnowledgeFile(parsed1);
102
+ const filePath2 = writeTempFile(serialized, "learned-principles.md");
103
+ try {
104
+ const parsed2 = parseKnowledgeFile(filePath2);
105
+ expect(parsed2.entries.length).toBe(parsed1.entries.length);
106
+ for (let i = 0; i < parsed1.entries.length; i++) {
107
+ expect(parsed2.entries[i].meta).toEqual(parsed1.entries[i].meta);
108
+ }
109
+ } finally {
110
+ fs.rmSync(path.dirname(filePath2), { recursive: true, force: true });
111
+ }
112
+ } finally {
113
+ fs.rmSync(path.dirname(filePath1), { recursive: true, force: true });
114
+ }
115
+ }),
116
+ { numRuns: 50 },
117
+ );
118
+ });
119
+
120
+ test("schema validates well-formed L0 entry", () => {
121
+ const validL0 = {
122
+ id: "K-0001",
123
+ layer: "L0",
124
+ created: "2026-08-03",
125
+ status: "active",
126
+ };
127
+ expect(knowledgeEntryMetaSchema.safeParse(validL0).success).toBe(true);
128
+ });
129
+
130
+ test("schema validates well-formed L2 entry with confirmations", () => {
131
+ const validL2 = {
132
+ id: "K-0001",
133
+ layer: "L2",
134
+ created: "2026-08-03",
135
+ lastConfirmedAt: "2026-08-03",
136
+ confirmations: 3,
137
+ status: "active",
138
+ };
139
+ expect(knowledgeEntryMetaSchema.safeParse(validL2).success).toBe(true);
140
+ });
141
+
142
+ test("schema rejects L0 with confirmations", () => {
143
+ const invalidL0 = {
144
+ id: "K-0001",
145
+ layer: "L0",
146
+ created: "2026-08-03",
147
+ confirmations: 5,
148
+ status: "active",
149
+ };
150
+ const result = knowledgeEntryMetaSchema.safeParse(invalidL0);
151
+ expect(result.success).toBe(false);
152
+ });
153
+
154
+ test("schema rejects L2 without confirmations", () => {
155
+ const invalidL2 = {
156
+ id: "K-0001",
157
+ layer: "L2",
158
+ created: "2026-08-03",
159
+ status: "active",
160
+ };
161
+ const result = knowledgeEntryMetaSchema.safeParse(invalidL2);
162
+ expect(result.success).toBe(false);
163
+ });
164
+ });