@warpgogol/forge 2.7.0 → 2.8.1
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 +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/src/config/forge-config.d.ts +1 -1
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -2
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/agents-generate.d.ts +2 -1
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +24 -4
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/profiles/profile-schema.d.ts +2 -0
- package/dist/src/profiles/profile-schema.d.ts.map +1 -1
- package/dist/src/profiles/profile-schema.js +1 -0
- package/dist/src/profiles/profile-schema.js.map +1 -1
- package/dist/src/profiles/stack-profile.d.ts +1 -0
- package/dist/src/profiles/stack-profile.d.ts.map +1 -1
- package/dist/src/profiles/stack-profile.js +2 -0
- package/dist/src/profiles/stack-profile.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +5 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/profiles/root-agents-godot.md +133 -0
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +51 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +217 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>forge.upgrade — additive sync for npm consumers. Refreshes .agents/skills/
|
|
4
|
+
from the installed forge package, adds missing binding defaults (RFC-0540) without
|
|
5
|
+
overwriting operator-set values, updates forge.syncedVersion, and runs forge.doctor.
|
|
6
|
+
With --update-npm, also updates @warpgogol/forge from npm before syncing (skipped in monorepo).</purpose>
|
|
7
|
+
<non-goals>
|
|
8
|
+
<item>Do not overwrite operator-set non-null bindings — additive only.</item>
|
|
9
|
+
<item>Do not create forge.yaml — that is forge.create's responsibility.</item>
|
|
10
|
+
<item>Do not modify PREFERENCES.md or AGENTS.md — upgrade is skill + binding sync only.</item>
|
|
11
|
+
</non-goals>
|
|
12
|
+
</MODULE_CONTRACT>
|
|
13
|
+
<CHANGE_SUMMARY>
|
|
14
|
+
<item>RFC-0543: initial forge.upgrade handler — 7-step additive sync flow.</item>
|
|
15
|
+
<item>RFC-0611: added nested AGENTS.md generation after skill sync.</item>
|
|
16
|
+
<item>RFC-0663: added syncSharedKnowledge step to sync shared knowledge layer to .agents/skills/shared-knowledge/.</item>
|
|
17
|
+
<item>RFC-0664: added scaffoldMemoryLayer step to scaffold .agents/memory/ and .gitignore block.</item>
|
|
18
|
+
<item>Added --update-npm flag: updates @warpgogol/forge from npm before syncing (skipped in monorepo).</item>
|
|
19
|
+
<item>Added npm latest-version check: warns when a newer @warpgogol/forge is available on npm.</item>
|
|
20
|
+
</CHANGE_SUMMARY>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { execSync } from "node:child_process";
|
|
26
|
+
import { stringify as stringifyYaml, parse as parseYaml } from "yaml";
|
|
27
|
+
import {
|
|
28
|
+
FORGE_CLI_BINDING_DEFAULTS,
|
|
29
|
+
resolveForgeRoot,
|
|
30
|
+
loadForgeConfig,
|
|
31
|
+
resolvePmRunner,
|
|
32
|
+
resolvePmInstall,
|
|
33
|
+
type ForgeConfig,
|
|
34
|
+
type ForgeBindings,
|
|
35
|
+
} from "../config/forge-config.ts";
|
|
36
|
+
import { FORGE_SKILLS, discoverPackSkills } from "../registry.ts";
|
|
37
|
+
import { generateNestedAgentsMd } from "./nested-agents-generate.ts";
|
|
38
|
+
import { scaffoldMemoryLayer } from "./memory-scaffold.ts";
|
|
39
|
+
import type { SkippedSkill } from "./init.ts";
|
|
40
|
+
import type { ForgeCommandInput, ForgeCommandResult, ForgeNextStep, ForgeRuntimeContext } from "../types.ts";
|
|
41
|
+
|
|
42
|
+
export interface UpgradeResult {
|
|
43
|
+
command: "forge.upgrade";
|
|
44
|
+
status: "pass" | "noop" | "fail";
|
|
45
|
+
fromVersion: string;
|
|
46
|
+
toVersion: string;
|
|
47
|
+
skillsUpdated: string[];
|
|
48
|
+
bindingsAdded: { key: string; value: string }[];
|
|
49
|
+
skippedSkills: SkippedSkill[];
|
|
50
|
+
nestedAgentsGenerated: string[];
|
|
51
|
+
memoryScaffold: { created: string[]; gitignoreUpdated: boolean; skipped: string[] };
|
|
52
|
+
npmUpdated: boolean;
|
|
53
|
+
npmUpdateSkipped: string | null;
|
|
54
|
+
latestNpmVersion: string | null;
|
|
55
|
+
npmVersionWarning: string | null;
|
|
56
|
+
doctorReport: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isMonorepoForge(workspaceRoot: string): boolean {
|
|
60
|
+
return fs.existsSync(path.join(workspaceRoot, "packages", "forge", "package.json"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function updateNpmPackage(
|
|
64
|
+
workspaceRoot: string,
|
|
65
|
+
config: ForgeConfig,
|
|
66
|
+
isDryRun: boolean,
|
|
67
|
+
): { updated: boolean; skipped: string | null } {
|
|
68
|
+
if (isMonorepoForge(workspaceRoot)) {
|
|
69
|
+
return { updated: false, skipped: "monorepo (local package, not npm-installed)" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pm = config.project.packageManager;
|
|
73
|
+
const installCmd = resolvePmInstall(pm);
|
|
74
|
+
const fullCmd = `${installCmd} @warpgogol/forge@latest`;
|
|
75
|
+
|
|
76
|
+
if (isDryRun) {
|
|
77
|
+
return { updated: false, skipped: "dry-run" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
execSync(fullCmd, {
|
|
82
|
+
cwd: workspaceRoot,
|
|
83
|
+
stdio: "pipe",
|
|
84
|
+
timeout: 120_000,
|
|
85
|
+
});
|
|
86
|
+
return { updated: true, skipped: null };
|
|
87
|
+
} catch {
|
|
88
|
+
return { updated: false, skipped: "install failed (network error or registry unavailable)" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readForgePackageVersion(forgeRoot: string): string {
|
|
93
|
+
const pkgPath = path.join(forgeRoot, "package.json");
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { version?: string };
|
|
95
|
+
return pkg.version ?? "0.0.0-unknown";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function checkLatestNpmVersion(
|
|
99
|
+
workspaceRoot: string,
|
|
100
|
+
installedVersion: string,
|
|
101
|
+
isMonorepo: boolean,
|
|
102
|
+
): { latest: string | null; warning: string | null } {
|
|
103
|
+
if (isMonorepo) {
|
|
104
|
+
return { latest: null, warning: null };
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const latest = execSync("npm view @warpgogol/forge version", {
|
|
108
|
+
cwd: workspaceRoot,
|
|
109
|
+
stdio: "pipe",
|
|
110
|
+
timeout: 15_000,
|
|
111
|
+
encoding: "utf8",
|
|
112
|
+
}).trim();
|
|
113
|
+
if (!latest || !/^\d+\.\d+\.\d+/.test(latest)) {
|
|
114
|
+
return { latest: null, warning: null };
|
|
115
|
+
}
|
|
116
|
+
if (latest !== installedVersion) {
|
|
117
|
+
const pm = (() => {
|
|
118
|
+
try {
|
|
119
|
+
const config = loadForgeConfig(workspaceRoot);
|
|
120
|
+
return resolvePmInstall(config.project.packageManager);
|
|
121
|
+
} catch {
|
|
122
|
+
return "npm install";
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
return {
|
|
126
|
+
latest,
|
|
127
|
+
warning: `Forge ${installedVersion} is installed, but ${latest} is available on npm. Run: ${pm} @warpgogol/forge@latest`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return { latest, warning: null };
|
|
131
|
+
} catch {
|
|
132
|
+
return { latest: null, warning: null };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function syncForgeSkills(
|
|
137
|
+
workspaceRoot: string,
|
|
138
|
+
forgeRoot: string,
|
|
139
|
+
skillsDir: string,
|
|
140
|
+
dryRun: boolean,
|
|
141
|
+
): string[] {
|
|
142
|
+
const updated: string[] = [];
|
|
143
|
+
const agentsSkillsDir = path.join(workspaceRoot, skillsDir);
|
|
144
|
+
|
|
145
|
+
for (const skill of FORGE_SKILLS) {
|
|
146
|
+
const srcPath = path.join(forgeRoot, skill.path);
|
|
147
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
148
|
+
|
|
149
|
+
const skillName = skill.name;
|
|
150
|
+
const destDir = path.join(agentsSkillsDir, skillName);
|
|
151
|
+
const destPath = path.join(destDir, "SKILL.md");
|
|
152
|
+
|
|
153
|
+
if (!dryRun) {
|
|
154
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
155
|
+
const content = fs.readFileSync(srcPath, "utf8");
|
|
156
|
+
fs.writeFileSync(destPath, content, "utf8");
|
|
157
|
+
|
|
158
|
+
// Sync knowledge files
|
|
159
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
160
|
+
if (fmMatch) {
|
|
161
|
+
try {
|
|
162
|
+
const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
163
|
+
const knowledgeFiles = fm["knowledge"];
|
|
164
|
+
if (Array.isArray(knowledgeFiles)) {
|
|
165
|
+
const skillSrcDir = path.dirname(srcPath);
|
|
166
|
+
for (const kf of knowledgeFiles) {
|
|
167
|
+
if (typeof kf !== "string") continue;
|
|
168
|
+
const kfSrcPath = path.join(skillSrcDir, kf);
|
|
169
|
+
const kfDestPath = path.join(destDir, kf);
|
|
170
|
+
if (fs.existsSync(kfSrcPath)) {
|
|
171
|
+
fs.writeFileSync(kfDestPath, fs.readFileSync(kfSrcPath, "utf8"), "utf8");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// Frontmatter parse error — SKILL-01 will catch in validation
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
updated.push(skillName);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return updated;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function syncSharedKnowledge(
|
|
187
|
+
workspaceRoot: string,
|
|
188
|
+
forgeRoot: string,
|
|
189
|
+
skillsDir: string,
|
|
190
|
+
dryRun: boolean,
|
|
191
|
+
): string[] {
|
|
192
|
+
const updated: string[] = [];
|
|
193
|
+
const srcPath = path.join(forgeRoot, "skills", "shared", "knowledge", "learned-principles.md");
|
|
194
|
+
if (!fs.existsSync(srcPath)) return updated;
|
|
195
|
+
|
|
196
|
+
const destDir = path.join(workspaceRoot, skillsDir, "shared-knowledge");
|
|
197
|
+
const destPath = path.join(destDir, "learned-principles.md");
|
|
198
|
+
|
|
199
|
+
if (!dryRun) {
|
|
200
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
201
|
+
fs.writeFileSync(destPath, fs.readFileSync(srcPath, "utf8"), "utf8");
|
|
202
|
+
}
|
|
203
|
+
updated.push("shared-knowledge");
|
|
204
|
+
return updated;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function syncPackSkills(
|
|
208
|
+
workspaceRoot: string,
|
|
209
|
+
config: ForgeConfig,
|
|
210
|
+
skillsDir: string,
|
|
211
|
+
dryRun: boolean,
|
|
212
|
+
): { updated: string[]; skipped: SkippedSkill[] } {
|
|
213
|
+
const updated: string[] = [];
|
|
214
|
+
const skipped: SkippedSkill[] = [];
|
|
215
|
+
const agentsSkillsDir = path.join(workspaceRoot, skillsDir);
|
|
216
|
+
const packSkills = discoverPackSkills(workspaceRoot, config);
|
|
217
|
+
const forgeSkillNames = new Set(FORGE_SKILLS.map((s) => s.name));
|
|
218
|
+
|
|
219
|
+
for (const skill of packSkills) {
|
|
220
|
+
const srcPath = path.join(workspaceRoot, skill.dir, skill.path);
|
|
221
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
222
|
+
|
|
223
|
+
const skillName = skill.name;
|
|
224
|
+
|
|
225
|
+
if (forgeSkillNames.has(skillName)) {
|
|
226
|
+
skipped.push({ name: skillName, reason: "conflict with Forge skill" });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const destDir = path.join(agentsSkillsDir, skillName);
|
|
231
|
+
const destPath = path.join(destDir, "SKILL.md");
|
|
232
|
+
|
|
233
|
+
if (!dryRun) {
|
|
234
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
235
|
+
const content = fs.readFileSync(srcPath, "utf8");
|
|
236
|
+
fs.writeFileSync(destPath, content, "utf8");
|
|
237
|
+
|
|
238
|
+
// Sync knowledge files
|
|
239
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
240
|
+
if (fmMatch) {
|
|
241
|
+
try {
|
|
242
|
+
const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
243
|
+
const knowledgeFiles = fm["knowledge"];
|
|
244
|
+
if (Array.isArray(knowledgeFiles)) {
|
|
245
|
+
const skillSrcDir = path.dirname(srcPath);
|
|
246
|
+
for (const kf of knowledgeFiles) {
|
|
247
|
+
if (typeof kf !== "string") continue;
|
|
248
|
+
const kfSrcPath = path.join(skillSrcDir, kf);
|
|
249
|
+
const kfDestPath = path.join(destDir, kf);
|
|
250
|
+
if (fs.existsSync(kfSrcPath)) {
|
|
251
|
+
fs.writeFileSync(kfDestPath, fs.readFileSync(kfSrcPath, "utf8"), "utf8");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// Frontmatter parse error
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
updated.push(skillName);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { updated, skipped };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function addMissingBindingDefaults(
|
|
267
|
+
config: ForgeConfig,
|
|
268
|
+
dryRun: boolean,
|
|
269
|
+
): { key: string; value: string }[] {
|
|
270
|
+
if (!config.bindings) return [];
|
|
271
|
+
|
|
272
|
+
const pm = config.project.packageManager;
|
|
273
|
+
const runner = resolvePmRunner(pm);
|
|
274
|
+
const added: { key: string; value: string }[] = [];
|
|
275
|
+
|
|
276
|
+
for (const entry of FORGE_CLI_BINDING_DEFAULTS) {
|
|
277
|
+
const bareKey = entry.key.replace("commands.", "") as keyof ForgeBindings["commands"];
|
|
278
|
+
const currentValue = config.bindings.commands[bareKey];
|
|
279
|
+
|
|
280
|
+
if (currentValue === null || currentValue === undefined) {
|
|
281
|
+
const resolvedValue = `${runner} ${entry.template}`;
|
|
282
|
+
added.push({ key: entry.key, value: resolvedValue });
|
|
283
|
+
|
|
284
|
+
if (!dryRun) {
|
|
285
|
+
config.bindings.commands[bareKey] = resolvedValue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return added;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function updateSyncedVersion(
|
|
294
|
+
workspaceRoot: string,
|
|
295
|
+
config: ForgeConfig,
|
|
296
|
+
version: string,
|
|
297
|
+
dryRun: boolean,
|
|
298
|
+
): void {
|
|
299
|
+
if (dryRun) return;
|
|
300
|
+
|
|
301
|
+
// Set the syncedVersion on the in-memory config (bindings already updated)
|
|
302
|
+
config.forge = { syncedVersion: version };
|
|
303
|
+
|
|
304
|
+
// Write the full config back to forge.yaml
|
|
305
|
+
const forgeYamlPath = path.join(workspaceRoot, "forge.yaml");
|
|
306
|
+
fs.writeFileSync(forgeYamlPath, stringifyYaml(config), "utf8");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function runUpgrade(
|
|
310
|
+
input: ForgeCommandInput,
|
|
311
|
+
context: ForgeRuntimeContext,
|
|
312
|
+
): Promise<ForgeCommandResult<UpgradeResult>> {
|
|
313
|
+
const { workspaceRoot, dryRun } = context;
|
|
314
|
+
const isDryRun = dryRun || input.flags["dry-run"] === true;
|
|
315
|
+
const isUpdateNpm = input.flags["update-npm"] === true;
|
|
316
|
+
|
|
317
|
+
// Step 0: Check forge.yaml exists
|
|
318
|
+
const forgeYamlPath = path.join(workspaceRoot, "forge.yaml");
|
|
319
|
+
if (!fs.existsSync(forgeYamlPath)) {
|
|
320
|
+
return {
|
|
321
|
+
data: {
|
|
322
|
+
command: "forge.upgrade",
|
|
323
|
+
status: "fail",
|
|
324
|
+
fromVersion: "",
|
|
325
|
+
toVersion: "",
|
|
326
|
+
skillsUpdated: [],
|
|
327
|
+
bindingsAdded: [],
|
|
328
|
+
skippedSkills: [],
|
|
329
|
+
nestedAgentsGenerated: [],
|
|
330
|
+
memoryScaffold: { created: [], gitignoreUpdated: false, skipped: [] },
|
|
331
|
+
npmUpdated: false,
|
|
332
|
+
npmUpdateSkipped: null,
|
|
333
|
+
latestNpmVersion: null,
|
|
334
|
+
npmVersionWarning: null,
|
|
335
|
+
doctorReport: null,
|
|
336
|
+
},
|
|
337
|
+
nextSteps: [{ action: "Run 'forge create' to create forge.yaml first", kind: "required" }],
|
|
338
|
+
exitCode: 1,
|
|
339
|
+
summary: "[forge.upgrade] FAIL — forge.yaml not found. Run 'forge create' first.",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Step 0.5: Update npm package if --update-npm flag is set
|
|
344
|
+
let npmUpdated = false;
|
|
345
|
+
let npmUpdateSkipped: string | null = null;
|
|
346
|
+
if (isUpdateNpm) {
|
|
347
|
+
let configForNpm: ForgeConfig;
|
|
348
|
+
try {
|
|
349
|
+
configForNpm = loadForgeConfig(workspaceRoot);
|
|
350
|
+
} catch {
|
|
351
|
+
configForNpm = { project: { packageManager: "npm" } } as unknown as ForgeConfig;
|
|
352
|
+
}
|
|
353
|
+
const npmResult = updateNpmPackage(workspaceRoot, configForNpm, isDryRun);
|
|
354
|
+
npmUpdated = npmResult.updated;
|
|
355
|
+
npmUpdateSkipped = npmResult.skipped;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Step 1: Resolve forge root and read installed version
|
|
359
|
+
let forgeRoot: string;
|
|
360
|
+
let toVersion: string;
|
|
361
|
+
try {
|
|
362
|
+
forgeRoot = context.forgeRoot ?? resolveForgeRoot(workspaceRoot);
|
|
363
|
+
toVersion = readForgePackageVersion(forgeRoot);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
return {
|
|
366
|
+
data: {
|
|
367
|
+
command: "forge.upgrade",
|
|
368
|
+
status: "fail",
|
|
369
|
+
fromVersion: "",
|
|
370
|
+
toVersion: "",
|
|
371
|
+
skillsUpdated: [],
|
|
372
|
+
bindingsAdded: [],
|
|
373
|
+
skippedSkills: [],
|
|
374
|
+
nestedAgentsGenerated: [],
|
|
375
|
+
memoryScaffold: { created: [], gitignoreUpdated: false, skipped: [] },
|
|
376
|
+
npmUpdated: false,
|
|
377
|
+
npmUpdateSkipped: null,
|
|
378
|
+
latestNpmVersion: null,
|
|
379
|
+
npmVersionWarning: null,
|
|
380
|
+
doctorReport: null,
|
|
381
|
+
},
|
|
382
|
+
nextSteps: [
|
|
383
|
+
{ action: "Install @warpgogol/forge first: npm install @warpgogol/forge", kind: "required" },
|
|
384
|
+
],
|
|
385
|
+
exitCode: 1,
|
|
386
|
+
summary: `[forge.upgrade] FAIL — ${(err as Error).message}`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Step 1.5: Check npm for newer version (non-fatal)
|
|
391
|
+
const npmCheck = checkLatestNpmVersion(workspaceRoot, toVersion, isMonorepoForge(workspaceRoot));
|
|
392
|
+
if (npmCheck.warning && context.outputFormat === "pretty") {
|
|
393
|
+
context.logger.warn(`⚠ ${npmCheck.warning}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Step 2: Load config and check syncedVersion
|
|
397
|
+
let config: ForgeConfig;
|
|
398
|
+
try {
|
|
399
|
+
config = loadForgeConfig(workspaceRoot);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return {
|
|
402
|
+
data: {
|
|
403
|
+
command: "forge.upgrade",
|
|
404
|
+
status: "fail",
|
|
405
|
+
fromVersion: "",
|
|
406
|
+
toVersion,
|
|
407
|
+
skillsUpdated: [],
|
|
408
|
+
bindingsAdded: [],
|
|
409
|
+
skippedSkills: [],
|
|
410
|
+
nestedAgentsGenerated: [],
|
|
411
|
+
memoryScaffold: { created: [], gitignoreUpdated: false, skipped: [] },
|
|
412
|
+
npmUpdated: false,
|
|
413
|
+
npmUpdateSkipped: null,
|
|
414
|
+
latestNpmVersion: npmCheck.latest,
|
|
415
|
+
npmVersionWarning: npmCheck.warning,
|
|
416
|
+
doctorReport: null,
|
|
417
|
+
},
|
|
418
|
+
nextSteps: [
|
|
419
|
+
{ action: `Fix forge.yaml: ${(err as Error).message}`, kind: "required" },
|
|
420
|
+
],
|
|
421
|
+
exitCode: 1,
|
|
422
|
+
summary: `[forge.upgrade] FAIL — forge.yaml invalid: ${(err as Error).message}`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const fromVersion = config.forge?.syncedVersion ?? null;
|
|
427
|
+
|
|
428
|
+
if (fromVersion === toVersion) {
|
|
429
|
+
// Noop — versions match
|
|
430
|
+
return {
|
|
431
|
+
data: {
|
|
432
|
+
command: "forge.upgrade",
|
|
433
|
+
status: "noop",
|
|
434
|
+
fromVersion,
|
|
435
|
+
toVersion,
|
|
436
|
+
skillsUpdated: [],
|
|
437
|
+
bindingsAdded: [],
|
|
438
|
+
skippedSkills: [],
|
|
439
|
+
nestedAgentsGenerated: [],
|
|
440
|
+
memoryScaffold: { created: [], gitignoreUpdated: false, skipped: [] },
|
|
441
|
+
npmUpdated: npmUpdated,
|
|
442
|
+
npmUpdateSkipped: npmUpdateSkipped,
|
|
443
|
+
latestNpmVersion: npmCheck.latest,
|
|
444
|
+
npmVersionWarning: npmCheck.warning,
|
|
445
|
+
doctorReport: null,
|
|
446
|
+
},
|
|
447
|
+
nextSteps: npmCheck.warning
|
|
448
|
+
? [{ action: npmCheck.warning, kind: "optional" }]
|
|
449
|
+
: [],
|
|
450
|
+
exitCode: 0,
|
|
451
|
+
summary: npmCheck.warning
|
|
452
|
+
? `[forge.upgrade] Already up to date (v${toVersion}) — ⚠ ${npmCheck.warning}`
|
|
453
|
+
: `[forge.upgrade] Already up to date (v${toVersion})`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Step 3: Sync forge skills
|
|
458
|
+
const forgeSkillsUpdated = syncForgeSkills(
|
|
459
|
+
workspaceRoot,
|
|
460
|
+
forgeRoot,
|
|
461
|
+
config.paths.skillsDir,
|
|
462
|
+
isDryRun,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// Step 3b: Sync pack skills
|
|
466
|
+
const packResult = syncPackSkills(
|
|
467
|
+
workspaceRoot,
|
|
468
|
+
config,
|
|
469
|
+
config.paths.skillsDir,
|
|
470
|
+
isDryRun,
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Step 3c: Sync shared knowledge layer (RFC-0663)
|
|
474
|
+
const sharedKnowledgeUpdated = syncSharedKnowledge(
|
|
475
|
+
workspaceRoot,
|
|
476
|
+
forgeRoot,
|
|
477
|
+
config.paths.skillsDir,
|
|
478
|
+
isDryRun,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const skillsUpdated = [...forgeSkillsUpdated, ...packResult.updated, ...sharedKnowledgeUpdated];
|
|
482
|
+
|
|
483
|
+
// Step 3d: Scaffold memory layer (RFC-0664)
|
|
484
|
+
const memoryScaffold = isDryRun ? { created: [], gitignoreUpdated: false, skipped: [] } : scaffoldMemoryLayer(workspaceRoot);
|
|
485
|
+
|
|
486
|
+
// Step 4: Add missing binding defaults
|
|
487
|
+
const bindingsAdded = addMissingBindingDefaults(config, isDryRun);
|
|
488
|
+
|
|
489
|
+
// Step 5: Update forge.syncedVersion
|
|
490
|
+
if (!isDryRun) {
|
|
491
|
+
updateSyncedVersion(workspaceRoot, config, toVersion, false);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Step 5b: Generate nested AGENTS.md (RFC-0611)
|
|
495
|
+
let nestedAgentsGenerated: string[] = [];
|
|
496
|
+
if (!isDryRun) {
|
|
497
|
+
try {
|
|
498
|
+
const nestedResult = await generateNestedAgentsMd(workspaceRoot, config, false);
|
|
499
|
+
nestedAgentsGenerated = nestedResult.generated;
|
|
500
|
+
} catch {
|
|
501
|
+
// Nested generation failure is non-fatal
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Step 6: Run doctor
|
|
506
|
+
let doctorReport: unknown = null;
|
|
507
|
+
if (!isDryRun) {
|
|
508
|
+
try {
|
|
509
|
+
const { runDoctor } = await import("./doctor.ts");
|
|
510
|
+
const doctorResult = await runDoctor(input, context);
|
|
511
|
+
doctorReport = doctorResult.data;
|
|
512
|
+
} catch {
|
|
513
|
+
// Doctor may fail in some contexts — non-fatal
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Step 7: Build nextSteps
|
|
518
|
+
const nextSteps: ForgeNextStep[] = [];
|
|
519
|
+
if (isDryRun) {
|
|
520
|
+
nextSteps.push({ action: "Re-run without --dry-run to apply changes", kind: "required" });
|
|
521
|
+
} else {
|
|
522
|
+
nextSteps.push({ action: "Review updated skills in .agents/skills/", kind: "optional" });
|
|
523
|
+
if (bindingsAdded.length > 0) {
|
|
524
|
+
nextSteps.push({
|
|
525
|
+
action: "Review newly added binding defaults in forge.yaml",
|
|
526
|
+
kind: "optional",
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const status: UpgradeResult["status"] = "pass";
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
data: {
|
|
535
|
+
command: "forge.upgrade",
|
|
536
|
+
status,
|
|
537
|
+
fromVersion: fromVersion ?? "never-synced",
|
|
538
|
+
toVersion,
|
|
539
|
+
skillsUpdated,
|
|
540
|
+
bindingsAdded,
|
|
541
|
+
skippedSkills: packResult.skipped,
|
|
542
|
+
nestedAgentsGenerated,
|
|
543
|
+
memoryScaffold,
|
|
544
|
+
npmUpdated,
|
|
545
|
+
npmUpdateSkipped,
|
|
546
|
+
latestNpmVersion: npmCheck.latest,
|
|
547
|
+
npmVersionWarning: npmCheck.warning,
|
|
548
|
+
doctorReport,
|
|
549
|
+
},
|
|
550
|
+
nextSteps,
|
|
551
|
+
exitCode: 0,
|
|
552
|
+
summary: isDryRun
|
|
553
|
+
? `[dry-run] forge.upgrade: would sync ${skillsUpdated.length} skill(s), add ${bindingsAdded.length} binding(s), update syncedVersion to ${toVersion}`
|
|
554
|
+
: npmCheck.warning
|
|
555
|
+
? `[forge.upgrade] OK — ${skillsUpdated.length} skill(s) synced, ${bindingsAdded.length} binding(s) added, syncedVersion → ${toVersion} — ⚠ ${npmCheck.warning}`
|
|
556
|
+
: `[forge.upgrade] OK — ${skillsUpdated.length} skill(s) synced, ${bindingsAdded.length} binding(s) added, syncedVersion → ${toVersion}`,
|
|
557
|
+
};
|
|
558
|
+
}
|