@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.
Files changed (268) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/src/config/forge-config.d.ts +1 -1
  5. package/dist/src/config/forge-config.d.ts.map +1 -1
  6. package/dist/src/config/forge-config.js +2 -2
  7. package/dist/src/config/forge-config.js.map +1 -1
  8. package/dist/src/onboarding/agents-generate.d.ts +2 -1
  9. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  10. package/dist/src/onboarding/agents-generate.js +24 -4
  11. package/dist/src/onboarding/agents-generate.js.map +1 -1
  12. package/dist/src/profiles/profile-schema.d.ts +2 -0
  13. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  14. package/dist/src/profiles/profile-schema.js +1 -0
  15. package/dist/src/profiles/profile-schema.js.map +1 -1
  16. package/dist/src/profiles/stack-profile.d.ts +1 -0
  17. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  18. package/dist/src/profiles/stack-profile.js +2 -0
  19. package/dist/src/profiles/stack-profile.js.map +1 -1
  20. package/os/adr/adr.module.ts +158 -0
  21. package/os/adr/frontmatter-io.ts +80 -0
  22. package/os/adr/handlers/archive.ts +244 -0
  23. package/os/adr/handlers/implement-stamp.ts +331 -0
  24. package/os/adr/handlers/list-create.ts +207 -0
  25. package/os/adr/handlers/validate.test.ts +232 -0
  26. package/os/adr/handlers/validate.ts +429 -0
  27. package/os/adr/index.ts +33 -0
  28. package/os/adr/types.ts +141 -0
  29. package/os/audit/audit.module.ts +48 -0
  30. package/os/audit/frontmatter-io.ts +84 -0
  31. package/os/audit/handlers/archive.ts +239 -0
  32. package/os/audit/index.ts +23 -0
  33. package/os/audit/types.ts +29 -0
  34. package/os/compass/compass.module.ts +186 -0
  35. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  36. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  37. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  38. package/os/compass/handlers/compass-inventory.ts +521 -0
  39. package/os/compass/handlers/git-revision.ts +137 -0
  40. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  41. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  42. package/os/compass/index.ts +33 -0
  43. package/os/core/core.module.ts +948 -0
  44. package/os/core/handlers/assets-check.ts +122 -0
  45. package/os/core/handlers/assets-helpers.ts +174 -0
  46. package/os/core/handlers/assets-list.ts +99 -0
  47. package/os/core/handlers/build.ts +155 -0
  48. package/os/core/handlers/determinism-check.ts +354 -0
  49. package/os/core/handlers/dev.ts +127 -0
  50. package/os/core/handlers/invariant-engine.test.ts +681 -0
  51. package/os/core/handlers/knowledge-compact.ts +248 -0
  52. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  53. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  54. package/os/core/handlers/note-link-validate.test.ts +117 -0
  55. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  56. package/os/core/handlers/package-health.test.ts +226 -0
  57. package/os/core/handlers/package-health.ts +231 -0
  58. package/os/core/handlers/pinned-check.ts +200 -0
  59. package/os/core/handlers/pinned-init.ts +333 -0
  60. package/os/core/handlers/pinned-types.ts +50 -0
  61. package/os/core/handlers/pinned-validate.ts +301 -0
  62. package/os/core/handlers/profile-resolve.ts +92 -0
  63. package/os/core/handlers/release-prepare.ts +294 -0
  64. package/os/core/handlers/release-publish.ts +211 -0
  65. package/os/core/handlers/validate.ts +262 -0
  66. package/os/core/handlers/workspace-deps.ts +55 -0
  67. package/os/core/index.ts +13 -0
  68. package/os/exploration/exploration.module.ts +90 -0
  69. package/os/exploration/frontmatter-io.ts +77 -0
  70. package/os/exploration/handlers/archive.ts +159 -0
  71. package/os/exploration/handlers/list.ts +77 -0
  72. package/os/exploration/handlers/show.ts +107 -0
  73. package/os/exploration/index.ts +25 -0
  74. package/os/exploration/types.ts +65 -0
  75. package/os/mission/handlers/archive.test.ts +391 -0
  76. package/os/mission/handlers/archive.ts +453 -0
  77. package/os/mission/index.ts +1 -0
  78. package/os/mission/mission.module.ts +48 -0
  79. package/os/mission/types.ts +41 -0
  80. package/os/naming/index.ts +14 -0
  81. package/os/naming/naming-convention.test.ts +163 -0
  82. package/os/naming/naming-convention.ts +378 -0
  83. package/os/naming/naming.module.ts +36 -0
  84. package/os/notes/index.ts +10 -0
  85. package/os/notes/notes.module.ts +109 -0
  86. package/os/plan/frontmatter-io.ts +82 -0
  87. package/os/plan/handlers/archive.ts +238 -0
  88. package/os/plan/index.ts +23 -0
  89. package/os/plan/plan.module.ts +48 -0
  90. package/os/plan/types.ts +29 -0
  91. package/os/program/discovery.ts +294 -0
  92. package/os/program/handlers/complete.ts +419 -0
  93. package/os/program/handlers/lease.ts +433 -0
  94. package/os/program/handlers/seal.ts +332 -0
  95. package/os/program/handlers/validate.ts +213 -0
  96. package/os/program/lease.ts +156 -0
  97. package/os/program/program.module.ts +212 -0
  98. package/os/program/schemas.ts +218 -0
  99. package/os/program/state.ts +264 -0
  100. package/os/rfc/acceptance.ts +344 -0
  101. package/os/rfc/decision-log.ts +310 -0
  102. package/os/rfc/dna-trace.ts +280 -0
  103. package/os/rfc/frontmatter-io.test.ts +132 -0
  104. package/os/rfc/frontmatter-io.ts +130 -0
  105. package/os/rfc/handlers/archive.ts +300 -0
  106. package/os/rfc/handlers/check.ts +238 -0
  107. package/os/rfc/handlers/implement-stamp.ts +515 -0
  108. package/os/rfc/handlers/index-graph.ts +237 -0
  109. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  110. package/os/rfc/handlers/lifecycle.ts +249 -0
  111. package/os/rfc/handlers/list-create.ts +296 -0
  112. package/os/rfc/handlers/pipeline-status.ts +173 -0
  113. package/os/rfc/handlers/shared.ts +121 -0
  114. package/os/rfc/handlers/supersede-propose.ts +241 -0
  115. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  116. package/os/rfc/handlers/validate-rules.ts +984 -0
  117. package/os/rfc/handlers/validate.ts +166 -0
  118. package/os/rfc/handlers.ts +19 -0
  119. package/os/rfc/index.ts +97 -0
  120. package/os/rfc/rfc.module.ts +416 -0
  121. package/os/rfc/types.ts +596 -0
  122. package/os/rfc/verification-evidence.ts +244 -0
  123. package/os/session/atif-parser.ts +166 -0
  124. package/os/session/frontmatter-io.ts +163 -0
  125. package/os/session/handlers/archive.ts +288 -0
  126. package/os/session/handlers/list.ts +174 -0
  127. package/os/session/handlers/save.ts +348 -0
  128. package/os/session/handlers/validate.ts +250 -0
  129. package/os/session/index.ts +50 -0
  130. package/os/session/session.module.ts +126 -0
  131. package/os/session/types.ts +203 -0
  132. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  133. package/os/spec/live-spec-list.ts +78 -0
  134. package/os/spec/live-spec-merge.test.ts +261 -0
  135. package/os/spec/live-spec-merge.ts +413 -0
  136. package/os/spec/live-spec-show.ts +117 -0
  137. package/os/spec/live-spec-types.ts +93 -0
  138. package/os/spec/live-spec-validate.ts +170 -0
  139. package/os/spec/spec-materialize.ts +390 -0
  140. package/os/spec/spec-schema.ts +174 -0
  141. package/os/spec/spec-status.ts +284 -0
  142. package/os/spec/spec-validate.ts +513 -0
  143. package/os/spec/spec.module.ts +135 -0
  144. package/os/werkstatt/handlers/lock.ts +168 -0
  145. package/os/werkstatt/handlers/schema.ts +47 -0
  146. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  147. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  148. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  149. package/os/werkstatt/index.ts +39 -0
  150. package/os/werkstatt/werkstatt.module.ts +62 -0
  151. package/os/workflow/handlers.ts +304 -0
  152. package/os/workflow/index.ts +28 -0
  153. package/os/workflow/types.ts +99 -0
  154. package/os/workflow/workflow.module.ts +54 -0
  155. package/package.json +3 -3
  156. package/profiles/godot-csharp.yaml +5 -4
  157. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  158. package/profiles/phaser-turborepo.yaml +5 -5
  159. package/profiles/root-agents-godot.md +133 -0
  160. package/skills/_shared/fo-session-summary.md +132 -16
  161. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  162. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  163. package/skills/fo/fo-handoff/SKILL.md +5 -0
  164. package/skills/fo/fo-idea/SKILL.md +1 -1
  165. package/skills/fo/fo-review/SKILL.md +1 -1
  166. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  167. package/skills/fo/fo-session-save/SKILL.md +8 -0
  168. package/src/cli-output.ts +59 -0
  169. package/src/config/forge-config.ts +524 -0
  170. package/src/forge-module.ts +39 -0
  171. package/src/index.ts +172 -0
  172. package/src/knowledge/budgets.ts +202 -0
  173. package/src/knowledge/compact.ts +405 -0
  174. package/src/knowledge/index.ts +53 -0
  175. package/src/knowledge/parse.ts +227 -0
  176. package/src/knowledge/promote.ts +156 -0
  177. package/src/knowledge/schema.ts +112 -0
  178. package/src/knowledge/serialize.ts +72 -0
  179. package/src/migration-adapters/git-utils.ts +71 -0
  180. package/src/migration-adapters/ignored-files.ts +221 -0
  181. package/src/migration-adapters/index.ts +21 -0
  182. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  183. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  184. package/src/migration-adapters/registry.ts +61 -0
  185. package/src/migration-adapters/types.ts +68 -0
  186. package/src/onboarding/agents-generate.ts +443 -0
  187. package/src/onboarding/create.ts +427 -0
  188. package/src/onboarding/doctor.ts +1297 -0
  189. package/src/onboarding/init.ts +337 -0
  190. package/src/onboarding/invariant-engine.ts +448 -0
  191. package/src/onboarding/memory-scaffold.ts +189 -0
  192. package/src/onboarding/nested-agents-generate.ts +93 -0
  193. package/src/onboarding/nested-agents-templates.ts +233 -0
  194. package/src/onboarding/profile-validate.ts +139 -0
  195. package/src/onboarding/scaffold-project.ts +299 -0
  196. package/src/onboarding/scaffold.ts +143 -0
  197. package/src/onboarding/upgrade.ts +558 -0
  198. package/src/onboarding/workspace-discovery.ts +167 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +51 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +217 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/fs-atomic.ts +93 -0
  256. package/src/utils/fs-idempotent.ts +41 -0
  257. package/src/utils/fs-trash-sync.ts +37 -0
  258. package/src/utils/fs-trash.ts +24 -0
  259. package/src/utils/fs.ts +74 -0
  260. package/src/utils/generated-marker.ts +166 -0
  261. package/src/utils/hash.ts +19 -0
  262. package/src/utils/index.ts +29 -0
  263. package/src/utils/string-utils.ts +19 -0
  264. package/src/validators/note-frontmatter-validate.ts +142 -0
  265. package/src/validators/note-link-validate.ts +145 -0
  266. package/src/validators/note-orphan-detect.ts +146 -0
  267. package/src/validators/port-validate.ts +97 -0
  268. 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
+ }