@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,93 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Shared nested AGENTS.md generation logic (RFC-0611). Discovery + package.json
4
+ metadata extraction + edit guard + write (or render-only in dryRun). Reused by
5
+ runAgentsGenerate, runUpgrade, and runDoctor (staleness check via dryRun).</purpose>
6
+ <non-goals>
7
+ <item>Do not generate AGENTS.md for non-workspace directories (no package.json).</item>
8
+ <item>Do not overwrite hand-written AGENTS.md files (no generated marker).</item>
9
+ <item>Do not add workspace-type detection rules — those live in workspace-discovery.ts.</item>
10
+ </non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY>
13
+ <item>RFC-0611: initial shared nested generation function with dryRun support.</item>
14
+ <item>Enriched template: read package.json metadata and pass to buildNestedAgentsMd for content-rich output.</item>
15
+ <item>RFC-0640: accept optional workspaceTypes from profile and pass to discoverWorkspaces for profile-driven detection.</item>
16
+ <item>RFC-0643: return workspaceTypeMap for per-file workspace type metadata in details field.</item>
17
+ <item>RFC-0643: use selectNestedTemplate for profile-driven nested templates with terminology substitution.</item>
18
+ </CHANGE_SUMMARY>
19
+ */
20
+
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import { writeFileIfChanged } from "../utils/index.ts";
24
+ import { discoverWorkspaces } from "./workspace-discovery.ts";
25
+ import { buildNestedAgentsMd, selectNestedTemplate, type PackageInfo } from "./nested-agents-templates.ts";
26
+ import type { ForgeConfig } from "../config/forge-config.ts";
27
+ import type { ProfileWorkspaceType } from "../profiles/profile-schema.ts";
28
+ import type { StackProfile } from "../profiles/stack-profile.ts";
29
+ import { resolveAllTerminology } from "../profiles/terminology-utils.ts";
30
+
31
+ export interface NestedGenerateResult {
32
+ generated: string[];
33
+ skipped: string[];
34
+ renderedFiles: { [relPath: string]: string };
35
+ workspaceTypeMap?: { [relPath: string]: string };
36
+ }
37
+
38
+ export function readPackageInfo(workspaceRoot: string, wsPath: string): PackageInfo | undefined {
39
+ const pkgJsonPath = path.join(workspaceRoot, wsPath, "package.json");
40
+ try {
41
+ const raw = fs.readFileSync(pkgJsonPath, "utf8");
42
+ return JSON.parse(raw) as PackageInfo;
43
+ } catch {
44
+ return undefined;
45
+ }
46
+ }
47
+
48
+ export async function generateNestedAgentsMd(
49
+ workspaceRoot: string,
50
+ config: ForgeConfig,
51
+ dryRun: boolean,
52
+ workspaceTypes?: ProfileWorkspaceType[],
53
+ ): Promise<NestedGenerateResult> {
54
+ const workspaces = discoverWorkspaces(workspaceRoot, workspaceTypes);
55
+ const generated: string[] = [];
56
+ const skipped: string[] = [];
57
+ const renderedFiles: { [relPath: string]: string } = {};
58
+ const workspaceTypeMap: { [relPath: string]: string } = {};
59
+
60
+ // RFC-0643: resolve terminology from config + profile for nested template substitution
61
+ const profile = config.profile as StackProfile | undefined;
62
+ const terminology = resolveAllTerminology(config, profile);
63
+
64
+ for (const ws of workspaces) {
65
+ const agentsMdPath = path.join(workspaceRoot, ws.path, "AGENTS.md");
66
+ const packageInfo = readPackageInfo(workspaceRoot, ws.path);
67
+ const fallback = buildNestedAgentsMd(ws, config, packageInfo);
68
+
69
+ // RFC-0643: use profile-driven template when available
70
+ const wsType = workspaceTypes?.find((wt) => wt.id === ws.type);
71
+ const content = selectNestedTemplate(wsType, profile, terminology, fallback);
72
+ const relPath = path.join(ws.path, "AGENTS.md");
73
+
74
+ if (dryRun) {
75
+ renderedFiles[relPath] = content;
76
+ continue;
77
+ }
78
+
79
+ if (ws.hasAgentsMd && !ws.isGenerated) {
80
+ skipped.push(`${relPath} (hand-written)`);
81
+ continue;
82
+ }
83
+
84
+ await writeFileIfChanged(agentsMdPath, content);
85
+ generated.push(relPath);
86
+ workspaceTypeMap[relPath] = ws.type;
87
+ }
88
+
89
+ return { generated, skipped, renderedFiles, workspaceTypeMap };
90
+ }
91
+
92
+ export { discoverWorkspaces, type WorkspaceDir, type WorkspaceType } from "./workspace-discovery.ts";
93
+ export { buildNestedAgentsMd, selectNestedTemplate, type PackageInfo } from "./nested-agents-templates.ts";
@@ -0,0 +1,233 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Nested AGENTS.md template builder (RFC-0611). Pure function that renders
4
+ content-rich AGENTS.md for a workspace from its package.json metadata. No I/O.</purpose>
5
+ <non-goals>
6
+ <item>Do not read or write files — this is a pure render function.</item>
7
+ <item>Do not invent metadata not present in package.json — omit sections when data is absent.</item>
8
+ </non-goals>
9
+ </MODULE_CONTRACT>
10
+ <CHANGE_SUMMARY>
11
+ <item>RFC-0611: initial minimal stub template for app, package, service workspaces.</item>
12
+ <item>Enriched template: render package name, description, entry points, scripts, and dependencies from package.json metadata instead of a minimal stub.</item>
13
+ <item>RFC-0643: selectNestedTemplate() for profile-driven template selection with path traversal guard.</item>
14
+ </CHANGE_SUMMARY>
15
+ */
16
+
17
+ import { buildGeneratedHeader } from "../utils/index.ts";
18
+ import type { ForgeConfig } from "../config/forge-config.ts";
19
+ import type { WorkspaceDir, WorkspaceType } from "./workspace-discovery.ts";
20
+ import type { ProfileWorkspaceType } from "../profiles/profile-schema.ts";
21
+ import type { StackProfile } from "../profiles/stack-profile.ts";
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+
25
+ export interface PackageInfo {
26
+ name?: string;
27
+ description?: string;
28
+ exports?: Record<string, string | { types?: string; default?: string } | undefined>;
29
+ main?: string;
30
+ types?: string;
31
+ scripts?: Record<string, string>;
32
+ dependencies?: Record<string, string>;
33
+ devDependencies?: Record<string, string>;
34
+ }
35
+
36
+ const TYPE_GUIDANCE: Record<WorkspaceType, string> = {
37
+ app: "This is an **app** workspace. Follow thin-route and content-driven composition rules from the root AGENTS.md.",
38
+ package:
39
+ "This is a **package** workspace. Expose stable typed APIs. Do not import from apps or services.",
40
+ service:
41
+ "This is a **service** workspace. Runtime composition only. Shared schemas and validators belong in packages.",
42
+ };
43
+
44
+ const TYPE_LABEL: Record<WorkspaceType, string> = {
45
+ app: "App",
46
+ package: "Package",
47
+ service: "Service",
48
+ };
49
+
50
+ interface EntryPoint {
51
+ name: string;
52
+ module: string;
53
+ }
54
+
55
+ function extractEntryPoints(
56
+ exports: PackageInfo["exports"],
57
+ main?: string,
58
+ ): EntryPoint[] {
59
+ if (!exports || typeof exports !== "object") {
60
+ if (main) return [{ name: ".", module: main }];
61
+ return [];
62
+ }
63
+ const result: EntryPoint[] = [];
64
+ for (const [key, value] of Object.entries(exports)) {
65
+ if (typeof value === "string") {
66
+ result.push({ name: key, module: value });
67
+ } else if (value && typeof value === "object") {
68
+ const module = value.types ?? value.default;
69
+ if (module) result.push({ name: key, module });
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+
75
+ function formatDependencies(
76
+ deps: Record<string, string> | undefined,
77
+ ): { workspace: string[]; external: string[] } {
78
+ if (!deps) return { workspace: [], external: [] };
79
+ const workspace: string[] = [];
80
+ const external: string[] = [];
81
+ for (const [name, version] of Object.entries(deps)) {
82
+ if (version.startsWith("workspace:")) {
83
+ workspace.push(`\`${name}\``);
84
+ } else {
85
+ external.push(`\`${name}\` \`${version}\``);
86
+ }
87
+ }
88
+ return { workspace, external };
89
+ }
90
+
91
+ function buildScriptsTable(scripts: Record<string, string> | undefined): string[] {
92
+ if (!scripts || Object.keys(scripts).length === 0) return [];
93
+ const lines: string[] = [];
94
+ lines.push("## Scripts");
95
+ lines.push("");
96
+ lines.push("| Script | Command |");
97
+ lines.push("| --- | --- |");
98
+ for (const [name, cmd] of Object.entries(scripts)) {
99
+ lines.push(`| \`${name}\` | \`${cmd}\` |`);
100
+ }
101
+ lines.push("");
102
+ return lines;
103
+ }
104
+
105
+ function buildDependenciesSection(deps: Record<string, string> | undefined): string[] {
106
+ const { workspace, external } = formatDependencies(deps);
107
+ if (workspace.length === 0 && external.length === 0) return [];
108
+ const lines: string[] = [];
109
+ lines.push("## Dependencies");
110
+ lines.push("");
111
+ if (workspace.length > 0) {
112
+ lines.push("**Workspace:**");
113
+ lines.push("");
114
+ for (const dep of workspace) lines.push(`- ${dep}`);
115
+ lines.push("");
116
+ }
117
+ if (external.length > 0) {
118
+ lines.push("**External:**");
119
+ lines.push("");
120
+ for (const dep of external) lines.push(`- ${dep}`);
121
+ lines.push("");
122
+ }
123
+ return lines;
124
+ }
125
+
126
+ /**
127
+ * RFC-0643: Select nested AGENTS.md template from profile workspace type.
128
+ * When agentsMdTemplate is present, resolve relative to profile directory.
129
+ * Reject absolute paths and parent-directory traversal (..) — fall back to hardcoded.
130
+ * Apply terminology substitution to the template content.
131
+ */
132
+ export function selectNestedTemplate(
133
+ workspaceType: ProfileWorkspaceType | undefined,
134
+ profile: StackProfile | undefined,
135
+ terminology: Record<string, string>,
136
+ fallback: string,
137
+ ): string {
138
+ if (!workspaceType?.agentsMdTemplate || !profile) {
139
+ return fallback;
140
+ }
141
+
142
+ const templateRel = workspaceType.agentsMdTemplate;
143
+
144
+ // Path traversal guard: reject absolute paths and ..
145
+ if (path.isAbsolute(templateRel) || templateRel.includes("..")) {
146
+ return fallback;
147
+ }
148
+
149
+ // Resolve relative to profiles/ directory (where profile YAMLs live)
150
+ const forgeRoot = path.resolve(import.meta.dirname, "..", "..");
151
+ const profilesDir = path.join(forgeRoot, "profiles");
152
+ const templatePath = path.resolve(profilesDir, templateRel);
153
+
154
+ // Ensure resolved path is within profiles directory (no traversal escape)
155
+ if (!templatePath.startsWith(profilesDir + path.sep) && templatePath !== profilesDir) {
156
+ return fallback;
157
+ }
158
+
159
+ try {
160
+ const content = fs.readFileSync(templatePath, "utf8");
161
+ return substituteNestedTemplate(content, terminology);
162
+ } catch {
163
+ // Template file not found — fall back
164
+ return fallback;
165
+ }
166
+ }
167
+
168
+ function substituteNestedTemplate(
169
+ content: string,
170
+ terminology: Record<string, string>,
171
+ ): string {
172
+ return content.replace(
173
+ /\{\{terminology\.(\w+)\}\}/g,
174
+ (_, key: string) => terminology[key] ?? key,
175
+ );
176
+ }
177
+
178
+ export function buildNestedAgentsMd(
179
+ workspace: WorkspaceDir,
180
+ _config: ForgeConfig,
181
+ packageInfo?: PackageInfo,
182
+ ): string {
183
+ const header = buildGeneratedHeader({
184
+ filePath: "AGENTS.md",
185
+ ownerCommand: "forge.agents.generate",
186
+ commandPrefix: "forge",
187
+ });
188
+
189
+ const lines: string[] = [];
190
+ lines.push(header);
191
+ lines.push("");
192
+
193
+ const displayName = packageInfo?.name ?? workspace.path;
194
+ lines.push(`# \`${displayName}\` — Agent Guide`);
195
+ lines.push("");
196
+
197
+ if (packageInfo?.description) {
198
+ lines.push(packageInfo.description);
199
+ lines.push("");
200
+ }
201
+
202
+ lines.push(`> This file is generated by \`forge.agents.generate\`.`);
203
+ lines.push(`> Do not edit by hand — re-run \`forge agents generate\` to regenerate.`);
204
+ lines.push("");
205
+
206
+ lines.push(`**Workspace type:** ${TYPE_LABEL[workspace.type]}`);
207
+ lines.push("");
208
+ lines.push(TYPE_GUIDANCE[workspace.type]);
209
+ lines.push("");
210
+
211
+ if (workspace.type === "package") {
212
+ const entryPoints = extractEntryPoints(packageInfo?.exports, packageInfo?.main);
213
+ if (entryPoints.length > 0) {
214
+ lines.push("## Entry points");
215
+ lines.push("");
216
+ lines.push("| Entry point | Module |");
217
+ lines.push("| --- | --- |");
218
+ for (const ep of entryPoints) {
219
+ const suffix = ep.name === "." ? "" : ep.name.replace(/^\.\//, "/");
220
+ lines.push(`| \`${displayName}${suffix}\` | \`${ep.module}\` |`);
221
+ }
222
+ lines.push("");
223
+ }
224
+ }
225
+
226
+ lines.push(...buildScriptsTable(packageInfo?.scripts));
227
+ lines.push(...buildDependenciesSection(packageInfo?.dependencies));
228
+
229
+ lines.push("See the root `AGENTS.md` for project-wide rules, skills, and capabilities.");
230
+ lines.push("");
231
+
232
+ return lines.join("\n");
233
+ }
@@ -0,0 +1,139 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>forge.profile.validate — validates profile YAML files under packages/forge/profiles/ against the extended stack-profile schema (RFC-0638). Supports --id flag for single-profile validation.</purpose>
4
+ <non-goals>
5
+ <item>Do not import from @warpgogol/* — this module is portable.</item>
6
+ <item>Do not modify profile files — validation is read-only.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0640: initial forge.profile.validate handler.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { parse as parseYaml } from "yaml";
17
+ import { stackProfileSchema } from "../profiles/stack-profile.ts";
18
+ import type {
19
+ ForgeCommandInput,
20
+ ForgeCommandResult,
21
+ ForgeRuntimeContext,
22
+ } from "../types.ts";
23
+
24
+ export interface ProfileValidateResult {
25
+ command: "forge.profile.validate";
26
+ valid: boolean;
27
+ profiles: Array<{
28
+ id: string;
29
+ valid: boolean;
30
+ errors: string[];
31
+ warnings: string[];
32
+ }>;
33
+ }
34
+
35
+ export async function runProfileValidate(
36
+ input: ForgeCommandInput,
37
+ context: ForgeRuntimeContext,
38
+ ): Promise<ForgeCommandResult<ProfileValidateResult>> {
39
+ const { workspaceRoot, logger, outputFormat } = context;
40
+ const filterId = input.flags["id"] as string | undefined;
41
+
42
+ const forgeRoot = context.forgeRoot ?? path.join(workspaceRoot, "packages", "forge");
43
+ const profilesDir = path.join(forgeRoot, "profiles");
44
+
45
+ const profileResults: ProfileValidateResult["profiles"] = [];
46
+
47
+ if (!fs.existsSync(profilesDir)) {
48
+ const result: ProfileValidateResult = {
49
+ command: "forge.profile.validate",
50
+ valid: true,
51
+ profiles: [],
52
+ };
53
+ if (outputFormat === "pretty") {
54
+ logger.warn(`No profiles directory found at ${profilesDir}`);
55
+ }
56
+ return {
57
+ data: result,
58
+ exitCode: 0,
59
+ summary: "forge.profile.validate: no profiles directory — 0 profiles checked",
60
+ };
61
+ }
62
+
63
+ const entries = fs.readdirSync(profilesDir);
64
+ for (const entry of entries) {
65
+ if (!entry.endsWith(".yaml") || entry.startsWith(".")) continue;
66
+
67
+ const profilePath = path.join(profilesDir, entry);
68
+ const raw = fs.readFileSync(profilePath, "utf8");
69
+
70
+ let parsed: unknown;
71
+ let parseError: string | null = null;
72
+ try {
73
+ parsed = parseYaml(raw);
74
+ } catch (err) {
75
+ parseError = (err as Error).message;
76
+ }
77
+
78
+ let profileId = entry.replace(/\.yaml$/, "");
79
+ if (parsed && typeof parsed === "object" && "id" in parsed) {
80
+ profileId = (parsed as { id: string }).id;
81
+ }
82
+
83
+ if (filterId && profileId !== filterId) continue;
84
+
85
+ const errors: string[] = [];
86
+ const warnings: string[] = [];
87
+
88
+ if (parseError) {
89
+ errors.push(`YAML parse error: ${parseError}`);
90
+ } else {
91
+ const result = stackProfileSchema.safeParse(parsed);
92
+ if (!result.success) {
93
+ for (const issue of result.error.issues) {
94
+ errors.push(`${issue.path.join(".")}: ${issue.message}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ profileResults.push({
100
+ id: profileId,
101
+ valid: errors.length === 0,
102
+ errors,
103
+ warnings,
104
+ });
105
+ }
106
+
107
+ const allValid = profileResults.every((p) => p.valid);
108
+
109
+ if (outputFormat === "pretty") {
110
+ logger.section(`Forge Profile Validate — ${profilesDir}`);
111
+ for (const p of profileResults) {
112
+ const icon = p.valid ? "✓" : "✖";
113
+ const fn = p.valid ? logger.success : logger.error;
114
+ fn(`${icon} ${p.id}: ${p.valid ? "valid" : `${p.errors.length} error(s)`}`);
115
+ for (const err of p.errors) {
116
+ logger.error(` - ${err}`);
117
+ }
118
+ }
119
+ if (allValid) {
120
+ logger.success(`All ${profileResults.length} profile(s) valid.`);
121
+ } else {
122
+ logger.error(`${profileResults.filter((p) => !p.valid).length} profile(s) invalid.`);
123
+ }
124
+ }
125
+
126
+ const result: ProfileValidateResult = {
127
+ command: "forge.profile.validate",
128
+ valid: allValid,
129
+ profiles: profileResults,
130
+ };
131
+
132
+ return {
133
+ data: result,
134
+ exitCode: allValid ? 0 : 1,
135
+ summary: allValid
136
+ ? `forge.profile.validate: OK — ${profileResults.length} profile(s) valid`
137
+ : `forge.profile.validate: ${profileResults.filter((p) => !p.valid).length} invalid profile(s)`,
138
+ };
139
+ }