@warpgogol/forge 5.2.0 → 5.2.4
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 +4 -1
- package/dist/os/adr/adr.module.js +1 -1
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/compass/compass.module.js +2 -2
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +3 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +87 -8
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +15 -1
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +22 -6
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/program/lease.js +1 -1
- package/dist/os/program/lease.js.map +1 -1
- package/dist/os/queue/queue.module.js +1 -1
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/rfc/handlers/list-create.d.ts.map +1 -1
- package/dist/os/rfc/handlers/list-create.js +79 -32
- package/dist/os/rfc/handlers/list-create.js.map +1 -1
- package/dist/os/rfc/rfc.module.js +1 -1
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +2 -0
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +9 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +7 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/knowledge/index.d.ts +1 -0
- package/dist/src/knowledge/index.d.ts.map +1 -1
- package/dist/src/knowledge/index.js +1 -0
- package/dist/src/knowledge/index.js.map +1 -1
- package/dist/src/knowledge/sync.d.ts +24 -0
- package/dist/src/knowledge/sync.d.ts.map +1 -0
- package/dist/src/knowledge/sync.js +127 -0
- package/dist/src/knowledge/sync.js.map +1 -0
- package/dist/src/onboarding/agents-generate.d.ts +2 -0
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +14 -18
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts +6 -0
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +124 -14
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +15 -8
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/memory-compact.d.ts +22 -0
- package/dist/src/onboarding/memory-compact.d.ts.map +1 -0
- package/dist/src/onboarding/memory-compact.js +147 -0
- package/dist/src/onboarding/memory-compact.js.map +1 -0
- package/dist/src/onboarding/memory-scaffold.d.ts +1 -0
- package/dist/src/onboarding/memory-scaffold.d.ts.map +1 -1
- package/dist/src/onboarding/memory-scaffold.js +1 -1
- package/dist/src/onboarding/memory-scaffold.js.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.js +1 -1
- package/dist/src/onboarding/nested-agents-generate.js.map +1 -1
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +42 -13
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts.map +1 -1
- package/dist/src/onboarding/workspace-discovery.js +14 -2
- package/dist/src/onboarding/workspace-discovery.js.map +1 -1
- package/dist/src/types.d.ts +1 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/fs-atomic.js +1 -1
- package/dist/src/utils/fs-atomic.js.map +1 -1
- package/docs/reference/forge-yaml.md +12 -0
- package/os/adr/adr.module.ts +1 -1
- package/os/compass/compass.module.ts +2 -2
- package/os/compass/handlers/compass-audit-handler.ts +116 -8
- package/os/compass/handlers/tests/compass-audit-plan.test.ts +98 -0
- package/os/compass/handlers/tests/compass-audit-record.test.ts +105 -0
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +133 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +124 -0
- package/os/core/core.module.ts +16 -1
- package/os/mission/handlers/archive.test.ts +49 -0
- package/os/mission/handlers/archive.ts +21 -6
- package/os/program/lease.ts +1 -1
- package/os/queue/queue.module.ts +1 -1
- package/os/rfc/handlers/list-create.ts +83 -35
- package/os/rfc/handlers/validate-rules.test.ts +50 -0
- package/os/rfc/rfc-create-concurrent.test.ts +118 -0
- package/os/rfc/rfc-create-hint.test.ts +90 -0
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +83 -0
- package/os/rfc/rfc.module.ts +1 -1
- package/os/spec/spec.module.ts +2 -0
- package/package.json +1 -1
- package/profiles/forge-shell.yaml +3 -0
- package/profiles/godot-game.yaml +3 -0
- package/profiles/phaser-game.yaml +3 -0
- package/profiles/site-workshop.yaml +567 -564
- package/src/config/forge-config.ts +11 -0
- package/src/knowledge/__tests__/sync.test.ts +227 -0
- package/src/knowledge/index.ts +7 -0
- package/src/knowledge/sync.ts +161 -0
- package/src/onboarding/agents-generate.ts +18 -18
- package/src/onboarding/doctor.ts +141 -15
- package/src/onboarding/init.ts +15 -8
- package/src/onboarding/memory-compact.ts +188 -0
- package/src/onboarding/memory-scaffold.ts +1 -1
- package/src/onboarding/nested-agents-generate.ts +5 -1
- package/src/onboarding/upgrade.ts +47 -16
- package/src/onboarding/workspace-discovery.ts +15 -1
- package/src/tests/agents-generate.test.ts +26 -3
- package/src/tests/doctor-dist-freshness.test.ts +110 -0
- package/src/tests/doctor-fix.test.ts +145 -0
- package/src/tests/doctor-nested-agents.test.ts +108 -0
- package/src/tests/memory-compact.test.ts +143 -0
- package/src/tests/upgrade.test.ts +53 -0
- package/src/tests/workspace-discovery.test.ts +28 -0
- package/src/types.ts +2 -2
- package/src/utils/fs-atomic.ts +1 -1
package/src/onboarding/doctor.ts
CHANGED
|
@@ -35,18 +35,20 @@ import { resolveForgeRoot, loadForgeConfig, resolveBinding, resolveTerminology,
|
|
|
35
35
|
import { FORGE_SKILLS, discoverPackSkills } from "../registry.ts";
|
|
36
36
|
import { discoverWorkspaces } from "./workspace-discovery.ts";
|
|
37
37
|
import { buildNestedAgentsMd, selectNestedTemplate } from "./nested-agents-templates.ts";
|
|
38
|
-
import { readPackageInfo } from "./nested-agents-generate.ts";
|
|
38
|
+
import { readPackageInfo, generateNestedAgentsMd } from "./nested-agents-generate.ts";
|
|
39
39
|
import { listStackProfiles, type StackProfile } from "../profiles/stack-profile.ts";
|
|
40
40
|
import { resolveAllTerminology } from "../profiles/terminology-utils.ts";
|
|
41
41
|
import { TERMINOLOGY_DEFAULTS } from "../profiles/profile-schema.ts";
|
|
42
42
|
import type { ProfileWorkspaceType } from "../profiles/profile-schema.ts";
|
|
43
43
|
import { runProfileValidate } from "./profile-validate.ts";
|
|
44
44
|
import { parseKnowledgeFile } from "../knowledge/index.ts";
|
|
45
|
+
import { planKnowledgeSync } from "../knowledge/index.ts";
|
|
45
46
|
import type { ParsedKnowledgeFile } from "../knowledge/index.ts";
|
|
46
47
|
import { computeLayerBudgets, resolveKnowledgeBudgets, DEFAULT_KNOWLEDGE_BUDGETS } from "../knowledge/budgets.ts";
|
|
47
48
|
import { detectDuplicatePrinciples } from "../knowledge/index.ts";
|
|
48
49
|
import type { DuplicatePair } from "../knowledge/index.ts";
|
|
49
|
-
import { checkMemoryLayerHealth } from "./memory-scaffold.ts";
|
|
50
|
+
import { checkMemoryLayerHealth, resolveMemoryBudget } from "./memory-scaffold.ts";
|
|
51
|
+
import { compactMemoryMd } from "./memory-compact.ts";
|
|
50
52
|
import { checkInvariants } from "./invariant-engine.ts";
|
|
51
53
|
import type { InvariantViolation } from "./invariant-engine.ts";
|
|
52
54
|
import { checkNpmToken, workshopNeedsWarpgogolToken } from "./npm-token-check.ts";
|
|
@@ -54,6 +56,12 @@ import { execSync } from "node:child_process";
|
|
|
54
56
|
import fs from "node:fs";
|
|
55
57
|
import type { ProfilePrerequisite } from "../profiles/profile-schema.ts";
|
|
56
58
|
|
|
59
|
+
interface DoctorFixResult {
|
|
60
|
+
check: string;
|
|
61
|
+
action: "fixed" | "skipped" | "failed";
|
|
62
|
+
detail: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
interface DoctorCheck {
|
|
58
66
|
name: string;
|
|
59
67
|
status: "pass" | "fail" | "warn";
|
|
@@ -243,9 +251,10 @@ async function checkStaleKnowledgeFiles(
|
|
|
243
251
|
const destExists = await pathExists(destPath);
|
|
244
252
|
|
|
245
253
|
if (srcExists && destExists) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
// Append-only contract: local copies legitimately accumulate entries.
|
|
255
|
+
// Stale = source has entries the local copy lacks (sync would merge).
|
|
256
|
+
const plan = planKnowledgeSync(srcPath, destPath);
|
|
257
|
+
if (plan.action === "merged") {
|
|
249
258
|
stale.push(`${skill.name}/${kf}`);
|
|
250
259
|
}
|
|
251
260
|
}
|
|
@@ -274,9 +283,8 @@ async function checkStaleKnowledgeFiles(
|
|
|
274
283
|
const destExists = await pathExists(destPath);
|
|
275
284
|
|
|
276
285
|
if (srcExists && destExists) {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
if (srcContent !== destContent) {
|
|
286
|
+
const plan = planKnowledgeSync(srcPath, destPath);
|
|
287
|
+
if (plan.action === "merged") {
|
|
280
288
|
stale.push(`${skill.name}/${kf}`);
|
|
281
289
|
}
|
|
282
290
|
}
|
|
@@ -825,7 +833,11 @@ async function checkNestedAgentsMd(
|
|
|
825
833
|
};
|
|
826
834
|
}
|
|
827
835
|
|
|
828
|
-
const workspaces = discoverWorkspaces(
|
|
836
|
+
const workspaces = discoverWorkspaces(
|
|
837
|
+
workspaceRoot,
|
|
838
|
+
workspaceTypes,
|
|
839
|
+
config.bindings?.workspaces?.skipDirs,
|
|
840
|
+
);
|
|
829
841
|
|
|
830
842
|
if (workspaces.length === 0) {
|
|
831
843
|
return {
|
|
@@ -1208,9 +1220,10 @@ function checkPrerequisites(prerequisites: ProfilePrerequisite[]): DoctorCheck {
|
|
|
1208
1220
|
export async function runDoctor(
|
|
1209
1221
|
input: ForgeCommandInput,
|
|
1210
1222
|
context: ForgeRuntimeContext,
|
|
1211
|
-
): Promise<ForgeCommandResult<{ command: string; checks: DoctorCheck[]; allPass: boolean; forbiddenImports: ForbiddenImport[]; bindings: BindingValidation; domain?: DomainReport }>> {
|
|
1212
|
-
const { workspaceRoot, logger, outputFormat } = context;
|
|
1223
|
+
): Promise<ForgeCommandResult<{ command: string; checks: DoctorCheck[]; allPass: boolean; forbiddenImports: ForbiddenImport[]; bindings: BindingValidation; domain?: DomainReport; fixes?: DoctorFixResult[] }>> {
|
|
1224
|
+
const { workspaceRoot, logger, outputFormat, dryRun } = context;
|
|
1213
1225
|
const strict = input.flags["strict"] === true;
|
|
1226
|
+
const fix = input.flags["fix"] === true;
|
|
1214
1227
|
const checks: DoctorCheck[] = [];
|
|
1215
1228
|
|
|
1216
1229
|
// Check forge.yaml
|
|
@@ -1417,6 +1430,9 @@ export async function runDoctor(
|
|
|
1417
1430
|
// RFC-0664: Check memory layer health (budget, gitignore coverage, leak risk)
|
|
1418
1431
|
checks.push(checkMemoryLayer(workspaceRoot));
|
|
1419
1432
|
|
|
1433
|
+
// RFC-1151: Check dist freshness — stale dist/ silently shadows src/ in source checkouts
|
|
1434
|
+
checks.push(checkDistFreshness(forgeRoot));
|
|
1435
|
+
|
|
1420
1436
|
// RFC-0539: Check pack skills — stale/missing copies and config validation
|
|
1421
1437
|
const packCheck = await checkPackSkills(workspaceRoot);
|
|
1422
1438
|
checks.push(packCheck);
|
|
@@ -1434,8 +1450,8 @@ export async function runDoctor(
|
|
|
1434
1450
|
// RFC-0640 fix: previously gated by isSoftwareDomain, which skipped the check
|
|
1435
1451
|
// entirely for non-software domains. Now runs for all domains — profile-driven
|
|
1436
1452
|
// wsTypes replace hardcoded detection when present.
|
|
1453
|
+
let wsTypes: ProfileWorkspaceType[] | undefined;
|
|
1437
1454
|
{
|
|
1438
|
-
let wsTypes: ProfileWorkspaceType[] | undefined;
|
|
1439
1455
|
try {
|
|
1440
1456
|
const config = loadForgeConfig(workspaceRoot);
|
|
1441
1457
|
// RFC-0643: prefer config.profile (loaded by loadForgeConfig)
|
|
@@ -1471,6 +1487,15 @@ export async function runDoctor(
|
|
|
1471
1487
|
const allPass = finalChecks.every((c) => c.status === "pass");
|
|
1472
1488
|
const hasFails = finalChecks.some((c) => c.status === "fail");
|
|
1473
1489
|
|
|
1490
|
+
// RFC-1151: --fix applies registered safe remediations for warn/fail checks
|
|
1491
|
+
const fixes: DoctorFixResult[] = [];
|
|
1492
|
+
if (fix) {
|
|
1493
|
+
for (const check of finalChecks) {
|
|
1494
|
+
if (check.status === "pass") continue;
|
|
1495
|
+
fixes.push(await applyDoctorRemediation(check, workspaceRoot, wsTypes, dryRun === true));
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1474
1499
|
if (outputFormat === "pretty") {
|
|
1475
1500
|
logger.section(`Forge Doctor — ${workspaceRoot}`);
|
|
1476
1501
|
for (const check of finalChecks) {
|
|
@@ -1478,6 +1503,10 @@ export async function runDoctor(
|
|
|
1478
1503
|
const fn = check.status === "pass" ? logger.success : check.status === "warn" ? logger.warn : logger.error;
|
|
1479
1504
|
fn(`${icon} ${check.name}: ${check.message}`);
|
|
1480
1505
|
}
|
|
1506
|
+
for (const f of fixes) {
|
|
1507
|
+
const fn = f.action === "fixed" ? logger.success : f.action === "failed" ? logger.error : logger.info;
|
|
1508
|
+
fn(` fix ${f.check}: ${f.action} — ${f.detail}`);
|
|
1509
|
+
}
|
|
1481
1510
|
if (allPass) {
|
|
1482
1511
|
logger.success("All checks passed — forge is properly configured.");
|
|
1483
1512
|
} else if (hasFails) {
|
|
@@ -1487,11 +1516,108 @@ export async function runDoctor(
|
|
|
1487
1516
|
}
|
|
1488
1517
|
}
|
|
1489
1518
|
|
|
1519
|
+
const fixFailed = fixes.some((f) => f.action === "failed");
|
|
1520
|
+
|
|
1490
1521
|
return {
|
|
1491
|
-
data: { command: "forge.doctor", checks: finalChecks, allPass, forbiddenImports, bindings: bindingsResult, domain: domainReport },
|
|
1492
|
-
exitCode: hasFails ? 1 : 0,
|
|
1522
|
+
data: { command: "forge.doctor", checks: finalChecks, allPass, forbiddenImports, bindings: bindingsResult, domain: domainReport, ...(fix ? { fixes } : {}) },
|
|
1523
|
+
exitCode: hasFails || fixFailed ? 1 : 0,
|
|
1493
1524
|
summary: allPass
|
|
1494
1525
|
? "forge.doctor: all checks passed"
|
|
1495
|
-
: `forge.doctor: ${finalChecks.filter((c) => c.status === "fail").length} fail(s), ${finalChecks.filter((c) => c.status === "warn").length} warn(s)`,
|
|
1526
|
+
: `forge.doctor: ${finalChecks.filter((c) => c.status === "fail").length} fail(s), ${finalChecks.filter((c) => c.status === "warn").length} warn(s)${fix ? `, ${fixes.filter((f) => f.action === "fixed").length} fixed` : ""}`,
|
|
1496
1527
|
};
|
|
1497
1528
|
}
|
|
1529
|
+
|
|
1530
|
+
// RFC-1151: dist-freshness — a stale dist/ silently shadows src/ because
|
|
1531
|
+
// bin/cli.js prefers dist when present. Warn-only; rebuild is a pnpm script,
|
|
1532
|
+
// not forge's call. Pass (skip) when src/ or dist/ is absent — npm consumers
|
|
1533
|
+
// ship dist-only, so this activates only in source checkouts.
|
|
1534
|
+
function checkDistFreshness(forgeRoot: string): DoctorCheck {
|
|
1535
|
+
const srcDir = join(forgeRoot, "src");
|
|
1536
|
+
const distDir = join(forgeRoot, "dist");
|
|
1537
|
+
if (!fs.existsSync(srcDir) || !fs.existsSync(distDir)) {
|
|
1538
|
+
return {
|
|
1539
|
+
name: "dist-freshness",
|
|
1540
|
+
status: "pass",
|
|
1541
|
+
message: "no src+dist pair — not a source checkout",
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
const newestSrc = newestMtime(srcDir);
|
|
1545
|
+
const distMtime = fs.statSync(distDir).mtimeMs;
|
|
1546
|
+
if (newestSrc > distMtime) {
|
|
1547
|
+
return {
|
|
1548
|
+
name: "dist-freshness",
|
|
1549
|
+
status: "warn",
|
|
1550
|
+
message: `dist/ older than newest src/ file — run 'pnpm run build' in ${forgeRoot}`,
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
return { name: "dist-freshness", status: "pass", message: "dist/ is fresh" };
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function newestMtime(dir: string): number {
|
|
1557
|
+
let newest = 0;
|
|
1558
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true, recursive: true })) {
|
|
1559
|
+
if (!entry.isFile()) continue;
|
|
1560
|
+
const m = fs.statSync(join(entry.parentPath, entry.name)).mtimeMs;
|
|
1561
|
+
if (m > newest) newest = m;
|
|
1562
|
+
}
|
|
1563
|
+
return newest;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// RFC-1151: remediation dispatch — check name → safe fix action.
|
|
1567
|
+
// Remediations reuse existing generators; hand-written files are never touched
|
|
1568
|
+
// (generated-marker edit guard inside generateNestedAgentsMd).
|
|
1569
|
+
async function applyDoctorRemediation(
|
|
1570
|
+
check: DoctorCheck,
|
|
1571
|
+
workspaceRoot: string,
|
|
1572
|
+
workspaceTypes: ProfileWorkspaceType[] | undefined,
|
|
1573
|
+
dryRun: boolean,
|
|
1574
|
+
): Promise<DoctorFixResult> {
|
|
1575
|
+
switch (check.name) {
|
|
1576
|
+
case "nested-AGENTS.md": {
|
|
1577
|
+
try {
|
|
1578
|
+
const config = loadForgeConfig(workspaceRoot);
|
|
1579
|
+
const res = await generateNestedAgentsMd(workspaceRoot, config, dryRun, workspaceTypes);
|
|
1580
|
+
return {
|
|
1581
|
+
check: check.name,
|
|
1582
|
+
action: res.generated.length > 0 ? "fixed" : "skipped",
|
|
1583
|
+
detail: `${res.generated.length} file(s) ${dryRun ? "would be " : ""}regenerated, ${res.skipped.length} skipped`,
|
|
1584
|
+
};
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
return { check: check.name, action: "failed", detail: (err as Error).message };
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
case "memory-layer": {
|
|
1590
|
+
try {
|
|
1591
|
+
const memoryPath = join(workspaceRoot, ".agents", "memory", "MEMORY.md");
|
|
1592
|
+
if (!fs.existsSync(memoryPath)) {
|
|
1593
|
+
return { check: check.name, action: "skipped", detail: "MEMORY.md absent" };
|
|
1594
|
+
}
|
|
1595
|
+
const content = fs.readFileSync(memoryPath, "utf8");
|
|
1596
|
+
const budget = resolveMemoryBudget(workspaceRoot);
|
|
1597
|
+
const { lines, removedLines, fitsBudget } = compactMemoryMd(content, budget);
|
|
1598
|
+
if (!fitsBudget) {
|
|
1599
|
+
return {
|
|
1600
|
+
check: check.name,
|
|
1601
|
+
action: "failed",
|
|
1602
|
+
detail: "still over budget after Environment notes truncation — manual editorial compaction required",
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
if (removedLines.length === 0) {
|
|
1606
|
+
return { check: check.name, action: "skipped", detail: "within budget" };
|
|
1607
|
+
}
|
|
1608
|
+
if (!dryRun) {
|
|
1609
|
+
fs.writeFileSync(memoryPath, lines.join("\n"), "utf8");
|
|
1610
|
+
}
|
|
1611
|
+
return {
|
|
1612
|
+
check: check.name,
|
|
1613
|
+
action: "fixed",
|
|
1614
|
+
detail: `${dryRun ? "would remove" : "removed"} ${removedLines.length} bullet(s)`,
|
|
1615
|
+
};
|
|
1616
|
+
} catch (err) {
|
|
1617
|
+
return { check: check.name, action: "failed", detail: (err as Error).message };
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
default:
|
|
1621
|
+
return { check: check.name, action: "skipped", detail: "no-remediation" };
|
|
1622
|
+
}
|
|
1623
|
+
}
|
package/src/onboarding/init.ts
CHANGED
|
@@ -24,6 +24,7 @@ import fs from "node:fs";
|
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { stringify as stringifyYaml, parse as parseYaml } from "yaml";
|
|
26
26
|
import { FORGE_SKILLS, discoverPackSkills } from "../registry.ts";
|
|
27
|
+
import { syncKnowledgeFile } from "../knowledge/index.ts";
|
|
27
28
|
import { defaultForgeConfig, loadForgeConfig, resolveForgeRoot, type ForgeConfig } from "../config/forge-config.ts";
|
|
28
29
|
import { listStackProfiles, detectStack } from "../profiles/stack-profile.ts";
|
|
29
30
|
|
|
@@ -292,9 +293,11 @@ export function runInit(
|
|
|
292
293
|
const kfSrcPath = path.join(skillSrcDir, kf);
|
|
293
294
|
const kfDestPath = path.join(destDir, kf);
|
|
294
295
|
if (fs.existsSync(kfSrcPath)) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
// Append-only sync — never overwrite local knowledge entries
|
|
297
|
+
const syncResult = syncKnowledgeFile(kfSrcPath, kfDestPath);
|
|
298
|
+
if (syncResult.action === "copied" || syncResult.action === "merged") {
|
|
299
|
+
created.push(`${config.paths.skillsDir}/${skillName}/${kf}`);
|
|
300
|
+
}
|
|
298
301
|
} else {
|
|
299
302
|
errors.push(`Knowledge file '${kf}' declared in ${skillName} but not found in source`);
|
|
300
303
|
}
|
|
@@ -362,9 +365,11 @@ export function runInit(
|
|
|
362
365
|
const kfSrcPath = path.join(skillSrcDir, kf);
|
|
363
366
|
const kfDestPath = path.join(destDir, kf);
|
|
364
367
|
if (fs.existsSync(kfSrcPath)) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
// Append-only sync — never overwrite local knowledge entries
|
|
369
|
+
const syncResult = syncKnowledgeFile(kfSrcPath, kfDestPath);
|
|
370
|
+
if (syncResult.action === "copied" || syncResult.action === "merged") {
|
|
371
|
+
created.push(`${config.paths.skillsDir}/${skillName}/${kf}`);
|
|
372
|
+
}
|
|
368
373
|
} else {
|
|
369
374
|
errors.push(`Knowledge file '${kf}' declared in ${skillName} but not found in source`);
|
|
370
375
|
}
|
|
@@ -382,8 +387,10 @@ export function runInit(
|
|
|
382
387
|
const sharedKnowledgeDestDir = path.join(agentsSkillsDir, "shared-knowledge");
|
|
383
388
|
fs.mkdirSync(sharedKnowledgeDestDir, { recursive: true });
|
|
384
389
|
const sharedKnowledgeDest = path.join(sharedKnowledgeDestDir, "learned-principles.md");
|
|
385
|
-
|
|
386
|
-
|
|
390
|
+
const syncResult = syncKnowledgeFile(sharedKnowledgeSrc, sharedKnowledgeDest);
|
|
391
|
+
if (syncResult.action === "copied" || syncResult.action === "merged") {
|
|
392
|
+
created.push(`${config.paths.skillsDir}/shared-knowledge/learned-principles.md`);
|
|
393
|
+
}
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
// 4. Create docs directories from config paths
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>forge memory.compact — mechanically enforce the MEMORY.md character budget
|
|
4
|
+
(RFC-0664) by removing oldest Environment notes bullets until the file fits.
|
|
5
|
+
Invoked standalone and by forge doctor --fix for the memory-layer check (RFC-1151).</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Never touch "## Current focus" or "## Decisions in flight" — those need editorial judgment.</item>
|
|
8
|
+
<item>No semantic rewriting — bullets are removed whole, never edited or merged.</item>
|
|
9
|
+
<item>No archiving into daily/ logs — removed content is recoverable via git history.</item>
|
|
10
|
+
</non-goals>
|
|
11
|
+
</MODULE_CONTRACT>
|
|
12
|
+
<CHANGE_SUMMARY>
|
|
13
|
+
<item>RFC-1151: initial memory.compact — oldest-first Environment notes truncation.</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import type { ForgeCommandInput, ForgeCommandResult, ForgeRuntimeContext } from "../types.ts";
|
|
20
|
+
import { resolveMemoryBudget } from "./memory-scaffold.ts";
|
|
21
|
+
|
|
22
|
+
const MEMORY_MD_PATH = path.join(".agents", "memory", "MEMORY.md");
|
|
23
|
+
const COMPACT_SECTION = "## Environment notes";
|
|
24
|
+
const PROTECTED_SECTIONS = new Set(["## Current focus", "## Decisions in flight"]);
|
|
25
|
+
|
|
26
|
+
export interface MemoryCompactResult {
|
|
27
|
+
command: "forge.memory.compact";
|
|
28
|
+
status: "pass" | "fail";
|
|
29
|
+
removed: number;
|
|
30
|
+
remainingChars: number;
|
|
31
|
+
budget: number;
|
|
32
|
+
removedLines?: string[];
|
|
33
|
+
errors: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Section {
|
|
37
|
+
header: string;
|
|
38
|
+
start: number; // line index of the "## ..." header
|
|
39
|
+
end: number; // line index one past the section's last line
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseSections(lines: string[]): Section[] {
|
|
43
|
+
const sections: Section[] = [];
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
if (lines[i].startsWith("## ")) {
|
|
46
|
+
if (sections.length > 0) sections[sections.length - 1].end = i;
|
|
47
|
+
sections.push({ header: lines[i].trim(), start: i, end: lines.length });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return sections;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isBullet(line: string): boolean {
|
|
54
|
+
return /^\s*-\s+/.test(line);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Remove oldest bullets from `## Environment notes` until the file fits the
|
|
59
|
+
* budget. Returns the new line list and the removed bullet lines.
|
|
60
|
+
* Exported for reuse by `forge doctor --fix` (RFC-1151).
|
|
61
|
+
*/
|
|
62
|
+
export function compactMemoryMd(
|
|
63
|
+
content: string,
|
|
64
|
+
budget: number,
|
|
65
|
+
): { lines: string[]; removedLines: string[]; fitsBudget: boolean } {
|
|
66
|
+
if (content.length <= budget) {
|
|
67
|
+
return { lines: content.split("\n"), removedLines: [], fitsBudget: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lines = content.split("\n");
|
|
71
|
+
const sections = parseSections(lines);
|
|
72
|
+
const target = sections.find((s) => s.header === COMPACT_SECTION);
|
|
73
|
+
|
|
74
|
+
const removedLines: string[] = [];
|
|
75
|
+
if (target) {
|
|
76
|
+
// Sanity: never remove from protected sections even if headers drift.
|
|
77
|
+
if (PROTECTED_SECTIONS.has(target.header)) {
|
|
78
|
+
return { lines, removedLines, fitsBudget: false };
|
|
79
|
+
}
|
|
80
|
+
// Remove bullet lines oldest-first (document order) within the section.
|
|
81
|
+
let i = target.start + 1;
|
|
82
|
+
while (i < target.end && lines.join("\n").length > budget) {
|
|
83
|
+
if (isBullet(lines[i])) {
|
|
84
|
+
removedLines.push(lines[i]);
|
|
85
|
+
lines.splice(i, 1);
|
|
86
|
+
target.end--;
|
|
87
|
+
// Recompute section ends after splice — only the target shrinks.
|
|
88
|
+
} else {
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const fitsBudget = lines.join("\n").length <= budget;
|
|
95
|
+
return { lines, removedLines, fitsBudget };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function runMemoryCompact(
|
|
99
|
+
_input: ForgeCommandInput,
|
|
100
|
+
context: ForgeRuntimeContext,
|
|
101
|
+
): Promise<ForgeCommandResult<MemoryCompactResult>> {
|
|
102
|
+
const { workspaceRoot, logger, outputFormat, dryRun } = context;
|
|
103
|
+
const memoryPath = path.join(workspaceRoot, MEMORY_MD_PATH);
|
|
104
|
+
const budget = resolveMemoryBudget(workspaceRoot);
|
|
105
|
+
|
|
106
|
+
const fail = (msg: string): ForgeCommandResult<MemoryCompactResult> => ({
|
|
107
|
+
data: {
|
|
108
|
+
command: "forge.memory.compact",
|
|
109
|
+
status: "fail",
|
|
110
|
+
removed: 0,
|
|
111
|
+
remainingChars: 0,
|
|
112
|
+
budget,
|
|
113
|
+
errors: [msg],
|
|
114
|
+
},
|
|
115
|
+
exitCode: 1,
|
|
116
|
+
summary: `forge.memory.compact: failed — ${msg}`,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!fs.existsSync(memoryPath)) {
|
|
120
|
+
return {
|
|
121
|
+
data: {
|
|
122
|
+
command: "forge.memory.compact",
|
|
123
|
+
status: "pass",
|
|
124
|
+
removed: 0,
|
|
125
|
+
remainingChars: 0,
|
|
126
|
+
budget,
|
|
127
|
+
errors: [],
|
|
128
|
+
},
|
|
129
|
+
exitCode: 0,
|
|
130
|
+
summary: "forge.memory.compact: OK — MEMORY.md absent, nothing to compact",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const content = fs.readFileSync(memoryPath, "utf8");
|
|
135
|
+
if (content.length <= budget) {
|
|
136
|
+
return {
|
|
137
|
+
data: {
|
|
138
|
+
command: "forge.memory.compact",
|
|
139
|
+
status: "pass",
|
|
140
|
+
removed: 0,
|
|
141
|
+
remainingChars: content.length,
|
|
142
|
+
budget,
|
|
143
|
+
errors: [],
|
|
144
|
+
},
|
|
145
|
+
exitCode: 0,
|
|
146
|
+
summary: `forge.memory.compact: OK — already within budget (${content.length}/${budget} chars)`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { lines, removedLines, fitsBudget } = compactMemoryMd(content, budget);
|
|
151
|
+
const remainingChars = lines.join("\n").length;
|
|
152
|
+
|
|
153
|
+
if (!fitsBudget) {
|
|
154
|
+
return fail(
|
|
155
|
+
`still over budget after removing ${removedLines.length} Environment notes bullet(s) ` +
|
|
156
|
+
`(${remainingChars}/${budget} chars) — manual editorial compaction required`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!dryRun) {
|
|
161
|
+
fs.writeFileSync(memoryPath, lines.join("\n"), "utf8");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (outputFormat === "pretty") {
|
|
165
|
+
for (const line of removedLines) {
|
|
166
|
+
logger.info(`removed: ${line.slice(0, 80)}`);
|
|
167
|
+
}
|
|
168
|
+
logger.success(
|
|
169
|
+
`${dryRun ? "[dry-run] would remove" : "removed"} ${removedLines.length} bullet(s) — ${remainingChars}/${budget} chars`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
data: {
|
|
175
|
+
command: "forge.memory.compact",
|
|
176
|
+
status: "pass",
|
|
177
|
+
removed: removedLines.length,
|
|
178
|
+
remainingChars,
|
|
179
|
+
budget,
|
|
180
|
+
removedLines: dryRun ? removedLines : undefined,
|
|
181
|
+
errors: [],
|
|
182
|
+
},
|
|
183
|
+
exitCode: 0,
|
|
184
|
+
summary: dryRun
|
|
185
|
+
? `forge.memory.compact: [dry-run] would remove ${removedLines.length} bullet(s) — ${remainingChars}/${budget} chars`
|
|
186
|
+
: `forge.memory.compact: OK — removed ${removedLines.length} bullet(s), ${remainingChars}/${budget} chars`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -138,7 +138,7 @@ export function scaffoldMemoryLayer(workspaceRoot: string): MemoryScaffoldResult
|
|
|
138
138
|
// Health check
|
|
139
139
|
// ---------------------------------------------------------------------------
|
|
140
140
|
|
|
141
|
-
function resolveMemoryBudget(workspaceRoot: string): number {
|
|
141
|
+
export function resolveMemoryBudget(workspaceRoot: string): number {
|
|
142
142
|
try {
|
|
143
143
|
const config = loadForgeConfig(workspaceRoot);
|
|
144
144
|
if (config.bindings?.memory?.budget !== undefined && config.bindings?.memory?.budget !== null) {
|
|
@@ -56,7 +56,11 @@ export async function generateNestedAgentsMd(
|
|
|
56
56
|
dryRun: boolean,
|
|
57
57
|
workspaceTypes?: ProfileWorkspaceType[],
|
|
58
58
|
): Promise<NestedGenerateResult> {
|
|
59
|
-
const workspaces = discoverWorkspaces(
|
|
59
|
+
const workspaces = discoverWorkspaces(
|
|
60
|
+
workspaceRoot,
|
|
61
|
+
workspaceTypes,
|
|
62
|
+
config.bindings?.workspaces?.skipDirs,
|
|
63
|
+
);
|
|
60
64
|
const generated: string[] = [];
|
|
61
65
|
const skipped: string[] = [];
|
|
62
66
|
const renderedFiles: { [relPath: string]: string } = {};
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type ForgeBindings,
|
|
40
40
|
} from "../config/forge-config.ts";
|
|
41
41
|
import { FORGE_SKILLS, discoverPackSkills } from "../registry.ts";
|
|
42
|
+
import { syncKnowledgeFile } from "../knowledge/index.ts";
|
|
42
43
|
import { generateNestedAgentsMd } from "./nested-agents-generate.ts";
|
|
43
44
|
import { scaffoldMemoryLayer } from "./memory-scaffold.ts";
|
|
44
45
|
import type { SkippedSkill } from "./init.ts";
|
|
@@ -143,8 +144,9 @@ function syncForgeSkills(
|
|
|
143
144
|
forgeRoot: string,
|
|
144
145
|
skillsDir: string,
|
|
145
146
|
dryRun: boolean,
|
|
146
|
-
): string[] {
|
|
147
|
+
): { updated: string[]; knowledgeConflicts: string[] } {
|
|
147
148
|
const updated: string[] = [];
|
|
149
|
+
const knowledgeConflicts: string[] = [];
|
|
148
150
|
const agentsSkillsDir = path.join(workspaceRoot, skillsDir);
|
|
149
151
|
|
|
150
152
|
for (const skill of FORGE_SKILLS) {
|
|
@@ -160,7 +162,7 @@ function syncForgeSkills(
|
|
|
160
162
|
const content = fs.readFileSync(srcPath, "utf8");
|
|
161
163
|
fs.writeFileSync(destPath, content, "utf8");
|
|
162
164
|
|
|
163
|
-
// Sync knowledge files
|
|
165
|
+
// Sync knowledge files (append-only — never overwrite local entries)
|
|
164
166
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
165
167
|
if (fmMatch) {
|
|
166
168
|
try {
|
|
@@ -173,7 +175,10 @@ function syncForgeSkills(
|
|
|
173
175
|
const kfSrcPath = path.join(skillSrcDir, kf);
|
|
174
176
|
const kfDestPath = path.join(destDir, kf);
|
|
175
177
|
if (fs.existsSync(kfSrcPath)) {
|
|
176
|
-
|
|
178
|
+
const syncResult = syncKnowledgeFile(kfSrcPath, kfDestPath);
|
|
179
|
+
for (const id of syncResult.conflicts) {
|
|
180
|
+
knowledgeConflicts.push(`${skillName}/${kf}#${id}`);
|
|
181
|
+
}
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -185,7 +190,7 @@ function syncForgeSkills(
|
|
|
185
190
|
updated.push(skillName);
|
|
186
191
|
}
|
|
187
192
|
|
|
188
|
-
return updated;
|
|
193
|
+
return { updated, knowledgeConflicts };
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
function syncSharedKnowledge(
|
|
@@ -193,20 +198,24 @@ function syncSharedKnowledge(
|
|
|
193
198
|
forgeRoot: string,
|
|
194
199
|
skillsDir: string,
|
|
195
200
|
dryRun: boolean,
|
|
196
|
-
): string[] {
|
|
201
|
+
): { updated: string[]; knowledgeConflicts: string[] } {
|
|
197
202
|
const updated: string[] = [];
|
|
203
|
+
const knowledgeConflicts: string[] = [];
|
|
198
204
|
const srcPath = path.join(forgeRoot, "skills", "shared", "knowledge", "learned-principles.md");
|
|
199
|
-
if (!fs.existsSync(srcPath)) return updated;
|
|
205
|
+
if (!fs.existsSync(srcPath)) return { updated, knowledgeConflicts };
|
|
200
206
|
|
|
201
207
|
const destDir = path.join(workspaceRoot, skillsDir, "shared-knowledge");
|
|
202
208
|
const destPath = path.join(destDir, "learned-principles.md");
|
|
203
209
|
|
|
204
210
|
if (!dryRun) {
|
|
205
211
|
fs.mkdirSync(destDir, { recursive: true });
|
|
206
|
-
|
|
212
|
+
const syncResult = syncKnowledgeFile(srcPath, destPath);
|
|
213
|
+
for (const id of syncResult.conflicts) {
|
|
214
|
+
knowledgeConflicts.push(`shared-knowledge/learned-principles.md#${id}`);
|
|
215
|
+
}
|
|
207
216
|
}
|
|
208
217
|
updated.push("shared-knowledge");
|
|
209
|
-
return updated;
|
|
218
|
+
return { updated, knowledgeConflicts };
|
|
210
219
|
}
|
|
211
220
|
|
|
212
221
|
function syncPackSkills(
|
|
@@ -214,9 +223,10 @@ function syncPackSkills(
|
|
|
214
223
|
config: ForgeConfig,
|
|
215
224
|
skillsDir: string,
|
|
216
225
|
dryRun: boolean,
|
|
217
|
-
): { updated: string[]; skipped: SkippedSkill[] } {
|
|
226
|
+
): { updated: string[]; skipped: SkippedSkill[]; knowledgeConflicts: string[] } {
|
|
218
227
|
const updated: string[] = [];
|
|
219
228
|
const skipped: SkippedSkill[] = [];
|
|
229
|
+
const knowledgeConflicts: string[] = [];
|
|
220
230
|
const agentsSkillsDir = path.join(workspaceRoot, skillsDir);
|
|
221
231
|
let packSkills: ReturnType<typeof discoverPackSkills> = [];
|
|
222
232
|
try {
|
|
@@ -245,7 +255,7 @@ function syncPackSkills(
|
|
|
245
255
|
const content = fs.readFileSync(srcPath, "utf8");
|
|
246
256
|
fs.writeFileSync(destPath, content, "utf8");
|
|
247
257
|
|
|
248
|
-
// Sync knowledge files
|
|
258
|
+
// Sync knowledge files (append-only — never overwrite local entries)
|
|
249
259
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
250
260
|
if (fmMatch) {
|
|
251
261
|
try {
|
|
@@ -258,7 +268,10 @@ function syncPackSkills(
|
|
|
258
268
|
const kfSrcPath = path.join(skillSrcDir, kf);
|
|
259
269
|
const kfDestPath = path.join(destDir, kf);
|
|
260
270
|
if (fs.existsSync(kfSrcPath)) {
|
|
261
|
-
|
|
271
|
+
const syncResult = syncKnowledgeFile(kfSrcPath, kfDestPath);
|
|
272
|
+
for (const id of syncResult.conflicts) {
|
|
273
|
+
knowledgeConflicts.push(`${skillName}/${kf}#${id}`);
|
|
274
|
+
}
|
|
262
275
|
}
|
|
263
276
|
}
|
|
264
277
|
}
|
|
@@ -270,7 +283,7 @@ function syncPackSkills(
|
|
|
270
283
|
updated.push(skillName);
|
|
271
284
|
}
|
|
272
285
|
|
|
273
|
-
return { updated, skipped };
|
|
286
|
+
return { updated, skipped, knowledgeConflicts };
|
|
274
287
|
}
|
|
275
288
|
|
|
276
289
|
function addMissingBindingDefaults(
|
|
@@ -467,7 +480,7 @@ export async function runUpgrade(
|
|
|
467
480
|
}
|
|
468
481
|
|
|
469
482
|
// Step 3: Sync forge skills
|
|
470
|
-
const
|
|
483
|
+
const forgeSkillsResult = syncForgeSkills(
|
|
471
484
|
workspaceRoot,
|
|
472
485
|
forgeRoot,
|
|
473
486
|
config.paths.skillsDir,
|
|
@@ -483,14 +496,29 @@ export async function runUpgrade(
|
|
|
483
496
|
);
|
|
484
497
|
|
|
485
498
|
// Step 3c: Sync shared knowledge layer (RFC-0663)
|
|
486
|
-
const
|
|
499
|
+
const sharedKnowledgeResult = syncSharedKnowledge(
|
|
487
500
|
workspaceRoot,
|
|
488
501
|
forgeRoot,
|
|
489
502
|
config.paths.skillsDir,
|
|
490
503
|
isDryRun,
|
|
491
504
|
);
|
|
492
505
|
|
|
493
|
-
const skillsUpdated = [
|
|
506
|
+
const skillsUpdated = [
|
|
507
|
+
...forgeSkillsResult.updated,
|
|
508
|
+
...packResult.updated,
|
|
509
|
+
...sharedKnowledgeResult.updated,
|
|
510
|
+
];
|
|
511
|
+
const knowledgeConflicts = [
|
|
512
|
+
...forgeSkillsResult.knowledgeConflicts,
|
|
513
|
+
...packResult.knowledgeConflicts,
|
|
514
|
+
...sharedKnowledgeResult.knowledgeConflicts,
|
|
515
|
+
];
|
|
516
|
+
if (knowledgeConflicts.length > 0) {
|
|
517
|
+
context.logger.warn(
|
|
518
|
+
`forge.upgrade: ${knowledgeConflicts.length} knowledge entr${knowledgeConflicts.length === 1 ? "y" : "ies"} ` +
|
|
519
|
+
`diverge between package and project — local versions kept: ${knowledgeConflicts.join(", ")}`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
494
522
|
|
|
495
523
|
// Step 3d: Scaffold memory layer (RFC-0664)
|
|
496
524
|
const memoryScaffold = isDryRun ? { created: [], gitignoreUpdated: false, skipped: [] } : scaffoldMemoryLayer(workspaceRoot);
|
|
@@ -565,6 +593,9 @@ export async function runUpgrade(
|
|
|
565
593
|
? `[dry-run] forge.upgrade: would sync ${skillsUpdated.length} skill(s), add ${bindingsAdded.length} binding(s), update syncedVersion to ${toVersion}`
|
|
566
594
|
: npmCheck.warning
|
|
567
595
|
? `[forge.upgrade] OK — ${skillsUpdated.length} skill(s) synced, ${bindingsAdded.length} binding(s) added, syncedVersion → ${toVersion} — ⚠ ${npmCheck.warning}`
|
|
568
|
-
: `[forge.upgrade] OK — ${skillsUpdated.length} skill(s) synced, ${bindingsAdded.length} binding(s) added, syncedVersion → ${toVersion}
|
|
596
|
+
: `[forge.upgrade] OK — ${skillsUpdated.length} skill(s) synced, ${bindingsAdded.length} binding(s) added, syncedVersion → ${toVersion}` +
|
|
597
|
+
(knowledgeConflicts.length > 0
|
|
598
|
+
? ` — ${knowledgeConflicts.length} knowledge conflict(s), local kept`
|
|
599
|
+
: ""),
|
|
569
600
|
};
|
|
570
601
|
}
|