@warpgogol/forge 0.3.1 → 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 (180) hide show
  1. package/AGENTS.md +152 -5
  2. package/dist/bin/cli.js +4 -5
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/os/adr/adr.module.d.ts.map +1 -1
  5. package/dist/os/adr/adr.module.js +4 -2
  6. package/dist/os/adr/adr.module.js.map +1 -1
  7. package/dist/os/adr/handlers/validate.d.ts.map +1 -1
  8. package/dist/os/adr/handlers/validate.js +32 -4
  9. package/dist/os/adr/handlers/validate.js.map +1 -1
  10. package/dist/os/compass/compass.module.d.ts.map +1 -1
  11. package/dist/os/compass/compass.module.js +4 -0
  12. package/dist/os/compass/compass.module.js.map +1 -1
  13. package/dist/os/compass/handlers/resolve-scan-root.d.ts.map +1 -1
  14. package/dist/os/compass/handlers/resolve-scan-root.js +16 -0
  15. package/dist/os/compass/handlers/resolve-scan-root.js.map +1 -1
  16. package/dist/os/core/core.module.d.ts.map +1 -1
  17. package/dist/os/core/core.module.js +82 -3
  18. package/dist/os/core/core.module.js.map +1 -1
  19. package/dist/os/core/handlers/knowledge-compact.d.ts +4 -0
  20. package/dist/os/core/handlers/knowledge-compact.d.ts.map +1 -0
  21. package/dist/os/core/handlers/knowledge-compact.js +198 -0
  22. package/dist/os/core/handlers/knowledge-compact.js.map +1 -0
  23. package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
  24. package/dist/os/rfc/handlers/implement-stamp.js +10 -6
  25. package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
  26. package/dist/os/rfc/handlers/index-graph.js +2 -2
  27. package/dist/os/rfc/handlers/index-graph.js.map +1 -1
  28. package/dist/os/rfc/handlers/lifecycle.d.ts.map +1 -1
  29. package/dist/os/rfc/handlers/lifecycle.js +4 -5
  30. package/dist/os/rfc/handlers/lifecycle.js.map +1 -1
  31. package/dist/os/rfc/handlers/pipeline-status.js +1 -1
  32. package/dist/os/rfc/handlers/pipeline-status.js.map +1 -1
  33. package/dist/os/rfc/handlers/validate-rules.d.ts.map +1 -1
  34. package/dist/os/rfc/handlers/validate-rules.js +41 -3
  35. package/dist/os/rfc/handlers/validate-rules.js.map +1 -1
  36. package/dist/os/rfc/handlers/validate.js +1 -1
  37. package/dist/os/rfc/handlers/validate.js.map +1 -1
  38. package/dist/os/rfc/rfc.module.d.ts.map +1 -1
  39. package/dist/os/rfc/rfc.module.js +15 -10
  40. package/dist/os/rfc/rfc.module.js.map +1 -1
  41. package/dist/os/session/handlers/validate.js +1 -1
  42. package/dist/os/session/handlers/validate.js.map +1 -1
  43. package/dist/os/session/session.module.d.ts.map +1 -1
  44. package/dist/os/session/session.module.js +4 -2
  45. package/dist/os/session/session.module.js.map +1 -1
  46. package/dist/os/spec/spec-schema.d.ts +1 -1
  47. package/dist/os/werkstatt/handlers/lock.d.ts.map +1 -1
  48. package/dist/os/werkstatt/handlers/lock.js +27 -1
  49. package/dist/os/werkstatt/handlers/lock.js.map +1 -1
  50. package/dist/os/werkstatt/handlers/schema.d.ts +1 -0
  51. package/dist/os/werkstatt/handlers/schema.d.ts.map +1 -1
  52. package/dist/os/werkstatt/handlers/schema.js +1 -0
  53. package/dist/os/werkstatt/handlers/schema.js.map +1 -1
  54. package/dist/src/config/forge-config.d.ts +70 -3
  55. package/dist/src/config/forge-config.d.ts.map +1 -1
  56. package/dist/src/config/forge-config.js +91 -4
  57. package/dist/src/config/forge-config.js.map +1 -1
  58. package/dist/src/index.d.ts +3 -1
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/index.js +6 -2
  61. package/dist/src/index.js.map +1 -1
  62. package/dist/src/knowledge/budgets.d.ts +38 -0
  63. package/dist/src/knowledge/budgets.d.ts.map +1 -0
  64. package/dist/src/knowledge/budgets.js +157 -0
  65. package/dist/src/knowledge/budgets.js.map +1 -0
  66. package/dist/src/knowledge/compact.d.ts +47 -0
  67. package/dist/src/knowledge/compact.d.ts.map +1 -0
  68. package/dist/src/knowledge/compact.js +322 -0
  69. package/dist/src/knowledge/compact.js.map +1 -0
  70. package/dist/src/knowledge/index.d.ts +7 -0
  71. package/dist/src/knowledge/index.d.ts.map +1 -0
  72. package/dist/src/knowledge/index.js +19 -0
  73. package/dist/src/knowledge/index.js.map +1 -0
  74. package/dist/src/knowledge/parse.d.ts +3 -0
  75. package/dist/src/knowledge/parse.d.ts.map +1 -0
  76. package/dist/src/knowledge/parse.js +205 -0
  77. package/dist/src/knowledge/parse.js.map +1 -0
  78. package/dist/src/knowledge/promote.d.ts +37 -0
  79. package/dist/src/knowledge/promote.d.ts.map +1 -0
  80. package/dist/src/knowledge/promote.js +120 -0
  81. package/dist/src/knowledge/promote.js.map +1 -0
  82. package/dist/src/knowledge/schema.d.ts +61 -0
  83. package/dist/src/knowledge/schema.d.ts.map +1 -0
  84. package/dist/src/knowledge/schema.js +64 -0
  85. package/dist/src/knowledge/schema.js.map +1 -0
  86. package/dist/src/knowledge/serialize.d.ts +3 -0
  87. package/dist/src/knowledge/serialize.d.ts.map +1 -0
  88. package/dist/src/knowledge/serialize.js +67 -0
  89. package/dist/src/knowledge/serialize.js.map +1 -0
  90. package/dist/src/onboarding/agents-generate.d.ts +19 -0
  91. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  92. package/dist/src/onboarding/agents-generate.js +156 -66
  93. package/dist/src/onboarding/agents-generate.js.map +1 -1
  94. package/dist/src/onboarding/create.d.ts.map +1 -1
  95. package/dist/src/onboarding/create.js +66 -10
  96. package/dist/src/onboarding/create.js.map +1 -1
  97. package/dist/src/onboarding/doctor.d.ts +13 -1
  98. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  99. package/dist/src/onboarding/doctor.js +613 -7
  100. package/dist/src/onboarding/doctor.js.map +1 -1
  101. package/dist/src/onboarding/init.d.ts +8 -1
  102. package/dist/src/onboarding/init.d.ts.map +1 -1
  103. package/dist/src/onboarding/init.js +33 -2
  104. package/dist/src/onboarding/init.js.map +1 -1
  105. package/dist/src/onboarding/memory-scaffold.d.ts +18 -0
  106. package/dist/src/onboarding/memory-scaffold.d.ts.map +1 -0
  107. package/dist/src/onboarding/memory-scaffold.js +151 -0
  108. package/dist/src/onboarding/memory-scaffold.js.map +1 -0
  109. package/dist/src/onboarding/nested-agents-generate.d.ts +16 -0
  110. package/dist/src/onboarding/nested-agents-generate.d.ts.map +1 -0
  111. package/dist/src/onboarding/nested-agents-generate.js +69 -0
  112. package/dist/src/onboarding/nested-agents-generate.js.map +1 -0
  113. package/dist/src/onboarding/nested-agents-templates.d.ts +26 -0
  114. package/dist/src/onboarding/nested-agents-templates.d.ts.map +1 -0
  115. package/dist/src/onboarding/nested-agents-templates.js +178 -0
  116. package/dist/src/onboarding/nested-agents-templates.js.map +1 -0
  117. package/dist/src/onboarding/profile-validate.d.ts +13 -0
  118. package/dist/src/onboarding/profile-validate.d.ts.map +1 -0
  119. package/dist/src/onboarding/profile-validate.js +109 -0
  120. package/dist/src/onboarding/profile-validate.js.map +1 -0
  121. package/dist/src/onboarding/upgrade.d.ts +6 -0
  122. package/dist/src/onboarding/upgrade.d.ts.map +1 -1
  123. package/dist/src/onboarding/upgrade.js +45 -1
  124. package/dist/src/onboarding/upgrade.js.map +1 -1
  125. package/dist/src/onboarding/workspace-discovery.d.ts +11 -0
  126. package/dist/src/onboarding/workspace-discovery.d.ts.map +1 -0
  127. package/dist/src/onboarding/workspace-discovery.js +138 -0
  128. package/dist/src/onboarding/workspace-discovery.js.map +1 -0
  129. package/dist/src/profiles/profile-schema.d.ts +116 -0
  130. package/dist/src/profiles/profile-schema.d.ts.map +1 -0
  131. package/dist/src/profiles/profile-schema.js +94 -0
  132. package/dist/src/profiles/profile-schema.js.map +1 -0
  133. package/dist/src/profiles/stack-profile.d.ts +41 -1
  134. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  135. package/dist/src/profiles/stack-profile.js +9 -0
  136. package/dist/src/profiles/stack-profile.js.map +1 -1
  137. package/dist/src/profiles/terminology-utils.d.ts +8 -0
  138. package/dist/src/profiles/terminology-utils.d.ts.map +1 -0
  139. package/dist/src/profiles/terminology-utils.js +31 -0
  140. package/dist/src/profiles/terminology-utils.js.map +1 -0
  141. package/dist/src/types.d.ts +0 -1
  142. package/dist/src/types.d.ts.map +1 -1
  143. package/dist/src/utils/fs-idempotent.d.ts +1 -1
  144. package/dist/src/utils/fs-idempotent.d.ts.map +1 -1
  145. package/dist/src/utils/fs-idempotent.js +12 -3
  146. package/dist/src/utils/fs-idempotent.js.map +1 -1
  147. package/dist/src/validators/skill-validate.d.ts +15 -0
  148. package/dist/src/validators/skill-validate.d.ts.map +1 -1
  149. package/dist/src/validators/skill-validate.js +204 -2
  150. package/dist/src/validators/skill-validate.js.map +1 -1
  151. package/package.json +2 -2
  152. package/profiles/astro-typescript-turborepo.yaml +87 -0
  153. package/profiles/editframe-html-templates/composition-agents.md +34 -0
  154. package/profiles/editframe-html-templates/composition.html +32 -0
  155. package/profiles/editframe-html.yaml +283 -0
  156. package/profiles/forge-shell.yaml +77 -0
  157. package/profiles/phaser-turborepo.yaml +87 -0
  158. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  159. package/skills/fo/fo-fix/SKILL.md +11 -0
  160. package/skills/fo/fo-handoff/SKILL.md +2 -1
  161. package/skills/fo/fo-idea-implement/SKILL.md +44 -2
  162. package/skills/fo/fo-idea-plan/SKILL.md +1 -0
  163. package/skills/fo/fo-knowledge-distill/SKILL.md +170 -0
  164. package/skills/fo/fo-memory-sync/SKILL.md +6 -3
  165. package/skills/fo/fo-memory-sync/fix-patterns.md +38 -4
  166. package/skills/fo/fo-review/SKILL.md +2 -2
  167. package/skills/fo/fo-session-retro/SKILL.md +21 -11
  168. package/skills/fo/fo-session-save/SKILL.md +3 -4
  169. package/skills/fo/fo-session-save/fix-patterns.md +53 -29
  170. package/skills/fo/fo-session-save/learned-principles.md +59 -8
  171. package/skills/fo/fo-step-commit/SKILL.md +55 -0
  172. package/skills/meta/forge-bootstrap/SKILL.md +28 -4
  173. package/skills/meta/forge-bootstrap/operator-profile-template.md +1 -0
  174. package/skills/meta/skill-create/SKILL.md +1 -1
  175. package/skills/shared/grilling/SKILL.md +1 -1
  176. package/skills/shared/grilling/learned-principles.md +14 -0
  177. package/skills/shared/grilling/qa-log.md +105 -6
  178. package/skills/shared/knowledge/learned-principles.md +5 -0
  179. package/skills/shared/windows-ai-tooling/SKILL.md +154 -1
  180. package/skills/shared/writing-great-skills/SKILL.md +74 -1
@@ -14,12 +14,29 @@ Checks for forge.yaml, AGENTS.md, PREFERENCES.md, .agents/skills/, docs/rfcs/,
14
14
  <item>RFC-0524: added stale knowledge file detection — compares source and .agents/ copies.</item>
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
+ <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>
17
23
  </CHANGE_SUMMARY>
18
24
  */
19
25
  import { readFile, readdir, stat } from "node:fs/promises";
26
+ import { readFileSync } from "node:fs";
20
27
  import { join, relative, dirname } from "node:path";
21
- 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";
22
30
  import { FORGE_SKILLS, discoverPackSkills } from "../registry.js";
31
+ import { discoverWorkspaces } from "./workspace-discovery.js";
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";
23
40
  async function pathExists(target) {
24
41
  try {
25
42
  await stat(target);
@@ -72,6 +89,7 @@ const BINDING_COMMAND_KEYS = [
72
89
  "commands.test",
73
90
  "commands.scopedBuild",
74
91
  "commands.specValidate",
92
+ "commands.sessionSave",
75
93
  ];
76
94
  async function validateBindings(workspaceRoot) {
77
95
  const result = { resolved: [], absent: [], invalid: [], notices: [] };
@@ -201,6 +219,294 @@ async function checkStaleKnowledgeFiles(workspaceRoot, forgeRoot) {
201
219
  };
202
220
  }
203
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
+ // ---------------------------------------------------------------------------
204
510
  // Pack skill diagnostics (RFC-0539)
205
511
  // ---------------------------------------------------------------------------
206
512
  async function checkPackSkills(workspaceRoot) {
@@ -262,8 +568,234 @@ async function checkPackSkills(workspaceRoot) {
262
568
  : `${issues.length} issue(s): ${issues.join(", ")} — run 'forge create' to sync`,
263
569
  };
264
570
  }
265
- export async function runDoctor(_input, context) {
571
+ // ---------------------------------------------------------------------------
572
+ // Nested AGENTS.md diagnostics (RFC-0611)
573
+ // ---------------------------------------------------------------------------
574
+ async function checkNestedAgentsMd(workspaceRoot, workspaceTypes) {
575
+ let config;
576
+ try {
577
+ config = loadForgeConfig(workspaceRoot);
578
+ }
579
+ catch {
580
+ return {
581
+ name: "nested-AGENTS.md",
582
+ status: "pass",
583
+ message: "forge.yaml not loadable — nested AGENTS.md check skipped",
584
+ };
585
+ }
586
+ const workspaces = discoverWorkspaces(workspaceRoot, workspaceTypes);
587
+ if (workspaces.length === 0) {
588
+ return {
589
+ name: "nested-AGENTS.md",
590
+ status: "pass",
591
+ message: "no workspace directories found",
592
+ };
593
+ }
594
+ const issues = [];
595
+ let missing = 0;
596
+ let stale = 0;
597
+ let handwritten = 0;
598
+ for (const ws of workspaces) {
599
+ if (!ws.hasAgentsMd) {
600
+ missing++;
601
+ issues.push(`${ws.path}/AGENTS.md missing`);
602
+ continue;
603
+ }
604
+ if (!ws.isGenerated) {
605
+ handwritten++;
606
+ const suggestions = [];
607
+ try {
608
+ const content = await readFile(join(workspaceRoot, ws.path, "AGENTS.md"), "utf8");
609
+ if (!content.includes("AGENTS.md")) {
610
+ suggestions.push("reference root AGENTS.md");
611
+ }
612
+ }
613
+ catch {
614
+ // unreadable — skip
615
+ }
616
+ if (suggestions.length > 0) {
617
+ issues.push(`${ws.path}/AGENTS.md hand-written — consider: ${suggestions.join(", ")}`);
618
+ }
619
+ continue;
620
+ }
621
+ // Stale check: compare in-memory render to committed file
622
+ try {
623
+ const expected = buildNestedAgentsMd(ws, config);
624
+ const actual = await readFile(join(workspaceRoot, ws.path, "AGENTS.md"), "utf8");
625
+ if (expected !== actual) {
626
+ stale++;
627
+ issues.push(`${ws.path}/AGENTS.md stale — run 'forge agents generate'`);
628
+ }
629
+ }
630
+ catch {
631
+ // unreadable — skip
632
+ }
633
+ }
634
+ if (issues.length === 0) {
635
+ return {
636
+ name: "nested-AGENTS.md",
637
+ status: "pass",
638
+ message: `${workspaces.length} workspace(s) — all AGENTS.md in sync`,
639
+ };
640
+ }
641
+ const parts = [];
642
+ if (missing > 0)
643
+ parts.push(`${missing} missing`);
644
+ if (stale > 0)
645
+ parts.push(`${stale} stale`);
646
+ if (handwritten > 0)
647
+ parts.push(`${handwritten} hand-written`);
648
+ return {
649
+ name: "nested-AGENTS.md",
650
+ status: stale > 0 ? "warn" : "pass",
651
+ message: `${parts.join(", ")} — ${issues.slice(0, 3).join("; ")}${issues.length > 3 ? ` (+${issues.length - 3} more)` : ""}`,
652
+ };
653
+ }
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) {
266
797
  const { workspaceRoot, logger, outputFormat } = context;
798
+ const strict = input.flags["strict"] === true;
267
799
  const checks = [];
268
800
  // Check forge.yaml
269
801
  const forgeYamlExists = await pathExists(join(workspaceRoot, "forge.yaml"));
@@ -327,6 +859,29 @@ export async function runDoctor(_input, context) {
327
859
  catch {
328
860
  forgeRoot = join(workspaceRoot, "packages", "forge");
329
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
+ });
330
885
  const forbiddenImports = await scanForForbiddenImports(forgeRoot, workspaceRoot);
331
886
  checks.push({
332
887
  name: "autonomy-guard",
@@ -347,14 +902,65 @@ export async function runDoctor(_input, context) {
347
902
  // Check stale knowledge files (RFC-0524, RFC-0539)
348
903
  const staleCheck = await checkStaleKnowledgeFiles(workspaceRoot, forgeRoot);
349
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));
350
919
  // RFC-0539: Check pack skills — stale/missing copies and config validation
351
920
  const packCheck = await checkPackSkills(workspaceRoot);
352
921
  checks.push(packCheck);
353
- const allPass = checks.every((c) => c.status === "pass");
354
- const hasFails = checks.some((c) => c.status === "fail");
922
+ // RFC-0611: Check nested AGENTS.md missing, stale, hand-written
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");
355
961
  if (outputFormat === "pretty") {
356
962
  logger.section(`Forge Doctor — ${workspaceRoot}`);
357
- for (const check of checks) {
963
+ for (const check of finalChecks) {
358
964
  const icon = check.status === "pass" ? "✓" : check.status === "warn" ? "⚠" : "✖";
359
965
  const fn = check.status === "pass" ? logger.success : check.status === "warn" ? logger.warn : logger.error;
360
966
  fn(`${icon} ${check.name}: ${check.message}`);
@@ -370,11 +976,11 @@ export async function runDoctor(_input, context) {
370
976
  }
371
977
  }
372
978
  return {
373
- data: { command: "forge.doctor", checks, allPass, forbiddenImports, bindings: bindingsResult },
979
+ data: { command: "forge.doctor", checks: finalChecks, allPass, forbiddenImports, bindings: bindingsResult, domain: domainReport },
374
980
  exitCode: hasFails ? 1 : 0,
375
981
  summary: allPass
376
982
  ? "forge.doctor: all checks passed"
377
- : `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)`,
378
984
  };
379
985
  }
380
986
  //# sourceMappingURL=doctor.js.map