@warpgogol/forge 0.4.0 → 0.6.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.
- package/AGENTS.md +123 -5
- package/dist/bin/cli.js +0 -0
- package/dist/os/adr/handlers/validate.d.ts.map +1 -1
- package/dist/os/adr/handlers/validate.js +31 -3
- package/dist/os/adr/handlers/validate.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +76 -2
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/core/handlers/knowledge-compact.d.ts +4 -0
- package/dist/os/core/handlers/knowledge-compact.d.ts.map +1 -0
- package/dist/os/core/handlers/knowledge-compact.js +198 -0
- package/dist/os/core/handlers/knowledge-compact.js.map +1 -0
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +6 -0
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/rfc/handlers/lifecycle.d.ts.map +1 -1
- package/dist/os/rfc/handlers/lifecycle.js +3 -4
- package/dist/os/rfc/handlers/lifecycle.js.map +1 -1
- package/dist/os/rfc/handlers/validate-rules.d.ts.map +1 -1
- package/dist/os/rfc/handlers/validate-rules.js +41 -3
- package/dist/os/rfc/handlers/validate-rules.js.map +1 -1
- package/dist/os/spec/spec-schema.d.ts +1 -1
- package/dist/src/config/forge-config.d.ts +67 -3
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +86 -2
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/knowledge/budgets.d.ts +38 -0
- package/dist/src/knowledge/budgets.d.ts.map +1 -0
- package/dist/src/knowledge/budgets.js +157 -0
- package/dist/src/knowledge/budgets.js.map +1 -0
- package/dist/src/knowledge/compact.d.ts +47 -0
- package/dist/src/knowledge/compact.d.ts.map +1 -0
- package/dist/src/knowledge/compact.js +322 -0
- package/dist/src/knowledge/compact.js.map +1 -0
- package/dist/src/knowledge/index.d.ts +7 -0
- package/dist/src/knowledge/index.d.ts.map +1 -0
- package/dist/src/knowledge/index.js +19 -0
- package/dist/src/knowledge/index.js.map +1 -0
- package/dist/src/knowledge/parse.d.ts +3 -0
- package/dist/src/knowledge/parse.d.ts.map +1 -0
- package/dist/src/knowledge/parse.js +205 -0
- package/dist/src/knowledge/parse.js.map +1 -0
- package/dist/src/knowledge/promote.d.ts +37 -0
- package/dist/src/knowledge/promote.d.ts.map +1 -0
- package/dist/src/knowledge/promote.js +120 -0
- package/dist/src/knowledge/promote.js.map +1 -0
- package/dist/src/knowledge/schema.d.ts +61 -0
- package/dist/src/knowledge/schema.d.ts.map +1 -0
- package/dist/src/knowledge/schema.js +64 -0
- package/dist/src/knowledge/schema.js.map +1 -0
- package/dist/src/knowledge/serialize.d.ts +3 -0
- package/dist/src/knowledge/serialize.d.ts.map +1 -0
- package/dist/src/knowledge/serialize.js +67 -0
- package/dist/src/knowledge/serialize.js.map +1 -0
- package/dist/src/onboarding/agents-generate.d.ts +15 -0
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +148 -241
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/onboarding/create.d.ts.map +1 -1
- package/dist/src/onboarding/create.js +62 -3
- package/dist/src/onboarding/create.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts +13 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +527 -11
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts +8 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +33 -2
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/memory-scaffold.d.ts +18 -0
- package/dist/src/onboarding/memory-scaffold.d.ts.map +1 -0
- package/dist/src/onboarding/memory-scaffold.js +151 -0
- package/dist/src/onboarding/memory-scaffold.js.map +1 -0
- package/dist/src/onboarding/nested-agents-generate.d.ts +6 -2
- package/dist/src/onboarding/nested-agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.js +34 -9
- package/dist/src/onboarding/nested-agents-generate.js.map +1 -1
- package/dist/src/onboarding/nested-agents-templates.d.ts +23 -1
- package/dist/src/onboarding/nested-agents-templates.d.ts.map +1 -1
- package/dist/src/onboarding/nested-agents-templates.js +138 -8
- package/dist/src/onboarding/nested-agents-templates.js.map +1 -1
- package/dist/src/onboarding/profile-validate.d.ts +13 -0
- package/dist/src/onboarding/profile-validate.d.ts.map +1 -0
- package/dist/src/onboarding/profile-validate.js +109 -0
- package/dist/src/onboarding/profile-validate.js.map +1 -0
- package/dist/src/onboarding/upgrade.d.ts +5 -0
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +27 -1
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts +4 -3
- package/dist/src/onboarding/workspace-discovery.d.ts.map +1 -1
- package/dist/src/onboarding/workspace-discovery.js +51 -3
- package/dist/src/onboarding/workspace-discovery.js.map +1 -1
- package/dist/src/profiles/profile-schema.d.ts +116 -0
- package/dist/src/profiles/profile-schema.d.ts.map +1 -0
- package/dist/src/profiles/profile-schema.js +94 -0
- package/dist/src/profiles/profile-schema.js.map +1 -0
- package/dist/src/profiles/stack-profile.d.ts +41 -1
- package/dist/src/profiles/stack-profile.d.ts.map +1 -1
- package/dist/src/profiles/stack-profile.js +9 -0
- package/dist/src/profiles/stack-profile.js.map +1 -1
- package/dist/src/profiles/terminology-utils.d.ts +8 -0
- package/dist/src/profiles/terminology-utils.d.ts.map +1 -0
- package/dist/src/profiles/terminology-utils.js +31 -0
- package/dist/src/profiles/terminology-utils.js.map +1 -0
- package/dist/src/validators/skill-validate.d.ts +15 -0
- package/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +204 -2
- package/dist/src/validators/skill-validate.js.map +1 -1
- package/package.json +71 -163
- package/profiles/astro-typescript-turborepo.yaml +87 -0
- package/profiles/editframe-html-templates/composition-agents.md +34 -0
- package/profiles/editframe-html-templates/composition.html +32 -0
- package/profiles/editframe-html.yaml +283 -0
- package/profiles/forge-shell.yaml +77 -0
- package/profiles/phaser-turborepo.yaml +87 -0
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +2 -1
- package/skills/fo/fo-idea-implement/SKILL.md +44 -2
- package/skills/fo/fo-idea-plan/SKILL.md +1 -0
- package/skills/fo/fo-knowledge-distill/SKILL.md +170 -0
- package/skills/fo/fo-memory-sync/SKILL.md +6 -3
- package/skills/fo/fo-memory-sync/fix-patterns.md +38 -4
- package/skills/fo/fo-review/SKILL.md +2 -2
- package/skills/fo/fo-session-retro/SKILL.md +21 -11
- package/skills/fo/fo-session-save/SKILL.md +1 -1
- package/skills/fo/fo-session-save/fix-patterns.md +53 -29
- package/skills/fo/fo-session-save/learned-principles.md +59 -8
- package/skills/meta/forge-bootstrap/SKILL.md +16 -4
- package/skills/meta/forge-bootstrap/operator-profile-template.md +1 -0
- package/skills/meta/skill-create/SKILL.md +1 -1
- package/skills/shared/grilling/SKILL.md +1 -1
- package/skills/shared/grilling/learned-principles.md +14 -0
- package/skills/shared/grilling/qa-log.md +105 -6
- package/skills/shared/knowledge/learned-principles.md +5 -0
- package/skills/shared/windows-ai-tooling/SKILL.md +154 -1
- package/skills/shared/writing-great-skills/SKILL.md +74 -1
- package/src/onboarding/templates/behavioral-layer-core.md +159 -0
- package/src/onboarding/templates/behavioral-layer-extended.md +45 -0
- package/src/onboarding/templates/root-agents-business.md +32 -0
- package/src/onboarding/templates/root-agents-creative.md +32 -0
- package/dist/src/onboarding/extended-behavioral-layer.d.ts +0 -10
- package/dist/src/onboarding/extended-behavioral-layer.d.ts.map +0 -1
- package/dist/src/onboarding/extended-behavioral-layer.js +0 -82
- package/dist/src/onboarding/extended-behavioral-layer.js.map +0 -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 {
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
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: ${
|
|
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
|