@warpgogol/forge 0.4.0 → 0.5.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 (137) hide show
  1. package/AGENTS.md +116 -4
  2. package/dist/os/adr/handlers/validate.d.ts.map +1 -1
  3. package/dist/os/adr/handlers/validate.js +31 -3
  4. package/dist/os/adr/handlers/validate.js.map +1 -1
  5. package/dist/os/core/core.module.d.ts.map +1 -1
  6. package/dist/os/core/core.module.js +76 -2
  7. package/dist/os/core/core.module.js.map +1 -1
  8. package/dist/os/core/handlers/knowledge-compact.d.ts +4 -0
  9. package/dist/os/core/handlers/knowledge-compact.d.ts.map +1 -0
  10. package/dist/os/core/handlers/knowledge-compact.js +198 -0
  11. package/dist/os/core/handlers/knowledge-compact.js.map +1 -0
  12. package/dist/os/rfc/handlers/lifecycle.d.ts.map +1 -1
  13. package/dist/os/rfc/handlers/lifecycle.js +3 -4
  14. package/dist/os/rfc/handlers/lifecycle.js.map +1 -1
  15. package/dist/os/rfc/handlers/validate-rules.d.ts.map +1 -1
  16. package/dist/os/rfc/handlers/validate-rules.js +41 -3
  17. package/dist/os/rfc/handlers/validate-rules.js.map +1 -1
  18. package/dist/os/spec/spec-schema.d.ts +1 -1
  19. package/dist/src/config/forge-config.d.ts +67 -3
  20. package/dist/src/config/forge-config.d.ts.map +1 -1
  21. package/dist/src/config/forge-config.js +86 -2
  22. package/dist/src/config/forge-config.js.map +1 -1
  23. package/dist/src/index.d.ts +3 -1
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +6 -2
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/knowledge/budgets.d.ts +38 -0
  28. package/dist/src/knowledge/budgets.d.ts.map +1 -0
  29. package/dist/src/knowledge/budgets.js +157 -0
  30. package/dist/src/knowledge/budgets.js.map +1 -0
  31. package/dist/src/knowledge/compact.d.ts +47 -0
  32. package/dist/src/knowledge/compact.d.ts.map +1 -0
  33. package/dist/src/knowledge/compact.js +322 -0
  34. package/dist/src/knowledge/compact.js.map +1 -0
  35. package/dist/src/knowledge/index.d.ts +7 -0
  36. package/dist/src/knowledge/index.d.ts.map +1 -0
  37. package/dist/src/knowledge/index.js +19 -0
  38. package/dist/src/knowledge/index.js.map +1 -0
  39. package/dist/src/knowledge/parse.d.ts +3 -0
  40. package/dist/src/knowledge/parse.d.ts.map +1 -0
  41. package/dist/src/knowledge/parse.js +205 -0
  42. package/dist/src/knowledge/parse.js.map +1 -0
  43. package/dist/src/knowledge/promote.d.ts +37 -0
  44. package/dist/src/knowledge/promote.d.ts.map +1 -0
  45. package/dist/src/knowledge/promote.js +120 -0
  46. package/dist/src/knowledge/promote.js.map +1 -0
  47. package/dist/src/knowledge/schema.d.ts +61 -0
  48. package/dist/src/knowledge/schema.d.ts.map +1 -0
  49. package/dist/src/knowledge/schema.js +64 -0
  50. package/dist/src/knowledge/schema.js.map +1 -0
  51. package/dist/src/knowledge/serialize.d.ts +3 -0
  52. package/dist/src/knowledge/serialize.d.ts.map +1 -0
  53. package/dist/src/knowledge/serialize.js +67 -0
  54. package/dist/src/knowledge/serialize.js.map +1 -0
  55. package/dist/src/onboarding/agents-generate.d.ts +15 -0
  56. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  57. package/dist/src/onboarding/agents-generate.js +118 -58
  58. package/dist/src/onboarding/agents-generate.js.map +1 -1
  59. package/dist/src/onboarding/create.d.ts.map +1 -1
  60. package/dist/src/onboarding/create.js +62 -3
  61. package/dist/src/onboarding/create.js.map +1 -1
  62. package/dist/src/onboarding/doctor.d.ts +13 -1
  63. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  64. package/dist/src/onboarding/doctor.js +527 -11
  65. package/dist/src/onboarding/doctor.js.map +1 -1
  66. package/dist/src/onboarding/init.d.ts +8 -1
  67. package/dist/src/onboarding/init.d.ts.map +1 -1
  68. package/dist/src/onboarding/init.js +33 -2
  69. package/dist/src/onboarding/init.js.map +1 -1
  70. package/dist/src/onboarding/memory-scaffold.d.ts +18 -0
  71. package/dist/src/onboarding/memory-scaffold.d.ts.map +1 -0
  72. package/dist/src/onboarding/memory-scaffold.js +151 -0
  73. package/dist/src/onboarding/memory-scaffold.js.map +1 -0
  74. package/dist/src/onboarding/nested-agents-generate.d.ts +6 -2
  75. package/dist/src/onboarding/nested-agents-generate.d.ts.map +1 -1
  76. package/dist/src/onboarding/nested-agents-generate.js +34 -9
  77. package/dist/src/onboarding/nested-agents-generate.js.map +1 -1
  78. package/dist/src/onboarding/nested-agents-templates.d.ts +23 -1
  79. package/dist/src/onboarding/nested-agents-templates.d.ts.map +1 -1
  80. package/dist/src/onboarding/nested-agents-templates.js +138 -8
  81. package/dist/src/onboarding/nested-agents-templates.js.map +1 -1
  82. package/dist/src/onboarding/profile-validate.d.ts +13 -0
  83. package/dist/src/onboarding/profile-validate.d.ts.map +1 -0
  84. package/dist/src/onboarding/profile-validate.js +109 -0
  85. package/dist/src/onboarding/profile-validate.js.map +1 -0
  86. package/dist/src/onboarding/upgrade.d.ts +5 -0
  87. package/dist/src/onboarding/upgrade.d.ts.map +1 -1
  88. package/dist/src/onboarding/upgrade.js +27 -1
  89. package/dist/src/onboarding/upgrade.js.map +1 -1
  90. package/dist/src/onboarding/workspace-discovery.d.ts +4 -3
  91. package/dist/src/onboarding/workspace-discovery.d.ts.map +1 -1
  92. package/dist/src/onboarding/workspace-discovery.js +51 -3
  93. package/dist/src/onboarding/workspace-discovery.js.map +1 -1
  94. package/dist/src/profiles/profile-schema.d.ts +116 -0
  95. package/dist/src/profiles/profile-schema.d.ts.map +1 -0
  96. package/dist/src/profiles/profile-schema.js +94 -0
  97. package/dist/src/profiles/profile-schema.js.map +1 -0
  98. package/dist/src/profiles/stack-profile.d.ts +41 -1
  99. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  100. package/dist/src/profiles/stack-profile.js +9 -0
  101. package/dist/src/profiles/stack-profile.js.map +1 -1
  102. package/dist/src/profiles/terminology-utils.d.ts +8 -0
  103. package/dist/src/profiles/terminology-utils.d.ts.map +1 -0
  104. package/dist/src/profiles/terminology-utils.js +31 -0
  105. package/dist/src/profiles/terminology-utils.js.map +1 -0
  106. package/dist/src/validators/skill-validate.d.ts +15 -0
  107. package/dist/src/validators/skill-validate.d.ts.map +1 -1
  108. package/dist/src/validators/skill-validate.js +204 -2
  109. package/dist/src/validators/skill-validate.js.map +1 -1
  110. package/package.json +11 -3
  111. package/profiles/astro-typescript-turborepo.yaml +87 -0
  112. package/profiles/editframe-html-templates/composition-agents.md +34 -0
  113. package/profiles/editframe-html-templates/composition.html +32 -0
  114. package/profiles/editframe-html.yaml +283 -0
  115. package/profiles/forge-shell.yaml +77 -0
  116. package/profiles/phaser-turborepo.yaml +87 -0
  117. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  118. package/skills/fo/fo-handoff/SKILL.md +2 -1
  119. package/skills/fo/fo-idea-implement/SKILL.md +44 -2
  120. package/skills/fo/fo-idea-plan/SKILL.md +1 -0
  121. package/skills/fo/fo-knowledge-distill/SKILL.md +170 -0
  122. package/skills/fo/fo-memory-sync/SKILL.md +6 -3
  123. package/skills/fo/fo-memory-sync/fix-patterns.md +38 -4
  124. package/skills/fo/fo-review/SKILL.md +2 -2
  125. package/skills/fo/fo-session-retro/SKILL.md +21 -11
  126. package/skills/fo/fo-session-save/SKILL.md +1 -1
  127. package/skills/fo/fo-session-save/fix-patterns.md +53 -29
  128. package/skills/fo/fo-session-save/learned-principles.md +59 -8
  129. package/skills/meta/forge-bootstrap/SKILL.md +16 -4
  130. package/skills/meta/forge-bootstrap/operator-profile-template.md +1 -0
  131. package/skills/meta/skill-create/SKILL.md +1 -1
  132. package/skills/shared/grilling/SKILL.md +1 -1
  133. package/skills/shared/grilling/learned-principles.md +14 -0
  134. package/skills/shared/grilling/qa-log.md +105 -6
  135. package/skills/shared/knowledge/learned-principles.md +5 -0
  136. package/skills/shared/windows-ai-tooling/SKILL.md +154 -1
  137. package/skills/shared/writing-great-skills/SKILL.md +74 -1
@@ -15,14 +15,28 @@ Checks for forge.yaml, AGENTS.md, PREFERENCES.md, .agents/skills/, docs/rfcs/,
15
15
  <item>RFC-0539: extended knowledge file check to iterate pack skills via discoverPackSkills. Added pack-skills check for stale/missing copies and skillPacks config validation.</item>
16
16
  <item>RFC-0540: added defaultable-binding-null notices for forge-CLI-backed bindings.</item>
17
17
  <item>RFC-0611: added nested AGENTS.md checks — missing, stale (dryRun comparison), hand-written improvement.</item>
18
+ <item>RFC-0640: added domain reporting, invariant listing (reported-only), terminology resolution, --strict flag, and software-specific check skipping for non-software domains.</item>
19
+ <item>RFC-0660: added legacy-section count reporting for structured knowledge files.</item>
20
+ <item>RFC-0661: added knowledge-budgets check — validates override shape, computes per-skill budget reports, reports summary with headroom %.</item>
21
+ <item>RFC-0663: added knowledge-duplicates check (cross-skill L2 duplicate detection) and shared-knowledge-file check (schema/id uniqueness for the shared layer).</item>
22
+ <item>RFC-0664: added memory-layer health check (budget usage, gitignore coverage, daily-file leak risk).</item>
18
23
  </CHANGE_SUMMARY>
19
24
  */
20
25
  import { readFile, readdir, stat } from "node:fs/promises";
26
+ import { readFileSync } from "node:fs";
21
27
  import { join, relative, dirname } from "node:path";
22
- import { resolveForgeRoot, loadForgeConfig, resolveBinding, FORGE_CLI_BINDING_DEFAULTS, resolvePmRunner } from "../config/forge-config.js";
28
+ import { parse as parseYaml } from "yaml";
29
+ import { resolveForgeRoot, loadForgeConfig, resolveBinding, resolveTerminology, FORGE_CLI_BINDING_DEFAULTS, resolvePmRunner } from "../config/forge-config.js";
23
30
  import { FORGE_SKILLS, discoverPackSkills } from "../registry.js";
24
31
  import { discoverWorkspaces } from "./workspace-discovery.js";
25
32
  import { buildNestedAgentsMd } from "./nested-agents-templates.js";
33
+ import { listStackProfiles } from "../profiles/stack-profile.js";
34
+ import { TERMINOLOGY_DEFAULTS } from "../profiles/profile-schema.js";
35
+ import { runProfileValidate } from "./profile-validate.js";
36
+ import { parseKnowledgeFile } from "../knowledge/index.js";
37
+ import { computeLayerBudgets, resolveKnowledgeBudgets, DEFAULT_KNOWLEDGE_BUDGETS } from "../knowledge/budgets.js";
38
+ import { detectDuplicatePrinciples } from "../knowledge/index.js";
39
+ import { checkMemoryLayerHealth } from "./memory-scaffold.js";
26
40
  async function pathExists(target) {
27
41
  try {
28
42
  await stat(target);
@@ -205,6 +219,294 @@ async function checkStaleKnowledgeFiles(workspaceRoot, forgeRoot) {
205
219
  };
206
220
  }
207
221
  // ---------------------------------------------------------------------------
222
+ // Legacy section detection (RFC-0660)
223
+ // ---------------------------------------------------------------------------
224
+ function checkLegacyKnowledgeSections(workspaceRoot, forgeRoot) {
225
+ let config;
226
+ try {
227
+ config = loadForgeConfig(workspaceRoot);
228
+ }
229
+ catch {
230
+ return { name: "knowledge-legacy", status: "pass", message: "No forge.yaml — legacy section check skipped" };
231
+ }
232
+ let totalLegacy = 0;
233
+ let filesWithLegacy = 0;
234
+ // Check forge skills
235
+ for (const skill of FORGE_SKILLS) {
236
+ if (!skill.knowledge || skill.knowledge.length === 0)
237
+ continue;
238
+ const skillDir = join(forgeRoot, dirname(skill.path));
239
+ for (const kf of skill.knowledge) {
240
+ const kfPath = join(skillDir, kf);
241
+ try {
242
+ const parsed = parseKnowledgeFile(kfPath);
243
+ if (parsed.isKnowledgeAdjacent)
244
+ continue;
245
+ if (parsed.legacySections.length > 0) {
246
+ totalLegacy += parsed.legacySections.length;
247
+ filesWithLegacy++;
248
+ }
249
+ }
250
+ catch {
251
+ // File read errors are handled by stale knowledge check
252
+ }
253
+ }
254
+ }
255
+ // Check pack skills
256
+ const packSkills = discoverPackSkills(workspaceRoot, config);
257
+ for (const skill of packSkills) {
258
+ if (!skill.knowledge || skill.knowledge.length === 0)
259
+ continue;
260
+ const skillDir = join(workspaceRoot, skill.dir, dirname(skill.path));
261
+ for (const kf of skill.knowledge) {
262
+ const kfPath = join(skillDir, kf);
263
+ try {
264
+ const parsed = parseKnowledgeFile(kfPath);
265
+ if (parsed.isKnowledgeAdjacent)
266
+ continue;
267
+ if (parsed.legacySections.length > 0) {
268
+ totalLegacy += parsed.legacySections.length;
269
+ filesWithLegacy++;
270
+ }
271
+ }
272
+ catch {
273
+ // File read errors are handled by stale knowledge check
274
+ }
275
+ }
276
+ }
277
+ return {
278
+ name: "knowledge-legacy",
279
+ status: totalLegacy === 0 ? "pass" : "warn",
280
+ message: totalLegacy === 0
281
+ ? "No legacy knowledge sections"
282
+ : `${totalLegacy} legacy section${totalLegacy === 1 ? "" : "s"} across ${filesWithLegacy} knowledge file${filesWithLegacy === 1 ? "" : "s"} — run the knowledge compaction command to migrate`,
283
+ };
284
+ }
285
+ // ---------------------------------------------------------------------------
286
+ // Knowledge budget check (RFC-0661)
287
+ // ---------------------------------------------------------------------------
288
+ function checkKnowledgeBudgets(workspaceRoot, forgeRoot) {
289
+ let config;
290
+ try {
291
+ config = loadForgeConfig(workspaceRoot);
292
+ }
293
+ catch {
294
+ return { name: "knowledge-budgets", status: "pass", message: "No forge.yaml — budget check skipped" };
295
+ }
296
+ // Validate override shape if present
297
+ const overrideWarnings = [];
298
+ const effectiveBudgets = resolveKnowledgeBudgets(workspaceRoot);
299
+ let usingDefaults = true;
300
+ try {
301
+ const forgeYamlPath = join(workspaceRoot, "forge.yaml");
302
+ const content = readFileSync(forgeYamlPath, "utf8");
303
+ const parsed = parseYaml(content);
304
+ const bindings = parsed?.bindings;
305
+ const knowledge = bindings?.knowledge;
306
+ const budgets = knowledge?.budgets;
307
+ if (budgets) {
308
+ usingDefaults = false;
309
+ if (typeof budgets.hot !== "number" || budgets.hot <= 0 || !Number.isInteger(budgets.hot)) {
310
+ overrideWarnings.push(`bindings.knowledge.budgets.hot invalid (got ${String(budgets.hot)}), using default ${DEFAULT_KNOWLEDGE_BUDGETS.hot}`);
311
+ }
312
+ if (typeof budgets.warm !== "number" || budgets.warm <= 0 || !Number.isInteger(budgets.warm)) {
313
+ overrideWarnings.push(`bindings.knowledge.budgets.warm invalid (got ${String(budgets.warm)}), using default ${DEFAULT_KNOWLEDGE_BUDGETS.warm}`);
314
+ }
315
+ }
316
+ }
317
+ catch {
318
+ // forge.yaml not readable — defaults already applied
319
+ }
320
+ // Collect all parsed knowledge files from forge skills and pack skills
321
+ const parsedFiles = [];
322
+ for (const skill of FORGE_SKILLS) {
323
+ if (!skill.knowledge || skill.knowledge.length === 0)
324
+ continue;
325
+ const skillDir = join(forgeRoot, dirname(skill.path));
326
+ for (const kf of skill.knowledge) {
327
+ const kfPath = join(skillDir, kf);
328
+ try {
329
+ const parsed = parseKnowledgeFile(kfPath);
330
+ if (parsed.layer !== null && parsed.layer !== "L0" && !parsed.isKnowledgeAdjacent && parsed.parseIssues.length === 0) {
331
+ parsedFiles.push({ file: parsed, skillName: skill.name });
332
+ }
333
+ }
334
+ catch {
335
+ // File read errors are handled by stale knowledge check
336
+ }
337
+ }
338
+ }
339
+ const packSkills = discoverPackSkills(workspaceRoot, config);
340
+ for (const skill of packSkills) {
341
+ if (!skill.knowledge || skill.knowledge.length === 0)
342
+ continue;
343
+ const skillDir = join(workspaceRoot, skill.dir, dirname(skill.path));
344
+ for (const kf of skill.knowledge) {
345
+ const kfPath = join(skillDir, kf);
346
+ try {
347
+ const parsed = parseKnowledgeFile(kfPath);
348
+ if (parsed.layer !== null && parsed.layer !== "L0" && !parsed.isKnowledgeAdjacent && parsed.parseIssues.length === 0) {
349
+ parsedFiles.push({ file: parsed, skillName: skill.name, pack: skill.pack });
350
+ }
351
+ }
352
+ catch {
353
+ // File read errors are handled by stale knowledge check
354
+ }
355
+ }
356
+ }
357
+ // Compute budget reports
358
+ const filesForBudget = parsedFiles.map((p) => p.file);
359
+ const skillNames = new Map();
360
+ for (const p of parsedFiles) {
361
+ skillNames.set(p.file.path, p.skillName);
362
+ }
363
+ const reports = computeLayerBudgets(filesForBudget, effectiveBudgets, skillNames);
364
+ // Build summary
365
+ const budgetSource = usingDefaults ? "defaults" : "override";
366
+ const parts = [`budgets: hot=${effectiveBudgets.hot} warm=${effectiveBudgets.warm} (${budgetSource})`];
367
+ if (reports.length > 0) {
368
+ const summary = reports.map((r) => {
369
+ const pct = Math.round((r.activeChars / r.budget) * 100);
370
+ return `${r.skill}/${r.file}[${r.layer}]: ${r.activeChars}/${r.budget} (${pct}%)`;
371
+ });
372
+ parts.push(`${reports.length} file${reports.length === 1 ? "" : "s"}: ${summary.join(", ")}`);
373
+ }
374
+ else {
375
+ parts.push("no hot/warm knowledge files to check");
376
+ }
377
+ if (overrideWarnings.length > 0) {
378
+ parts.push(overrideWarnings.join("; "));
379
+ }
380
+ const status = overrideWarnings.length > 0 ? "warn" : "pass";
381
+ return {
382
+ name: "knowledge-budgets",
383
+ status,
384
+ message: parts.join(" | "),
385
+ };
386
+ }
387
+ // ---------------------------------------------------------------------------
388
+ // Knowledge duplicate detection (RFC-0663)
389
+ // ---------------------------------------------------------------------------
390
+ function collectL2Files(workspaceRoot, forgeRoot) {
391
+ let config;
392
+ try {
393
+ config = loadForgeConfig(workspaceRoot);
394
+ }
395
+ catch {
396
+ return [];
397
+ }
398
+ const files = [];
399
+ for (const skill of FORGE_SKILLS) {
400
+ if (!skill.knowledge || skill.knowledge.length === 0)
401
+ continue;
402
+ const skillDir = join(forgeRoot, dirname(skill.path));
403
+ for (const kf of skill.knowledge) {
404
+ if (kf !== "learned-principles.md")
405
+ continue;
406
+ const kfPath = join(skillDir, kf);
407
+ try {
408
+ const parsed = parseKnowledgeFile(kfPath);
409
+ if (parsed.isKnowledgeAdjacent)
410
+ continue;
411
+ if (parsed.parseIssues.length > 0)
412
+ continue;
413
+ files.push({ skill: skill.name, parsed });
414
+ }
415
+ catch {
416
+ // File read errors are handled by stale knowledge check
417
+ }
418
+ }
419
+ }
420
+ const packSkills = discoverPackSkills(workspaceRoot, config);
421
+ for (const skill of packSkills) {
422
+ if (!skill.knowledge || skill.knowledge.length === 0)
423
+ continue;
424
+ const skillDir = join(workspaceRoot, skill.dir, dirname(skill.path));
425
+ for (const kf of skill.knowledge) {
426
+ if (kf !== "learned-principles.md")
427
+ continue;
428
+ const kfPath = join(skillDir, kf);
429
+ try {
430
+ const parsed = parseKnowledgeFile(kfPath);
431
+ if (parsed.isKnowledgeAdjacent)
432
+ continue;
433
+ if (parsed.parseIssues.length > 0)
434
+ continue;
435
+ files.push({ skill: skill.name, parsed });
436
+ }
437
+ catch {
438
+ // File read errors are handled by stale knowledge check
439
+ }
440
+ }
441
+ }
442
+ const sharedPath = join(forgeRoot, "skills", "shared", "knowledge", "learned-principles.md");
443
+ try {
444
+ const parsed = parseKnowledgeFile(sharedPath);
445
+ if (!parsed.isKnowledgeAdjacent && parsed.parseIssues.length === 0 && parsed.entries.length > 0) {
446
+ files.push({ skill: "shared", parsed });
447
+ }
448
+ }
449
+ catch {
450
+ // Shared layer not initialized — not an error
451
+ }
452
+ return files;
453
+ }
454
+ function checkKnowledgeDuplicates(workspaceRoot, forgeRoot) {
455
+ const l2Files = collectL2Files(workspaceRoot, forgeRoot);
456
+ if (l2Files.length === 0) {
457
+ return { name: "knowledge-duplicates", status: "pass", message: "No L2 knowledge files to check" };
458
+ }
459
+ const pairs = detectDuplicatePrinciples(l2Files);
460
+ if (pairs.length === 0) {
461
+ return { name: "knowledge-duplicates", status: "pass", message: "No cross-skill duplicate principles detected" };
462
+ }
463
+ const pairSummaries = pairs.map((p) => `${p.a.skill}/${p.a.entryId} ↔ ${p.b.skill}/${p.b.entryId} (${p.kind})`);
464
+ return {
465
+ name: "knowledge-duplicates",
466
+ status: "warn",
467
+ message: `${pairs.length} duplicate pair${pairs.length === 1 ? "" : "s"} detected — run fo-knowledge-distill to promote: ${pairSummaries.join("; ")}`,
468
+ };
469
+ }
470
+ // ---------------------------------------------------------------------------
471
+ // Shared knowledge file validation (RFC-0663)
472
+ // ---------------------------------------------------------------------------
473
+ function checkSharedKnowledgeFile(workspaceRoot, forgeRoot) {
474
+ const sharedPath = join(forgeRoot, "skills", "shared", "knowledge", "learned-principles.md");
475
+ let parsed;
476
+ try {
477
+ parsed = parseKnowledgeFile(sharedPath);
478
+ }
479
+ catch {
480
+ return { name: "shared-knowledge-file", status: "pass", message: "Shared knowledge layer not initialized — created on first promotion" };
481
+ }
482
+ if (parsed.isKnowledgeAdjacent) {
483
+ return { name: "shared-knowledge-file", status: "pass", message: "Shared knowledge layer is empty (no structured entries)" };
484
+ }
485
+ const issues = [];
486
+ for (const issue of parsed.parseIssues) {
487
+ issues.push(`line ${issue.line}: ${issue.message}`);
488
+ }
489
+ const seenIds = new Set();
490
+ for (const entry of parsed.entries) {
491
+ if (seenIds.has(entry.meta.id)) {
492
+ issues.push(`duplicate entry id: ${entry.meta.id}`);
493
+ }
494
+ seenIds.add(entry.meta.id);
495
+ }
496
+ if (issues.length > 0) {
497
+ return {
498
+ name: "shared-knowledge-file",
499
+ status: "fail",
500
+ message: `${issues.length} issue${issues.length === 1 ? "" : "s"}: ${issues.join("; ")}`,
501
+ };
502
+ }
503
+ return {
504
+ name: "shared-knowledge-file",
505
+ status: "pass",
506
+ message: `Shared knowledge layer: ${parsed.entries.length} entr${parsed.entries.length === 1 ? "y" : "ies"}`,
507
+ };
508
+ }
509
+ // ---------------------------------------------------------------------------
208
510
  // Pack skill diagnostics (RFC-0539)
209
511
  // ---------------------------------------------------------------------------
210
512
  async function checkPackSkills(workspaceRoot) {
@@ -269,7 +571,7 @@ async function checkPackSkills(workspaceRoot) {
269
571
  // ---------------------------------------------------------------------------
270
572
  // Nested AGENTS.md diagnostics (RFC-0611)
271
573
  // ---------------------------------------------------------------------------
272
- async function checkNestedAgentsMd(workspaceRoot) {
574
+ async function checkNestedAgentsMd(workspaceRoot, workspaceTypes) {
273
575
  let config;
274
576
  try {
275
577
  config = loadForgeConfig(workspaceRoot);
@@ -281,7 +583,7 @@ async function checkNestedAgentsMd(workspaceRoot) {
281
583
  message: "forge.yaml not loadable — nested AGENTS.md check skipped",
282
584
  };
283
585
  }
284
- const workspaces = discoverWorkspaces(workspaceRoot);
586
+ const workspaces = discoverWorkspaces(workspaceRoot, workspaceTypes);
285
587
  if (workspaces.length === 0) {
286
588
  return {
287
589
  name: "nested-AGENTS.md",
@@ -349,8 +651,151 @@ async function checkNestedAgentsMd(workspaceRoot) {
349
651
  message: `${parts.join(", ")} — ${issues.slice(0, 3).join("; ")}${issues.length > 3 ? ` (+${issues.length - 3} more)` : ""}`,
350
652
  };
351
653
  }
352
- export async function runDoctor(_input, context) {
654
+ function buildTerminologyMap(config, profileTerminology) {
655
+ const result = {};
656
+ for (const key of Object.keys(TERMINOLOGY_DEFAULTS)) {
657
+ result[key] = resolveTerminology(config, profileTerminology, key);
658
+ }
659
+ // Also include any custom keys from bindings.terminology
660
+ if (config.bindings?.terminology) {
661
+ for (const key of Object.keys(config.bindings.terminology)) {
662
+ if (!(key in result)) {
663
+ result[key] = resolveTerminology(config, profileTerminology, key);
664
+ }
665
+ }
666
+ }
667
+ return result;
668
+ }
669
+ function resolveDomainReport(workspaceRoot, forgeRoot) {
670
+ // 1. Try forge.yaml domain field
671
+ try {
672
+ const config = loadForgeConfig(workspaceRoot);
673
+ if (config.project.domain) {
674
+ const terminology = buildTerminologyMap(config);
675
+ const register = readRegisterFromPrefs(workspaceRoot);
676
+ return {
677
+ domain: config.project.domain,
678
+ register,
679
+ terminology,
680
+ invariants: [],
681
+ source: "forge.yaml",
682
+ };
683
+ }
684
+ }
685
+ catch {
686
+ // forge.yaml not loadable
687
+ }
688
+ // 2. Try to find domain from stack profile matching config.project.stack
689
+ try {
690
+ const config = loadForgeConfig(workspaceRoot);
691
+ const profiles = listStackProfiles(forgeRoot);
692
+ for (const stackId of config.project.stack) {
693
+ const profile = profiles.find((p) => p.id === stackId);
694
+ if (profile?.domain) {
695
+ const terminology = buildTerminologyMap(config, profile.terminology);
696
+ const register = profile.register ?? readRegisterFromPrefs(workspaceRoot);
697
+ return {
698
+ domain: profile.domain,
699
+ register,
700
+ terminology,
701
+ invariants: profile.invariants ?? [],
702
+ source: "profile",
703
+ };
704
+ }
705
+ }
706
+ }
707
+ catch {
708
+ // config or profiles not loadable
709
+ }
710
+ // 3. Default: software domain
711
+ return {
712
+ domain: "software",
713
+ register: null,
714
+ terminology: {},
715
+ invariants: [],
716
+ source: "default",
717
+ };
718
+ }
719
+ function readRegisterFromPrefs(workspaceRoot) {
720
+ try {
721
+ const prefsPath = join(workspaceRoot, "PREFERENCES.md");
722
+ const content = readFileSync(prefsPath, "utf8");
723
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
724
+ if (fmMatch) {
725
+ const fm = parseYaml(fmMatch[1]);
726
+ if (fm["register"] === "creative")
727
+ return "creative";
728
+ if (fm["register"] === "business")
729
+ return "business";
730
+ }
731
+ }
732
+ catch {
733
+ // PREFERENCES.md missing or unreadable
734
+ }
735
+ return null;
736
+ }
737
+ function checkDomainInfo(domainReport) {
738
+ const parts = [`domain: ${domainReport.domain}`, `source: ${domainReport.source}`];
739
+ if (domainReport.register) {
740
+ parts.push(`register: ${domainReport.register}`);
741
+ }
742
+ if (Object.keys(domainReport.terminology).length > 0) {
743
+ const terms = Object.entries(domainReport.terminology)
744
+ .map(([k, v]) => `${k}=${v}`)
745
+ .join(", ");
746
+ parts.push(`terminology: ${terms}`);
747
+ }
748
+ if (domainReport.invariants.length > 0) {
749
+ parts.push(`invariants: ${domainReport.invariants.length} declared`);
750
+ }
751
+ return {
752
+ name: "domain-info",
753
+ status: "pass",
754
+ message: parts.join(", "),
755
+ };
756
+ }
757
+ // RFC-0664: Memory layer health check
758
+ function checkMemoryLayer(workspaceRoot) {
759
+ const health = checkMemoryLayerHealth(workspaceRoot);
760
+ if (!health.memoryMdExists) {
761
+ return {
762
+ name: "memory-layer",
763
+ status: "pass",
764
+ message: "Memory layer not initialized — run 'forge create' or 'forge upgrade' to scaffold .agents/memory/",
765
+ };
766
+ }
767
+ const overBudget = health.memoryMdChars > health.budget;
768
+ const leakRisk = !health.gitignoreCoversDaily && health.dailyFileCount > 0;
769
+ if (overBudget && leakRisk) {
770
+ return {
771
+ name: "memory-layer",
772
+ status: "warn",
773
+ message: `MEMORY.md over budget (${health.memoryMdChars}/${health.budget} chars); ${health.dailyFileCount} daily file(s) untracked by .gitignore — add forge-agent-memory block`,
774
+ };
775
+ }
776
+ if (overBudget) {
777
+ return {
778
+ name: "memory-layer",
779
+ status: "warn",
780
+ message: `MEMORY.md over budget (${health.memoryMdChars}/${health.budget} chars) — compact or promote to skill knowledge`,
781
+ };
782
+ }
783
+ if (leakRisk) {
784
+ return {
785
+ name: "memory-layer",
786
+ status: "warn",
787
+ message: `${health.dailyFileCount} daily file(s) not covered by .gitignore — add forge-agent-memory block to prevent committing ephemeral logs`,
788
+ };
789
+ }
790
+ return {
791
+ name: "memory-layer",
792
+ status: "pass",
793
+ message: `MEMORY.md ${health.memoryMdChars}/${health.budget} chars; ${health.dailyFileCount} daily file(s); gitignore ${health.gitignoreCoversDaily ? "covers" : "absent (no daily files)"}`,
794
+ };
795
+ }
796
+ export async function runDoctor(input, context) {
353
797
  const { workspaceRoot, logger, outputFormat } = context;
798
+ const strict = input.flags["strict"] === true;
354
799
  const checks = [];
355
800
  // Check forge.yaml
356
801
  const forgeYamlExists = await pathExists(join(workspaceRoot, "forge.yaml"));
@@ -414,6 +859,29 @@ export async function runDoctor(_input, context) {
414
859
  catch {
415
860
  forgeRoot = join(workspaceRoot, "packages", "forge");
416
861
  }
862
+ // RFC-0640: Resolve domain report for domain-aware checks
863
+ const domainReport = resolveDomainReport(workspaceRoot, forgeRoot);
864
+ // RFC-0640: Domain info check
865
+ checks.push(checkDomainInfo(domainReport));
866
+ // RFC-0640: List invariants (reported-only, not automatically checked)
867
+ if (domainReport.invariants.length > 0) {
868
+ const invariantMessages = domainReport.invariants.map((inv) => `${inv.id}: ${inv.rule}`);
869
+ checks.push({
870
+ name: "domain-invariants",
871
+ status: "pass",
872
+ message: `${domainReport.invariants.length} invariant(s) declared (advisory): ${invariantMessages.slice(0, 3).join("; ")}${invariantMessages.length > 3 ? ` (+${invariantMessages.length - 3} more)` : ""}`,
873
+ });
874
+ }
875
+ // RFC-0640: Run forge.profile.validate as advisory check (warn on failure, not fail)
876
+ const profileValidateResult = await runProfileValidate({ argv: [], flags: {} }, { ...context, forgeRoot });
877
+ const invalidProfiles = profileValidateResult.data?.profiles.filter((p) => !p.valid) ?? [];
878
+ checks.push({
879
+ name: "profile-validate",
880
+ status: invalidProfiles.length === 0 ? "pass" : "warn",
881
+ message: invalidProfiles.length === 0
882
+ ? `${profileValidateResult.data?.profiles.length ?? 0} profile(s) valid`
883
+ : `${invalidProfiles.length} profile(s) invalid (advisory): ${invalidProfiles.map((p) => p.id).join(", ")}`,
884
+ });
417
885
  const forbiddenImports = await scanForForbiddenImports(forgeRoot, workspaceRoot);
418
886
  checks.push({
419
887
  name: "autonomy-guard",
@@ -434,17 +902,65 @@ export async function runDoctor(_input, context) {
434
902
  // Check stale knowledge files (RFC-0524, RFC-0539)
435
903
  const staleCheck = await checkStaleKnowledgeFiles(workspaceRoot, forgeRoot);
436
904
  checks.push(staleCheck);
905
+ // RFC-0660: Check legacy sections in knowledge files
906
+ const legacyCheck = checkLegacyKnowledgeSections(workspaceRoot, forgeRoot);
907
+ checks.push(legacyCheck);
908
+ // RFC-0661: Check knowledge layer token budgets
909
+ const budgetCheck = checkKnowledgeBudgets(workspaceRoot, forgeRoot);
910
+ checks.push(budgetCheck);
911
+ // RFC-0663: Check cross-skill duplicate principles
912
+ const duplicatesCheck = checkKnowledgeDuplicates(workspaceRoot, forgeRoot);
913
+ checks.push(duplicatesCheck);
914
+ // RFC-0663: Validate shared knowledge layer file
915
+ const sharedKnowledgeCheck = checkSharedKnowledgeFile(workspaceRoot, forgeRoot);
916
+ checks.push(sharedKnowledgeCheck);
917
+ // RFC-0664: Check memory layer health (budget, gitignore coverage, leak risk)
918
+ checks.push(checkMemoryLayer(workspaceRoot));
437
919
  // RFC-0539: Check pack skills — stale/missing copies and config validation
438
920
  const packCheck = await checkPackSkills(workspaceRoot);
439
921
  checks.push(packCheck);
440
922
  // RFC-0611: Check nested AGENTS.md — missing, stale, hand-written
441
- const nestedCheck = await checkNestedAgentsMd(workspaceRoot);
442
- checks.push(nestedCheck);
443
- const allPass = checks.every((c) => c.status === "pass");
444
- const hasFails = checks.some((c) => c.status === "fail");
923
+ // RFC-0640: Use profile-driven workspace types when available (all domains)
924
+ // RFC-0640 fix: previously gated by isSoftwareDomain, which skipped the check
925
+ // entirely for non-software domains. Now runs for all domains — profile-driven
926
+ // wsTypes replace hardcoded detection when present.
927
+ {
928
+ let wsTypes;
929
+ try {
930
+ const config = loadForgeConfig(workspaceRoot);
931
+ // RFC-0643: prefer config.profile (loaded by loadForgeConfig)
932
+ if (config.profile?.workspaceTypes && config.profile.workspaceTypes.length > 0) {
933
+ wsTypes = config.profile.workspaceTypes;
934
+ }
935
+ else {
936
+ const profiles = listStackProfiles(forgeRoot);
937
+ for (const stackId of config.project.stack) {
938
+ const profile = profiles.find((p) => p.id === stackId);
939
+ if (profile?.workspaceTypes && profile.workspaceTypes.length > 0) {
940
+ wsTypes = profile.workspaceTypes;
941
+ break;
942
+ }
943
+ }
944
+ }
945
+ }
946
+ catch {
947
+ // profiles not loadable
948
+ }
949
+ const nestedCheck = await checkNestedAgentsMd(workspaceRoot, wsTypes);
950
+ checks.push(nestedCheck);
951
+ }
952
+ // RFC-0640: --strict flag elevates warn to fail for domain-related checks
953
+ const finalChecks = strict
954
+ ? checks.map((c) => c.status === "warn" &&
955
+ (c.name === "domain-invariants" || c.name === "profile-validate")
956
+ ? { ...c, status: "fail" }
957
+ : c)
958
+ : checks;
959
+ const allPass = finalChecks.every((c) => c.status === "pass");
960
+ const hasFails = finalChecks.some((c) => c.status === "fail");
445
961
  if (outputFormat === "pretty") {
446
962
  logger.section(`Forge Doctor — ${workspaceRoot}`);
447
- for (const check of checks) {
963
+ for (const check of finalChecks) {
448
964
  const icon = check.status === "pass" ? "✓" : check.status === "warn" ? "⚠" : "✖";
449
965
  const fn = check.status === "pass" ? logger.success : check.status === "warn" ? logger.warn : logger.error;
450
966
  fn(`${icon} ${check.name}: ${check.message}`);
@@ -460,11 +976,11 @@ export async function runDoctor(_input, context) {
460
976
  }
461
977
  }
462
978
  return {
463
- data: { command: "forge.doctor", checks, allPass, forbiddenImports, bindings: bindingsResult },
979
+ data: { command: "forge.doctor", checks: finalChecks, allPass, forbiddenImports, bindings: bindingsResult, domain: domainReport },
464
980
  exitCode: hasFails ? 1 : 0,
465
981
  summary: allPass
466
982
  ? "forge.doctor: all checks passed"
467
- : `forge.doctor: ${checks.filter((c) => c.status === "fail").length} fail(s), ${checks.filter((c) => c.status === "warn").length} warn(s)`,
983
+ : `forge.doctor: ${finalChecks.filter((c) => c.status === "fail").length} fail(s), ${finalChecks.filter((c) => c.status === "warn").length} warn(s)`,
468
984
  };
469
985
  }
470
986
  //# sourceMappingURL=doctor.js.map