@warpgogol/forge 2.8.0 → 2.8.2

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 (271) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/os/session/handlers/validate.d.ts.map +1 -1
  5. package/dist/os/session/handlers/validate.js +23 -1
  6. package/dist/os/session/handlers/validate.js.map +1 -1
  7. package/dist/os/session/types.d.ts +31 -1
  8. package/dist/os/session/types.d.ts.map +1 -1
  9. package/dist/os/session/types.js +7 -0
  10. package/dist/os/session/types.js.map +1 -1
  11. package/os/adr/adr.module.ts +158 -0
  12. package/os/adr/frontmatter-io.ts +80 -0
  13. package/os/adr/handlers/archive.ts +244 -0
  14. package/os/adr/handlers/implement-stamp.ts +331 -0
  15. package/os/adr/handlers/list-create.ts +207 -0
  16. package/os/adr/handlers/validate.test.ts +232 -0
  17. package/os/adr/handlers/validate.ts +429 -0
  18. package/os/adr/index.ts +33 -0
  19. package/os/adr/types.ts +141 -0
  20. package/os/audit/audit.module.ts +48 -0
  21. package/os/audit/frontmatter-io.ts +84 -0
  22. package/os/audit/handlers/archive.ts +239 -0
  23. package/os/audit/index.ts +23 -0
  24. package/os/audit/types.ts +29 -0
  25. package/os/compass/compass.module.ts +186 -0
  26. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  27. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  28. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  29. package/os/compass/handlers/compass-inventory.ts +521 -0
  30. package/os/compass/handlers/git-revision.ts +137 -0
  31. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  32. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  33. package/os/compass/index.ts +33 -0
  34. package/os/core/core.module.ts +948 -0
  35. package/os/core/handlers/assets-check.ts +122 -0
  36. package/os/core/handlers/assets-helpers.ts +174 -0
  37. package/os/core/handlers/assets-list.ts +99 -0
  38. package/os/core/handlers/build.ts +155 -0
  39. package/os/core/handlers/determinism-check.ts +354 -0
  40. package/os/core/handlers/dev.ts +127 -0
  41. package/os/core/handlers/invariant-engine.test.ts +681 -0
  42. package/os/core/handlers/knowledge-compact.ts +248 -0
  43. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  44. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  45. package/os/core/handlers/note-link-validate.test.ts +117 -0
  46. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  47. package/os/core/handlers/package-health.test.ts +226 -0
  48. package/os/core/handlers/package-health.ts +231 -0
  49. package/os/core/handlers/pinned-check.ts +200 -0
  50. package/os/core/handlers/pinned-init.ts +333 -0
  51. package/os/core/handlers/pinned-types.ts +50 -0
  52. package/os/core/handlers/pinned-validate.ts +301 -0
  53. package/os/core/handlers/profile-resolve.ts +92 -0
  54. package/os/core/handlers/release-prepare.ts +294 -0
  55. package/os/core/handlers/release-publish.ts +211 -0
  56. package/os/core/handlers/validate.ts +262 -0
  57. package/os/core/handlers/workspace-deps.ts +55 -0
  58. package/os/core/index.ts +13 -0
  59. package/os/exploration/exploration.module.ts +90 -0
  60. package/os/exploration/frontmatter-io.ts +77 -0
  61. package/os/exploration/handlers/archive.ts +159 -0
  62. package/os/exploration/handlers/list.ts +77 -0
  63. package/os/exploration/handlers/show.ts +107 -0
  64. package/os/exploration/index.ts +25 -0
  65. package/os/exploration/types.ts +65 -0
  66. package/os/mission/handlers/archive.test.ts +391 -0
  67. package/os/mission/handlers/archive.ts +453 -0
  68. package/os/mission/index.ts +1 -0
  69. package/os/mission/mission.module.ts +48 -0
  70. package/os/mission/types.ts +41 -0
  71. package/os/naming/index.ts +14 -0
  72. package/os/naming/naming-convention.test.ts +163 -0
  73. package/os/naming/naming-convention.ts +378 -0
  74. package/os/naming/naming.module.ts +36 -0
  75. package/os/notes/index.ts +10 -0
  76. package/os/notes/notes.module.ts +109 -0
  77. package/os/plan/frontmatter-io.ts +82 -0
  78. package/os/plan/handlers/archive.ts +238 -0
  79. package/os/plan/index.ts +23 -0
  80. package/os/plan/plan.module.ts +48 -0
  81. package/os/plan/types.ts +29 -0
  82. package/os/program/discovery.ts +294 -0
  83. package/os/program/handlers/complete.ts +419 -0
  84. package/os/program/handlers/lease.ts +433 -0
  85. package/os/program/handlers/seal.ts +332 -0
  86. package/os/program/handlers/validate.ts +213 -0
  87. package/os/program/lease.ts +156 -0
  88. package/os/program/program.module.ts +212 -0
  89. package/os/program/schemas.ts +218 -0
  90. package/os/program/state.ts +264 -0
  91. package/os/rfc/acceptance.ts +344 -0
  92. package/os/rfc/decision-log.ts +310 -0
  93. package/os/rfc/dna-trace.ts +280 -0
  94. package/os/rfc/frontmatter-io.test.ts +132 -0
  95. package/os/rfc/frontmatter-io.ts +130 -0
  96. package/os/rfc/handlers/archive.ts +300 -0
  97. package/os/rfc/handlers/check.ts +238 -0
  98. package/os/rfc/handlers/implement-stamp.ts +515 -0
  99. package/os/rfc/handlers/index-graph.ts +237 -0
  100. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  101. package/os/rfc/handlers/lifecycle.ts +249 -0
  102. package/os/rfc/handlers/list-create.ts +296 -0
  103. package/os/rfc/handlers/pipeline-status.ts +173 -0
  104. package/os/rfc/handlers/shared.ts +121 -0
  105. package/os/rfc/handlers/supersede-propose.ts +241 -0
  106. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  107. package/os/rfc/handlers/validate-rules.ts +984 -0
  108. package/os/rfc/handlers/validate.ts +166 -0
  109. package/os/rfc/handlers.ts +19 -0
  110. package/os/rfc/index.ts +97 -0
  111. package/os/rfc/rfc.module.ts +416 -0
  112. package/os/rfc/types.ts +596 -0
  113. package/os/rfc/verification-evidence.ts +244 -0
  114. package/os/session/atif-parser.ts +166 -0
  115. package/os/session/frontmatter-io.ts +163 -0
  116. package/os/session/handlers/archive.ts +288 -0
  117. package/os/session/handlers/list.ts +174 -0
  118. package/os/session/handlers/save.ts +348 -0
  119. package/os/session/handlers/validate.ts +250 -0
  120. package/os/session/index.ts +50 -0
  121. package/os/session/session.module.ts +126 -0
  122. package/os/session/types.ts +203 -0
  123. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  124. package/os/spec/live-spec-list.ts +78 -0
  125. package/os/spec/live-spec-merge.test.ts +261 -0
  126. package/os/spec/live-spec-merge.ts +413 -0
  127. package/os/spec/live-spec-show.ts +117 -0
  128. package/os/spec/live-spec-types.ts +93 -0
  129. package/os/spec/live-spec-validate.ts +170 -0
  130. package/os/spec/spec-materialize.ts +390 -0
  131. package/os/spec/spec-schema.ts +174 -0
  132. package/os/spec/spec-status.ts +284 -0
  133. package/os/spec/spec-validate.ts +513 -0
  134. package/os/spec/spec.module.ts +135 -0
  135. package/os/werkstatt/handlers/lock.ts +168 -0
  136. package/os/werkstatt/handlers/schema.ts +47 -0
  137. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  138. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  139. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  140. package/os/werkstatt/index.ts +39 -0
  141. package/os/werkstatt/werkstatt.module.ts +62 -0
  142. package/os/workflow/handlers.ts +304 -0
  143. package/os/workflow/index.ts +28 -0
  144. package/os/workflow/types.ts +99 -0
  145. package/os/workflow/workflow.module.ts +54 -0
  146. package/package.json +3 -3
  147. package/profiles/godot-csharp.yaml +4 -4
  148. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  149. package/profiles/phaser-turborepo.yaml +5 -5
  150. package/skills/_shared/fo-session-summary.md +132 -16
  151. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  152. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  153. package/skills/fo/fo-handoff/SKILL.md +5 -0
  154. package/skills/fo/fo-idea/SKILL.md +1 -1
  155. package/skills/fo/fo-review/SKILL.md +1 -1
  156. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  157. package/skills/fo/fo-session-save/SKILL.md +8 -0
  158. package/src/cli-output.ts +59 -0
  159. package/src/config/__tests__/resolve-terminology.test.ts +45 -0
  160. package/src/config/forge-config.ts +524 -0
  161. package/src/forge-module.ts +39 -0
  162. package/src/index.ts +172 -0
  163. package/src/knowledge/__tests__/promote.test.ts +219 -0
  164. package/src/knowledge/__tests__/serialize.test.ts +192 -0
  165. package/src/knowledge/budgets.ts +202 -0
  166. package/src/knowledge/compact.ts +405 -0
  167. package/src/knowledge/index.ts +53 -0
  168. package/src/knowledge/parse.ts +227 -0
  169. package/src/knowledge/promote.ts +156 -0
  170. package/src/knowledge/schema.ts +112 -0
  171. package/src/knowledge/serialize.ts +72 -0
  172. package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
  173. package/src/migration-adapters/__tests__/registry.test.ts +131 -0
  174. package/src/migration-adapters/__tests__/types.test.ts +86 -0
  175. package/src/migration-adapters/git-utils.ts +71 -0
  176. package/src/migration-adapters/ignored-files.ts +221 -0
  177. package/src/migration-adapters/index.ts +21 -0
  178. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  179. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  180. package/src/migration-adapters/registry.ts +61 -0
  181. package/src/migration-adapters/types.ts +68 -0
  182. package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
  183. package/src/onboarding/agents-generate.ts +443 -0
  184. package/src/onboarding/create.ts +427 -0
  185. package/src/onboarding/doctor.ts +1297 -0
  186. package/src/onboarding/init.ts +337 -0
  187. package/src/onboarding/invariant-engine.ts +448 -0
  188. package/src/onboarding/memory-scaffold.ts +189 -0
  189. package/src/onboarding/nested-agents-generate.ts +93 -0
  190. package/src/onboarding/nested-agents-templates.ts +233 -0
  191. package/src/onboarding/profile-validate.ts +139 -0
  192. package/src/onboarding/scaffold-project.ts +299 -0
  193. package/src/onboarding/scaffold.ts +143 -0
  194. package/src/onboarding/upgrade.ts +558 -0
  195. package/src/onboarding/workspace-discovery.ts +167 -0
  196. package/src/profiles/__tests__/profile-schema.test.ts +228 -0
  197. package/src/profiles/__tests__/stack-profile.test.ts +190 -0
  198. package/src/profiles/__tests__/terminology-utils.test.ts +71 -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 +54 -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 +221 -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/__tests__/fs-idempotent.test.ts +73 -0
  256. package/src/utils/__tests__/fs-trash.test.ts +45 -0
  257. package/src/utils/fs-atomic.ts +93 -0
  258. package/src/utils/fs-idempotent.ts +41 -0
  259. package/src/utils/fs-trash-sync.ts +37 -0
  260. package/src/utils/fs-trash.ts +24 -0
  261. package/src/utils/fs.ts +74 -0
  262. package/src/utils/generated-marker.ts +166 -0
  263. package/src/utils/hash.ts +19 -0
  264. package/src/utils/index.ts +29 -0
  265. package/src/utils/string-utils.ts +19 -0
  266. package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
  267. package/src/validators/note-frontmatter-validate.ts +142 -0
  268. package/src/validators/note-link-validate.ts +145 -0
  269. package/src/validators/note-orphan-detect.ts +146 -0
  270. package/src/validators/port-validate.ts +97 -0
  271. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,443 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>forge.agents.generate — regenerates AGENTS.md deterministically from forge.yaml + skill registry. Carries the standard generated-file marker.</purpose>
4
+ <non-goals>
5
+ <item>Do not overwrite a hand-written AGENTS.md (no generated marker) — refuse with exit 1.</item>
6
+ <item>Do not read or modify forge.yaml — only read it via loadForgeConfig.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0391: initial forge.agents.generate handler.</item>
11
+ <item>RFC-0393: added Capabilities section rendered from resolved bindings.</item>
12
+ <item>RFC-0548: added Core behavioral layer section with intent-to-skill routing table from triggers, fixed policy text, and conditional extended layer (RFC-0549) based on register.</item>
13
+ <item>RFC-0549: replaced extended layer stub with full nine-section content from extended-behavioral-layer.ts.</item>
14
+ <item>RFC-0551: added register-conditional commit policy to core behavioral layer.</item>
15
+ <item>RFC-0611: added nested AGENTS.md generation for workspace directories + dryRun support.</item>
16
+ <item>RFC-0640: load workspaceTypes from stack profile and pass to generateNestedAgentsMd for profile-driven workspace detection.</item>
17
+ <item>RFC-0643: terminology substitution on final content, root template selection by register, details field in result.</item>
18
+ <item>RFC-0664: added project memory layer read discipline section to generated AGENTS.md.</item>
19
+ <item>Profile-driven root AGENTS.md template via rootAgentsMdTemplate field — profiles can now provide stack-specific root templates instead of generic business/creative templates.</item>
20
+ </CHANGE_SUMMARY>
21
+ */
22
+
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+ import { parse as parseYaml } from "yaml";
26
+ import { loadForgeConfig, resolveBinding, resolveForgeRoot } from "../config/forge-config.ts";
27
+ import { resolveAllTerminology } from "../profiles/terminology-utils.ts";
28
+ import { FORGE_SKILLS } from "../registry.ts";
29
+ import { buildGeneratedHeader, hasGeneratedMarker, writeFileIfChanged } from "../utils/index.ts";
30
+ import { generateNestedAgentsMd } from "./nested-agents-generate.ts";
31
+ import { listStackProfiles } from "../profiles/stack-profile.ts";
32
+ import type { StackProfile } from "../profiles/stack-profile.ts";
33
+ import type { ProfileWorkspaceType } from "../profiles/profile-schema.ts";
34
+ import type {
35
+ ForgeCommandInput,
36
+ ForgeCommandResult,
37
+ ForgeRuntimeContext,
38
+ } from "../types.ts";
39
+
40
+ type BehavioralRegister = "business" | "creative";
41
+
42
+ // RFC-0643: Template placeholder substitution for {{terminology.key}} patterns
43
+ export function substituteTemplate(
44
+ content: string,
45
+ terminology: Record<string, string>,
46
+ ): string {
47
+ return content.replace(
48
+ /\{\{terminology\.(\w+)\}\}/g,
49
+ (_, key: string) => terminology[key] ?? key,
50
+ );
51
+ }
52
+
53
+ // RFC-0643: Template context for root AGENTS.md generation
54
+ export interface TemplateContext {
55
+ terminology: Record<string, string>;
56
+ register: BehavioralRegister;
57
+ domain: string | null;
58
+ workspaceType?: string;
59
+ }
60
+
61
+ // RFC-0643: Root template paths
62
+ const BUSINESS_ROOT_TEMPLATE = path.join(import.meta.dirname, "templates", "root-agents-business.md");
63
+ const CREATIVE_ROOT_TEMPLATE = path.join(import.meta.dirname, "templates", "root-agents-creative.md");
64
+
65
+ // Behavioral layer template paths
66
+ const BEHAVIORAL_LAYER_CORE_TEMPLATE = path.join(import.meta.dirname, "templates", "behavioral-layer-core.md");
67
+ const BEHAVIORAL_LAYER_EXTENDED_TEMPLATE = path.join(import.meta.dirname, "templates", "behavioral-layer-extended.md");
68
+
69
+ export function selectRootTemplate(
70
+ register: BehavioralRegister,
71
+ profile?: StackProfile,
72
+ ): string {
73
+ // Profile-driven root template (rootAgentsMdTemplate)
74
+ if (profile?.rootAgentsMdTemplate) {
75
+ const templateRel = profile.rootAgentsMdTemplate;
76
+ // Path traversal guard: reject absolute paths and ..
77
+ if (!path.isAbsolute(templateRel) && !templateRel.includes("..")) {
78
+ const forgeRoot = path.resolve(import.meta.dirname, "..", "..");
79
+ const profilesDir = path.join(forgeRoot, "profiles");
80
+ const templatePath = path.resolve(profilesDir, templateRel);
81
+ // Ensure resolved path is within profiles directory
82
+ if (templatePath.startsWith(profilesDir + path.sep) || templatePath === profilesDir) {
83
+ try {
84
+ return fs.readFileSync(templatePath, "utf8");
85
+ } catch {
86
+ // Template file not found — fall through to register-based template
87
+ }
88
+ }
89
+ }
90
+ }
91
+ try {
92
+ const templatePath = register === "creative" ? CREATIVE_ROOT_TEMPLATE : BUSINESS_ROOT_TEMPLATE;
93
+ return fs.readFileSync(templatePath, "utf8");
94
+ } catch {
95
+ // Template file not found — fall back to empty string (dynamic sections will still be appended)
96
+ return "";
97
+ }
98
+ }
99
+
100
+ function replaceProjectPlaceholders(
101
+ template: string,
102
+ config: ReturnType<typeof loadForgeConfig>,
103
+ dynamicSections: string,
104
+ ): string {
105
+ let result = template;
106
+ result = result.replace(/\{\{projectName\}\}/g, config.project.name);
107
+ result = result.replace(/\{\{projectStack\}\}/g, config.project.stack.length > 0 ? config.project.stack.join(", ") : "(none)");
108
+ result = result.replace(/\{\{projectPm\}\}/g, config.project.packageManager);
109
+ result = result.replace(/\{\{rfcsDir\}\}/g, config.paths.rfcsDir);
110
+ result = result.replace(/\{\{adrsDir\}\}/g, config.paths.adrsDir);
111
+ result = result.replace(/\{\{plansDir\}\}/g, config.paths.plansDir);
112
+ result = result.replace(/\{\{auditsDir\}\}/g, config.paths.auditsDir);
113
+ result = result.replace(/\{\{specsDir\}\}/g, config.paths.specsDir);
114
+ result = result.replace(/\{\{skillsDir\}\}/g, config.paths.skillsDir);
115
+ result = result.replace(/\{\{dynamicSections\}\}/g, dynamicSections);
116
+ return result;
117
+ }
118
+
119
+ function readRegister(workspaceRoot: string): BehavioralRegister {
120
+ const prefsPath = path.join(workspaceRoot, "PREFERENCES.md");
121
+ try {
122
+ const content = fs.readFileSync(prefsPath, "utf8");
123
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
124
+ if (fmMatch) {
125
+ const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
126
+ if (fm["register"] === "creative") return "creative";
127
+ }
128
+ } catch {
129
+ // PREFERENCES.md missing or unreadable — default to business
130
+ }
131
+ return "business";
132
+ }
133
+
134
+ function extractTriggers(workspaceRoot: string): Array<{ name: string; triggers: string[] }> {
135
+ const forgeRoot = path.join(workspaceRoot, "packages", "forge");
136
+ const result: Array<{ name: string; triggers: string[] }> = [];
137
+ for (const skill of FORGE_SKILLS) {
138
+ if (skill.category !== "fo") continue;
139
+ const skillPath = path.join(forgeRoot, skill.path);
140
+ try {
141
+ const content = fs.readFileSync(skillPath, "utf8");
142
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
143
+ if (!fmMatch) continue;
144
+ const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
145
+ if (Array.isArray(fm["triggers"])) {
146
+ result.push({
147
+ name: skill.name,
148
+ triggers: (fm["triggers"] as unknown[]).filter((v): v is string => typeof v === "string"),
149
+ });
150
+ }
151
+ } catch {
152
+ // SKILL.md not found or unreadable — skip
153
+ }
154
+ }
155
+ return result;
156
+ }
157
+
158
+ function generateTriggersTable(triggers: Array<{ name: string; triggers: string[] }>): string {
159
+ const rows: string[] = [];
160
+ for (const { name, triggers: skillTriggers } of triggers) {
161
+ if (skillTriggers.length === 0) continue;
162
+ const display = skillTriggers.map((t) => `"${t}"`).join(", ");
163
+ rows.push(`| ${display} | \`${name}\` |`);
164
+ }
165
+ return rows.length > 0 ? rows.join("\n") + "\n" : "";
166
+ }
167
+
168
+ function generateBehavioralLayer(
169
+ workspaceRoot: string,
170
+ register: BehavioralRegister,
171
+ ): string {
172
+ const triggers = extractTriggers(workspaceRoot);
173
+
174
+ let template: string;
175
+ try {
176
+ template = fs.readFileSync(BEHAVIORAL_LAYER_CORE_TEMPLATE, "utf8").trimEnd();
177
+ } catch {
178
+ return "";
179
+ }
180
+
181
+ const triggersTable = generateTriggersTable(triggers);
182
+
183
+ let extendedLayer = "";
184
+ if (register === "creative") {
185
+ try {
186
+ extendedLayer = fs.readFileSync(BEHAVIORAL_LAYER_EXTENDED_TEMPLATE, "utf8").trimEnd() + "\n\n";
187
+ } catch {
188
+ // Extended template missing — skip
189
+ }
190
+ }
191
+
192
+ const result = template
193
+ .replace(/\{\{triggersTable\}\}/g, triggersTable)
194
+ .replace(/\{\{register\}\}/g, register)
195
+ .replace(/\{\{extendedLayer\}\}/g, extendedLayer);
196
+
197
+ return result;
198
+ }
199
+
200
+ interface AgentsGenerateResult {
201
+ command: string;
202
+ status: "pass" | "fail";
203
+ configPath: string;
204
+ generated: string[];
205
+ skipped: string[];
206
+ errors: string[];
207
+ renderedFiles?: { [relPath: string]: string };
208
+ details?: Array<{ path: string; domain?: string; register?: string; workspaceType?: string }>;
209
+ }
210
+
211
+ export async function runAgentsGenerate(
212
+ _input: ForgeCommandInput,
213
+ context: ForgeRuntimeContext,
214
+ ): Promise<ForgeCommandResult<AgentsGenerateResult>> {
215
+ const { workspaceRoot, logger, outputFormat, dryRun } = context;
216
+
217
+ let config;
218
+ try {
219
+ config = loadForgeConfig(workspaceRoot, context.forgeRoot);
220
+ } catch (err) {
221
+ const msg = (err as Error).message;
222
+ if (outputFormat === "pretty") {
223
+ logger.error(msg);
224
+ }
225
+ return {
226
+ data: {
227
+ command: "forge.agents.generate",
228
+ status: "fail",
229
+ configPath: "forge.yaml",
230
+ generated: [],
231
+ skipped: [],
232
+ errors: [msg],
233
+ },
234
+ exitCode: 1,
235
+ summary: `forge.agents.generate: failed — ${msg}`,
236
+ };
237
+ }
238
+
239
+ const agentsMdPath = path.join(workspaceRoot, "AGENTS.md");
240
+
241
+ // Edit guard: refuse to overwrite a hand-written AGENTS.md (skipped in dryRun)
242
+ if (!dryRun && fs.existsSync(agentsMdPath)) {
243
+ const existing = fs.readFileSync(agentsMdPath, "utf8");
244
+ if (!hasGeneratedMarker(existing)) {
245
+ const msg = `AGENTS.md exists without a generated marker — refusing to overwrite a hand-written file. Delete or rename it first, then re-run forge.agents.generate.`;
246
+ if (outputFormat === "pretty") {
247
+ logger.error(msg);
248
+ }
249
+ return {
250
+ data: {
251
+ command: "forge.agents.generate",
252
+ status: "fail",
253
+ configPath: "forge.yaml",
254
+ generated: [],
255
+ skipped: [],
256
+ errors: [msg],
257
+ },
258
+ exitCode: 1,
259
+ summary: `forge.agents.generate: failed — hand-written AGENTS.md`,
260
+ };
261
+ }
262
+ }
263
+
264
+ // Generate AGENTS.md content
265
+ const header = buildGeneratedHeader({
266
+ filePath: "AGENTS.md",
267
+ ownerCommand: "forge.agents.generate",
268
+ commandPrefix: "forge",
269
+ });
270
+
271
+ // RFC-0643: determine register from PREFERENCES.md or profile
272
+ const register = readRegister(workspaceRoot);
273
+
274
+ // Build dynamic sections (skills table, capabilities, behavioral layer)
275
+ const dynamicLines: string[] = [];
276
+
277
+ // Skills table
278
+ dynamicLines.push("## Skills");
279
+ dynamicLines.push("");
280
+ dynamicLines.push("| Name | Category | Invocation | Concerns |");
281
+ dynamicLines.push("| --- | --- | --- | --- |");
282
+ for (const skill of FORGE_SKILLS) {
283
+ dynamicLines.push(`| ${skill.name} | ${skill.category} | ${skill.invocation} | ${skill.concerns} |`);
284
+ }
285
+ dynamicLines.push("");
286
+
287
+ // Capabilities section (RFC-0393)
288
+ if (config.bindings) {
289
+ dynamicLines.push("## Capabilities");
290
+ dynamicLines.push("");
291
+ dynamicLines.push("Bindings resolved from `forge.yaml`:");
292
+ dynamicLines.push("");
293
+ dynamicLines.push("| Key | Status | Value |");
294
+ dynamicLines.push("| --- | --- | --- |");
295
+
296
+ const bindingKeys = [
297
+ "commands.validateRfc",
298
+ "commands.validateAdr",
299
+ "commands.typecheck",
300
+ "commands.test",
301
+ "commands.scopedBuild",
302
+ "commands.specValidate",
303
+ "commands.sessionSave",
304
+ "paths.invariantsFile",
305
+ "paths.reviewsDir",
306
+ "paths.handoffsDir",
307
+ "paths.sessionsDir",
308
+ ];
309
+
310
+ for (const key of bindingKeys) {
311
+ const value = resolveBinding(config, key);
312
+ const status = value === null ? "absent" : "resolved";
313
+ const display = value === null ? "—" : typeof value === "string" ? `\`${value}\`` : String(value);
314
+ dynamicLines.push(`| ${key} | ${status} | ${display} |`);
315
+ }
316
+
317
+ // compassDocs array
318
+ const compassDocs = resolveBinding(config, "paths.compassDocs");
319
+ if (Array.isArray(compassDocs) && compassDocs.length > 0) {
320
+ dynamicLines.push(`| paths.compassDocs | resolved | ${compassDocs.map((d) => `\`${d}\``).join(", ")} |`);
321
+ } else {
322
+ dynamicLines.push(`| paths.compassDocs | absent | — |`);
323
+ }
324
+
325
+ // terminology
326
+ if (config.bindings.terminology && Object.keys(config.bindings.terminology).length > 0) {
327
+ dynamicLines.push("");
328
+ dynamicLines.push("**Terminology:**");
329
+ dynamicLines.push("");
330
+ for (const [term, value] of Object.entries(config.bindings.terminology)) {
331
+ dynamicLines.push(`- ${term}: ${value}`);
332
+ }
333
+ }
334
+
335
+ dynamicLines.push("");
336
+ }
337
+
338
+ // Behavioral layer section (RFC-0548)
339
+ const behavioralLayer = generateBehavioralLayer(workspaceRoot, register);
340
+ dynamicLines.push(behavioralLayer);
341
+ dynamicLines.push("");
342
+
343
+ // RFC-0664: Project memory layer read discipline
344
+ dynamicLines.push("## Project memory layer");
345
+ dynamicLines.push("");
346
+ dynamicLines.push("`.agents/memory/MEMORY.md` is the curated hot store (versioned). `.agents/memory/daily/YYYY-MM-DD.md` are append-only warm logs (git-ignored).");
347
+ dynamicLines.push("");
348
+ dynamicLines.push("**Session-start read discipline (advisory):** At session start, read `MEMORY.md` (always), then `daily/<today>.md` and `daily/<yesterday>.md` (if present). Older daily files are cold — use grep when a task references past context.");
349
+ dynamicLines.push("");
350
+
351
+ // RFC-0643: Load root template, replace project placeholders, insert dynamic sections
352
+ const profile = config.profile as StackProfile | undefined;
353
+ const rootTemplate = selectRootTemplate(register, profile);
354
+ const dynamicSections = dynamicLines.join("\n");
355
+ let content = replaceProjectPlaceholders(rootTemplate, config, dynamicSections);
356
+
357
+ // Prepend generated header (above template content)
358
+ content = header + "\n\n" + content;
359
+
360
+ // RFC-0643: Apply terminology substitution on final assembled content
361
+ const resolvedTerminology = resolveAllTerminology(config, profile);
362
+ content = substituteTemplate(content, resolvedTerminology);
363
+
364
+ const generated: string[] = ["AGENTS.md"];
365
+ const skipped: string[] = [];
366
+ const renderedFiles: { [relPath: string]: string } = {};
367
+ const details: Array<{ path: string; domain?: string; register?: string; workspaceType?: string }> = [];
368
+ details.push({
369
+ path: "AGENTS.md",
370
+ domain: profile?.domain,
371
+ register,
372
+ });
373
+
374
+ if (dryRun) {
375
+ renderedFiles["AGENTS.md"] = content;
376
+ } else {
377
+ await writeFileIfChanged(agentsMdPath, content);
378
+ if (outputFormat === "pretty") {
379
+ logger.success(`Generated ${path.relative(workspaceRoot, agentsMdPath)}`);
380
+ }
381
+ }
382
+
383
+ // Nested AGENTS.md generation (RFC-0611)
384
+ // RFC-0640: load workspaceTypes from stack profile for profile-driven detection
385
+ // RFC-0643: prefer config.profile (loaded by loadForgeConfig), fallback to stack-based lookup
386
+ let workspaceTypes: ProfileWorkspaceType[] | undefined;
387
+ if (profile?.workspaceTypes && profile.workspaceTypes.length > 0) {
388
+ workspaceTypes = profile.workspaceTypes;
389
+ } else {
390
+ try {
391
+ const forgeRoot = resolveForgeRoot(workspaceRoot);
392
+ const profiles = listStackProfiles(forgeRoot);
393
+ for (const stackId of config.project.stack) {
394
+ const stackProfile = profiles.find((p) => p.id === stackId);
395
+ if (stackProfile?.workspaceTypes && stackProfile.workspaceTypes.length > 0) {
396
+ workspaceTypes = stackProfile.workspaceTypes;
397
+ break;
398
+ }
399
+ }
400
+ } catch {
401
+ // forge root not resolvable — fallback to hardcoded detection
402
+ }
403
+ }
404
+ const nestedResult = await generateNestedAgentsMd(workspaceRoot, config, dryRun, workspaceTypes);
405
+ generated.push(...nestedResult.generated);
406
+ skipped.push(...nestedResult.skipped);
407
+ Object.assign(renderedFiles, nestedResult.renderedFiles);
408
+
409
+ // RFC-0643: add details for nested files
410
+ for (const rel of nestedResult.generated) {
411
+ details.push({
412
+ path: rel,
413
+ domain: profile?.domain,
414
+ workspaceType: nestedResult.workspaceTypeMap?.[rel],
415
+ });
416
+ }
417
+
418
+ if (!dryRun && outputFormat === "pretty") {
419
+ for (const rel of nestedResult.generated) {
420
+ logger.success(`Generated ${rel}`);
421
+ }
422
+ for (const rel of nestedResult.skipped) {
423
+ logger.warn(`Skipped ${rel}`);
424
+ }
425
+ }
426
+
427
+ return {
428
+ data: {
429
+ command: "forge.agents.generate",
430
+ status: "pass",
431
+ configPath: "forge.yaml",
432
+ generated,
433
+ skipped,
434
+ errors: [],
435
+ details,
436
+ ...(dryRun ? { renderedFiles } : {}),
437
+ },
438
+ exitCode: 0,
439
+ summary: dryRun
440
+ ? `forge.agents.generate: [dry-run] would generate ${generated.length} file(s), skip ${skipped.length}`
441
+ : `forge.agents.generate: OK — ${generated.length} file(s) generated, ${skipped.length} skipped`,
442
+ };
443
+ }