@warpgogol/forge 5.2.0 → 5.2.3

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 (78) hide show
  1. package/AGENTS.md +2 -1
  2. package/dist/os/adr/adr.module.js +1 -1
  3. package/dist/os/adr/adr.module.js.map +1 -1
  4. package/dist/os/compass/compass.module.js +2 -2
  5. package/dist/os/compass/compass.module.js.map +1 -1
  6. package/dist/os/compass/handlers/compass-audit-handler.d.ts +3 -0
  7. package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
  8. package/dist/os/compass/handlers/compass-audit-handler.js +87 -8
  9. package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
  10. package/dist/os/core/core.module.js +1 -1
  11. package/dist/os/core/core.module.js.map +1 -1
  12. package/dist/os/mission/handlers/archive.d.ts.map +1 -1
  13. package/dist/os/mission/handlers/archive.js +22 -6
  14. package/dist/os/mission/handlers/archive.js.map +1 -1
  15. package/dist/os/program/lease.js +1 -1
  16. package/dist/os/program/lease.js.map +1 -1
  17. package/dist/os/queue/queue.module.js +1 -1
  18. package/dist/os/queue/queue.module.js.map +1 -1
  19. package/dist/os/rfc/handlers/list-create.d.ts.map +1 -1
  20. package/dist/os/rfc/handlers/list-create.js +79 -32
  21. package/dist/os/rfc/handlers/list-create.js.map +1 -1
  22. package/dist/os/rfc/rfc.module.js +1 -1
  23. package/dist/os/rfc/rfc.module.js.map +1 -1
  24. package/dist/os/spec/spec.module.d.ts.map +1 -1
  25. package/dist/os/spec/spec.module.js +2 -0
  26. package/dist/os/spec/spec.module.js.map +1 -1
  27. package/dist/src/knowledge/index.d.ts +1 -0
  28. package/dist/src/knowledge/index.d.ts.map +1 -1
  29. package/dist/src/knowledge/index.js +1 -0
  30. package/dist/src/knowledge/index.js.map +1 -1
  31. package/dist/src/knowledge/sync.d.ts +24 -0
  32. package/dist/src/knowledge/sync.d.ts.map +1 -0
  33. package/dist/src/knowledge/sync.js +127 -0
  34. package/dist/src/knowledge/sync.js.map +1 -0
  35. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  36. package/dist/src/onboarding/doctor.js +7 -6
  37. package/dist/src/onboarding/doctor.js.map +1 -1
  38. package/dist/src/onboarding/init.d.ts.map +1 -1
  39. package/dist/src/onboarding/init.js +15 -8
  40. package/dist/src/onboarding/init.js.map +1 -1
  41. package/dist/src/onboarding/upgrade.d.ts.map +1 -1
  42. package/dist/src/onboarding/upgrade.js +42 -13
  43. package/dist/src/onboarding/upgrade.js.map +1 -1
  44. package/dist/src/types.d.ts +1 -2
  45. package/dist/src/types.d.ts.map +1 -1
  46. package/dist/src/types.js +1 -0
  47. package/dist/src/types.js.map +1 -1
  48. package/dist/src/utils/fs-atomic.js +1 -1
  49. package/dist/src/utils/fs-atomic.js.map +1 -1
  50. package/os/adr/adr.module.ts +1 -1
  51. package/os/compass/compass.module.ts +2 -2
  52. package/os/compass/handlers/compass-audit-handler.ts +116 -8
  53. package/os/compass/handlers/tests/compass-audit-plan.test.ts +98 -0
  54. package/os/compass/handlers/tests/compass-audit-record.test.ts +105 -0
  55. package/os/compass/handlers/tests/compass-audit-validate.test.ts +133 -0
  56. package/os/compass/handlers/tests/compass-ledger-scope.test.ts +124 -0
  57. package/os/core/core.module.ts +1 -1
  58. package/os/mission/handlers/archive.test.ts +49 -0
  59. package/os/mission/handlers/archive.ts +21 -6
  60. package/os/program/lease.ts +1 -1
  61. package/os/queue/queue.module.ts +1 -1
  62. package/os/rfc/handlers/list-create.ts +83 -35
  63. package/os/rfc/handlers/validate-rules.test.ts +50 -0
  64. package/os/rfc/rfc-create-concurrent.test.ts +118 -0
  65. package/os/rfc/rfc-create-hint.test.ts +90 -0
  66. package/os/rfc/rfc-read-only-no-side-effects.test.ts +83 -0
  67. package/os/rfc/rfc.module.ts +1 -1
  68. package/os/spec/spec.module.ts +2 -0
  69. package/package.json +1 -1
  70. package/src/knowledge/__tests__/sync.test.ts +227 -0
  71. package/src/knowledge/index.ts +7 -0
  72. package/src/knowledge/sync.ts +161 -0
  73. package/src/onboarding/doctor.ts +7 -6
  74. package/src/onboarding/init.ts +15 -8
  75. package/src/onboarding/upgrade.ts +47 -16
  76. package/src/tests/upgrade.test.ts +53 -0
  77. package/src/types.ts +2 -2
  78. package/src/utils/fs-atomic.ts +1 -1
@@ -24,6 +24,7 @@ import fs from "node:fs";
24
24
  import path from "node:path";
25
25
  import { stringify as stringifyYaml, parse as parseYaml } from "yaml";
26
26
  import { FORGE_SKILLS, discoverPackSkills } from "../registry.ts";
27
+ import { syncKnowledgeFile } from "../knowledge/index.ts";
27
28
  import { defaultForgeConfig, loadForgeConfig, resolveForgeRoot, type ForgeConfig } from "../config/forge-config.ts";
28
29
  import { listStackProfiles, detectStack } from "../profiles/stack-profile.ts";
29
30
 
@@ -292,9 +293,11 @@ export function runInit(
292
293
  const kfSrcPath = path.join(skillSrcDir, kf);
293
294
  const kfDestPath = path.join(destDir, kf);
294
295
  if (fs.existsSync(kfSrcPath)) {
295
- const kfContent = fs.readFileSync(kfSrcPath, "utf8");
296
- fs.writeFileSync(kfDestPath, kfContent, "utf8");
297
- created.push(`${config.paths.skillsDir}/${skillName}/${kf}`);
296
+ // Append-only sync — never overwrite local knowledge entries
297
+ const syncResult = syncKnowledgeFile(kfSrcPath, kfDestPath);
298
+ if (syncResult.action === "copied" || syncResult.action === "merged") {
299
+ created.push(`${config.paths.skillsDir}/${skillName}/${kf}`);
300
+ }
298
301
  } else {
299
302
  errors.push(`Knowledge file '${kf}' declared in ${skillName} but not found in source`);
300
303
  }
@@ -362,9 +365,11 @@ export function runInit(
362
365
  const kfSrcPath = path.join(skillSrcDir, kf);
363
366
  const kfDestPath = path.join(destDir, kf);
364
367
  if (fs.existsSync(kfSrcPath)) {
365
- const kfContent = fs.readFileSync(kfSrcPath, "utf8");
366
- fs.writeFileSync(kfDestPath, kfContent, "utf8");
367
- created.push(`${config.paths.skillsDir}/${skillName}/${kf}`);
368
+ // Append-only sync — never overwrite local knowledge entries
369
+ const syncResult = syncKnowledgeFile(kfSrcPath, kfDestPath);
370
+ if (syncResult.action === "copied" || syncResult.action === "merged") {
371
+ created.push(`${config.paths.skillsDir}/${skillName}/${kf}`);
372
+ }
368
373
  } else {
369
374
  errors.push(`Knowledge file '${kf}' declared in ${skillName} but not found in source`);
370
375
  }
@@ -382,8 +387,10 @@ export function runInit(
382
387
  const sharedKnowledgeDestDir = path.join(agentsSkillsDir, "shared-knowledge");
383
388
  fs.mkdirSync(sharedKnowledgeDestDir, { recursive: true });
384
389
  const sharedKnowledgeDest = path.join(sharedKnowledgeDestDir, "learned-principles.md");
385
- fs.writeFileSync(sharedKnowledgeDest, fs.readFileSync(sharedKnowledgeSrc, "utf8"), "utf8");
386
- created.push(`${config.paths.skillsDir}/shared-knowledge/learned-principles.md`);
390
+ const syncResult = syncKnowledgeFile(sharedKnowledgeSrc, sharedKnowledgeDest);
391
+ if (syncResult.action === "copied" || syncResult.action === "merged") {
392
+ created.push(`${config.paths.skillsDir}/shared-knowledge/learned-principles.md`);
393
+ }
387
394
  }
388
395
 
389
396
  // 4. Create docs directories from config paths
@@ -39,6 +39,7 @@ import {
39
39
  type ForgeBindings,
40
40
  } from "../config/forge-config.ts";
41
41
  import { FORGE_SKILLS, discoverPackSkills } from "../registry.ts";
42
+ import { syncKnowledgeFile } from "../knowledge/index.ts";
42
43
  import { generateNestedAgentsMd } from "./nested-agents-generate.ts";
43
44
  import { scaffoldMemoryLayer } from "./memory-scaffold.ts";
44
45
  import type { SkippedSkill } from "./init.ts";
@@ -143,8 +144,9 @@ function syncForgeSkills(
143
144
  forgeRoot: string,
144
145
  skillsDir: string,
145
146
  dryRun: boolean,
146
- ): string[] {
147
+ ): { updated: string[]; knowledgeConflicts: string[] } {
147
148
  const updated: string[] = [];
149
+ const knowledgeConflicts: string[] = [];
148
150
  const agentsSkillsDir = path.join(workspaceRoot, skillsDir);
149
151
 
150
152
  for (const skill of FORGE_SKILLS) {
@@ -160,7 +162,7 @@ function syncForgeSkills(
160
162
  const content = fs.readFileSync(srcPath, "utf8");
161
163
  fs.writeFileSync(destPath, content, "utf8");
162
164
 
163
- // Sync knowledge files
165
+ // Sync knowledge files (append-only — never overwrite local entries)
164
166
  const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
165
167
  if (fmMatch) {
166
168
  try {
@@ -173,7 +175,10 @@ function syncForgeSkills(
173
175
  const kfSrcPath = path.join(skillSrcDir, kf);
174
176
  const kfDestPath = path.join(destDir, kf);
175
177
  if (fs.existsSync(kfSrcPath)) {
176
- fs.writeFileSync(kfDestPath, fs.readFileSync(kfSrcPath, "utf8"), "utf8");
178
+ const syncResult = syncKnowledgeFile(kfSrcPath, kfDestPath);
179
+ for (const id of syncResult.conflicts) {
180
+ knowledgeConflicts.push(`${skillName}/${kf}#${id}`);
181
+ }
177
182
  }
178
183
  }
179
184
  }
@@ -185,7 +190,7 @@ function syncForgeSkills(
185
190
  updated.push(skillName);
186
191
  }
187
192
 
188
- return updated;
193
+ return { updated, knowledgeConflicts };
189
194
  }
190
195
 
191
196
  function syncSharedKnowledge(
@@ -193,20 +198,24 @@ function syncSharedKnowledge(
193
198
  forgeRoot: string,
194
199
  skillsDir: string,
195
200
  dryRun: boolean,
196
- ): string[] {
201
+ ): { updated: string[]; knowledgeConflicts: string[] } {
197
202
  const updated: string[] = [];
203
+ const knowledgeConflicts: string[] = [];
198
204
  const srcPath = path.join(forgeRoot, "skills", "shared", "knowledge", "learned-principles.md");
199
- if (!fs.existsSync(srcPath)) return updated;
205
+ if (!fs.existsSync(srcPath)) return { updated, knowledgeConflicts };
200
206
 
201
207
  const destDir = path.join(workspaceRoot, skillsDir, "shared-knowledge");
202
208
  const destPath = path.join(destDir, "learned-principles.md");
203
209
 
204
210
  if (!dryRun) {
205
211
  fs.mkdirSync(destDir, { recursive: true });
206
- fs.writeFileSync(destPath, fs.readFileSync(srcPath, "utf8"), "utf8");
212
+ const syncResult = syncKnowledgeFile(srcPath, destPath);
213
+ for (const id of syncResult.conflicts) {
214
+ knowledgeConflicts.push(`shared-knowledge/learned-principles.md#${id}`);
215
+ }
207
216
  }
208
217
  updated.push("shared-knowledge");
209
- return updated;
218
+ return { updated, knowledgeConflicts };
210
219
  }
211
220
 
212
221
  function syncPackSkills(
@@ -214,9 +223,10 @@ function syncPackSkills(
214
223
  config: ForgeConfig,
215
224
  skillsDir: string,
216
225
  dryRun: boolean,
217
- ): { updated: string[]; skipped: SkippedSkill[] } {
226
+ ): { updated: string[]; skipped: SkippedSkill[]; knowledgeConflicts: string[] } {
218
227
  const updated: string[] = [];
219
228
  const skipped: SkippedSkill[] = [];
229
+ const knowledgeConflicts: string[] = [];
220
230
  const agentsSkillsDir = path.join(workspaceRoot, skillsDir);
221
231
  let packSkills: ReturnType<typeof discoverPackSkills> = [];
222
232
  try {
@@ -245,7 +255,7 @@ function syncPackSkills(
245
255
  const content = fs.readFileSync(srcPath, "utf8");
246
256
  fs.writeFileSync(destPath, content, "utf8");
247
257
 
248
- // Sync knowledge files
258
+ // Sync knowledge files (append-only — never overwrite local entries)
249
259
  const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
250
260
  if (fmMatch) {
251
261
  try {
@@ -258,7 +268,10 @@ function syncPackSkills(
258
268
  const kfSrcPath = path.join(skillSrcDir, kf);
259
269
  const kfDestPath = path.join(destDir, kf);
260
270
  if (fs.existsSync(kfSrcPath)) {
261
- fs.writeFileSync(kfDestPath, fs.readFileSync(kfSrcPath, "utf8"), "utf8");
271
+ const syncResult = syncKnowledgeFile(kfSrcPath, kfDestPath);
272
+ for (const id of syncResult.conflicts) {
273
+ knowledgeConflicts.push(`${skillName}/${kf}#${id}`);
274
+ }
262
275
  }
263
276
  }
264
277
  }
@@ -270,7 +283,7 @@ function syncPackSkills(
270
283
  updated.push(skillName);
271
284
  }
272
285
 
273
- return { updated, skipped };
286
+ return { updated, skipped, knowledgeConflicts };
274
287
  }
275
288
 
276
289
  function addMissingBindingDefaults(
@@ -467,7 +480,7 @@ export async function runUpgrade(
467
480
  }
468
481
 
469
482
  // Step 3: Sync forge skills
470
- const forgeSkillsUpdated = syncForgeSkills(
483
+ const forgeSkillsResult = syncForgeSkills(
471
484
  workspaceRoot,
472
485
  forgeRoot,
473
486
  config.paths.skillsDir,
@@ -483,14 +496,29 @@ export async function runUpgrade(
483
496
  );
484
497
 
485
498
  // Step 3c: Sync shared knowledge layer (RFC-0663)
486
- const sharedKnowledgeUpdated = syncSharedKnowledge(
499
+ const sharedKnowledgeResult = syncSharedKnowledge(
487
500
  workspaceRoot,
488
501
  forgeRoot,
489
502
  config.paths.skillsDir,
490
503
  isDryRun,
491
504
  );
492
505
 
493
- const skillsUpdated = [...forgeSkillsUpdated, ...packResult.updated, ...sharedKnowledgeUpdated];
506
+ const skillsUpdated = [
507
+ ...forgeSkillsResult.updated,
508
+ ...packResult.updated,
509
+ ...sharedKnowledgeResult.updated,
510
+ ];
511
+ const knowledgeConflicts = [
512
+ ...forgeSkillsResult.knowledgeConflicts,
513
+ ...packResult.knowledgeConflicts,
514
+ ...sharedKnowledgeResult.knowledgeConflicts,
515
+ ];
516
+ if (knowledgeConflicts.length > 0) {
517
+ context.logger.warn(
518
+ `forge.upgrade: ${knowledgeConflicts.length} knowledge entr${knowledgeConflicts.length === 1 ? "y" : "ies"} ` +
519
+ `diverge between package and project — local versions kept: ${knowledgeConflicts.join(", ")}`,
520
+ );
521
+ }
494
522
 
495
523
  // Step 3d: Scaffold memory layer (RFC-0664)
496
524
  const memoryScaffold = isDryRun ? { created: [], gitignoreUpdated: false, skipped: [] } : scaffoldMemoryLayer(workspaceRoot);
@@ -565,6 +593,9 @@ export async function runUpgrade(
565
593
  ? `[dry-run] forge.upgrade: would sync ${skillsUpdated.length} skill(s), add ${bindingsAdded.length} binding(s), update syncedVersion to ${toVersion}`
566
594
  : npmCheck.warning
567
595
  ? `[forge.upgrade] OK — ${skillsUpdated.length} skill(s) synced, ${bindingsAdded.length} binding(s) added, syncedVersion → ${toVersion} — ⚠ ${npmCheck.warning}`
568
- : `[forge.upgrade] OK — ${skillsUpdated.length} skill(s) synced, ${bindingsAdded.length} binding(s) added, syncedVersion → ${toVersion}`,
596
+ : `[forge.upgrade] OK — ${skillsUpdated.length} skill(s) synced, ${bindingsAdded.length} binding(s) added, syncedVersion → ${toVersion}` +
597
+ (knowledgeConflicts.length > 0
598
+ ? ` — ${knowledgeConflicts.length} knowledge conflict(s), local kept`
599
+ : ""),
569
600
  };
570
601
  }
@@ -431,3 +431,56 @@ test("forge.upgrade without --update-npm does not attempt npm install", async ()
431
431
  expect(result.data?.npmUpdated).toBe(false);
432
432
  expect(result.data?.npmUpdateSkipped).toBe(null);
433
433
  });
434
+
435
+ function qaLog(entries: { id: string; title: string; body: string }[]): string {
436
+ const blocks = entries.map(
437
+ (e) =>
438
+ `### ${e.id}: ${e.title}\n\n\`\`\`knowledge-entry\nid: ${e.id}\nlayer: L0\ncreated: 2026-08-03\nstatus: active\n\`\`\`\n\n${e.body}\n`,
439
+ );
440
+ return `<!-- knowledge-layer: L0 -->\n\n# Q&A Log (L0)\n\n${blocks.join("\n")}`;
441
+ }
442
+
443
+ test("forge.upgrade preserves project-accumulated knowledge entries (append-only merge)", async () => {
444
+ await setupForgeSource(tempDir, "0.2.0");
445
+ // Declare a knowledge file on the fixture skill
446
+ await writeFile(
447
+ join(tempDir, "packages", "forge", "skills", "fo", "fo-idea", "SKILL.md"),
448
+ "---\nname: fo-idea\nknowledge: [qa-log.md]\n---\n# fo-idea\n",
449
+ "utf8",
450
+ );
451
+ // Package ships K-0001 + new K-0003
452
+ await writeFile(
453
+ join(tempDir, "packages", "forge", "skills", "fo", "fo-idea", "qa-log.md"),
454
+ qaLog([
455
+ { id: "K-0001", title: "First", body: "Package body." },
456
+ { id: "K-0003", title: "Third", body: "New package entry." },
457
+ ]),
458
+ "utf8",
459
+ );
460
+ await setupConsumerForgeYaml(tempDir, null);
461
+
462
+ // Local copy already accumulated: edited K-0001 + project-only K-0002
463
+ await mkdir(join(tempDir, ".agents", "skills", "fo-idea"), { recursive: true });
464
+ await writeFile(
465
+ join(tempDir, ".agents", "skills", "fo-idea", "qa-log.md"),
466
+ qaLog([
467
+ { id: "K-0001", title: "First", body: "Locally edited body." },
468
+ { id: "K-0002", title: "Local record", body: "Project-accumulated entry." },
469
+ ]),
470
+ "utf8",
471
+ );
472
+
473
+ const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
474
+
475
+ expect(result.data?.status).toBe("pass");
476
+ const merged = await readFile(join(tempDir, ".agents", "skills", "fo-idea", "qa-log.md"), "utf8");
477
+ // Local entries preserved — the data-loss bug would have wiped K-0002
478
+ expect(merged).toContain("K-0002");
479
+ expect(merged).toContain("Project-accumulated entry.");
480
+ // Local version of the conflicting entry wins
481
+ expect(merged).toContain("Locally edited body.");
482
+ expect(merged).not.toContain("Package body.");
483
+ // New package entry appended
484
+ expect(merged).toContain("K-0003");
485
+ expect(merged).toContain("New package entry.");
486
+ });
package/src/types.ts CHANGED
@@ -16,6 +16,7 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
16
16
  <item>RFC-1097: sweep — werkstatt-engine clean
17
17
 
18
18
  Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.</item>
19
+ <item>RFC-1147: ForgeRegisteredCommandInfo.provider narrowed to "workspace" and siteName field removed — forge never enumerated sites; mirrors the KernelRegisteredCommandInfo cleanup.</item>
19
20
  </CHANGE_SUMMARY>
20
21
  */
21
22
 
@@ -112,8 +113,7 @@ export interface GateMetadata {
112
113
 
113
114
  export interface ForgeRegisteredCommandInfo extends ForgeCommandMetadata {
114
115
  name: string;
115
- provider: "workspace" | "site";
116
- siteName?: string;
116
+ provider: "workspace";
117
117
  module?: string;
118
118
  flags?: Record<string, ForgeFlagSpec>;
119
119
  reads?: string[];
@@ -56,7 +56,7 @@ function sleep(ms: number): Promise<void> {
56
56
  function makeTempPath(filePath: string): string {
57
57
  const dir = dirname(filePath);
58
58
  const name = basename(filePath);
59
- const random = randomBytes(6).toString("hex");
59
+ const random = Array.from(randomBytes(6), (b) => b.toString(16).padStart(2, "0")).join("");
60
60
  return join(dir, `${name}.${random}.tmp`);
61
61
  }
62
62