@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,211 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>forge.release.publish — publish a prepared release to the declared target (local, R2, S3). Supports --dry-run, --json, --profile.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement release preparation — that lives in release-prepare.ts.</item>
6
+ <item>Do not import from @warpgogol/* in autonomous modules — os/core/ may import from @warpgogol/*.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0680: initial forge.release.publish handler with local/r2/s3 targets, --dry-run, env var validation.</item>
11
+ <item>RFC-0680 review fix: remove unused readdir import (A-1).</item>
12
+ <item>RFC-0680 review fix: add S3_ENDPOINT to required env vars (G-1).</item>
13
+ <item>RFC-0680 review fix: add 30s request timeout on S3Client (G-2).</item>
14
+ </CHANGE_SUMMARY>
15
+ */
16
+
17
+ import { readFile, copyFile, mkdir } from "node:fs/promises";
18
+ import { join, basename } from "node:path";
19
+ import type {
20
+ ForgeCommandInput,
21
+ ForgeCommandResult,
22
+ ForgeRuntimeContext,
23
+ } from "../../../src/types.ts";
24
+ import { resolveActiveProfile, resolveLifecycleFlags } from "./profile-resolve.ts";
25
+ import type { ReleaseManifest } from "./release-prepare.ts";
26
+
27
+ export interface ForgeReleasePublishResult {
28
+ command: "forge.release.publish";
29
+ profileId: string;
30
+ target: string;
31
+ publishedFiles: Array<{ path: string; targetPath: string }>;
32
+ manifestPath: string;
33
+ }
34
+
35
+ function getRequiredEnvVars(target: string): string[] {
36
+ if (target === "r2") {
37
+ return ["R2_AXIOM_ACCOUNT_ID", "R2_AXIOM_ACCESS_KEY_ID", "R2_AXIOM_SECRET_ACCESS_KEY"];
38
+ }
39
+ if (target === "s3") {
40
+ return ["S3_BUCKET", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY", "S3_ENDPOINT"];
41
+ }
42
+ return [];
43
+ }
44
+
45
+ export async function runReleasePublish(
46
+ input: ForgeCommandInput,
47
+ context: ForgeRuntimeContext,
48
+ ): Promise<ForgeCommandResult<ForgeReleasePublishResult>> {
49
+ const { workspaceRoot, logger } = context;
50
+ const { dryRun, profileIdOverride } = resolveLifecycleFlags(input, context);
51
+
52
+ const resolved = resolveActiveProfile(workspaceRoot, context.forgeRoot, profileIdOverride);
53
+ if (!resolved) {
54
+ return {
55
+ data: {
56
+ command: "forge.release.publish",
57
+ profileId: "",
58
+ target: "",
59
+ publishedFiles: [],
60
+ manifestPath: "",
61
+ },
62
+ exitCode: 1,
63
+ summary: "No active profile found. Set `profile` in forge.yaml or use --profile <id>.",
64
+ nextSteps: [{ action: "Set profile in forge.yaml or use --profile <id>", kind: "required" }],
65
+ };
66
+ }
67
+
68
+ const { profile } = resolved;
69
+
70
+ if (!profile.release) {
71
+ return {
72
+ data: {
73
+ command: "forge.release.publish",
74
+ profileId: profile.id,
75
+ target: "",
76
+ publishedFiles: [],
77
+ manifestPath: "",
78
+ },
79
+ exitCode: 1,
80
+ summary: `Profile ${profile.id} does not declare a release configuration.`,
81
+ };
82
+ }
83
+
84
+ const releaseConfig = profile.release;
85
+ const releaseDir = join(workspaceRoot, releaseConfig.outputDir);
86
+ const manifestPath = join(releaseDir, releaseConfig.manifestName);
87
+
88
+ let manifest: ReleaseManifest;
89
+ try {
90
+ const raw = await readFile(manifestPath, "utf8");
91
+ manifest = JSON.parse(raw) as ReleaseManifest;
92
+ } catch {
93
+ return {
94
+ data: {
95
+ command: "forge.release.publish",
96
+ profileId: profile.id,
97
+ target: releaseConfig.target,
98
+ publishedFiles: [],
99
+ manifestPath,
100
+ },
101
+ exitCode: 1,
102
+ summary: `No release manifest found at ${manifestPath}. Run \`forge release prepare\` first.`,
103
+ };
104
+ }
105
+
106
+ const target = releaseConfig.target;
107
+ const requiredVars = getRequiredEnvVars(target);
108
+ const missingVars = requiredVars.filter((v) => !process.env[v]);
109
+
110
+ if (missingVars.length > 0) {
111
+ return {
112
+ data: {
113
+ command: "forge.release.publish",
114
+ profileId: profile.id,
115
+ target,
116
+ publishedFiles: [],
117
+ manifestPath,
118
+ },
119
+ exitCode: 1,
120
+ summary: `Missing required environment variables: ${missingVars.join(", ")}`,
121
+ };
122
+ }
123
+
124
+ const filesToPublish: string[] = [];
125
+ for (const a of manifest.artifacts) {
126
+ filesToPublish.push(join(releaseDir, basename(a.path)));
127
+ }
128
+ filesToPublish.push(manifestPath);
129
+
130
+ if (dryRun) {
131
+ logger.info(`[dry-run] forge.release.publish — profile: ${profile.id}`);
132
+ logger.info(` target: ${target}`);
133
+ for (const f of filesToPublish) {
134
+ logger.info(` file: ${f}`);
135
+ }
136
+ return {
137
+ data: {
138
+ command: "forge.release.publish",
139
+ profileId: profile.id,
140
+ target,
141
+ publishedFiles: filesToPublish.map((f) => ({
142
+ path: f,
143
+ targetPath:
144
+ target === "local"
145
+ ? join(releaseDir, "published", basename(f))
146
+ : `${target}://${basename(f)}`,
147
+ })),
148
+ manifestPath,
149
+ },
150
+ summary: `[dry-run] Would publish ${filesToPublish.length} file(s) to ${target}`,
151
+ };
152
+ }
153
+
154
+ const publishedFiles: Array<{ path: string; targetPath: string }> = [];
155
+
156
+ if (target === "local") {
157
+ const publishedDir = join(releaseDir, "published");
158
+ await mkdir(publishedDir, { recursive: true });
159
+ for (const f of filesToPublish) {
160
+ const dst = join(publishedDir, basename(f));
161
+ await copyFile(f, dst);
162
+ publishedFiles.push({ path: f, targetPath: dst });
163
+ }
164
+ } else if (target === "r2" || target === "s3") {
165
+ const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
166
+ const { readFile: fsReadFile } = await import("node:fs/promises");
167
+
168
+ const endpoint =
169
+ target === "r2"
170
+ ? `https://${process.env.R2_AXIOM_ACCOUNT_ID}.r2.cloudflarestorage.com`
171
+ : process.env.S3_ENDPOINT;
172
+ const bucket = target === "r2" ? releaseConfig.r2?.bucket : process.env.S3_BUCKET;
173
+ const accessKeyId =
174
+ target === "r2" ? process.env.R2_AXIOM_ACCESS_KEY_ID : process.env.S3_ACCESS_KEY_ID;
175
+ const secretAccessKey =
176
+ target === "r2" ? process.env.R2_AXIOM_SECRET_ACCESS_KEY : process.env.S3_SECRET_ACCESS_KEY;
177
+ const prefix = target === "r2" ? (releaseConfig.r2?.prefix ?? "") : "";
178
+
179
+ const client = new S3Client({
180
+ endpoint,
181
+ region: "auto",
182
+ credentials: { accessKeyId: accessKeyId!, secretAccessKey: secretAccessKey! },
183
+ requestHandler: { requestTimeout: 30_000 },
184
+ });
185
+
186
+ for (const f of filesToPublish) {
187
+ const key = prefix ? `${prefix}/${basename(f)}` : basename(f);
188
+ const body = await fsReadFile(f);
189
+ await client.send(
190
+ new PutObjectCommand({
191
+ Bucket: bucket,
192
+ Key: key,
193
+ Body: body,
194
+ }),
195
+ );
196
+ publishedFiles.push({ path: f, targetPath: `${target}://${bucket}/${key}` });
197
+ }
198
+ }
199
+
200
+ logger.success(` Published ${publishedFiles.length} file(s) to ${target}`);
201
+ return {
202
+ data: {
203
+ command: "forge.release.publish",
204
+ profileId: profile.id,
205
+ target,
206
+ publishedFiles,
207
+ manifestPath,
208
+ },
209
+ summary: `Published ${publishedFiles.length} file(s) to ${target}`,
210
+ };
211
+ }
@@ -0,0 +1,262 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>forge.validate — execute validate commands for all artifacts declared in the active stack profile. Supports --dry-run, --json, --artifact filtering, and violation parsing.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement dev or build logic — those are separate handlers.</item>
6
+ <item>Do not import from @warpgogol/* — this module is portable.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0674: initial forge.validate handler with profile resolution, --dry-run, and per-artifact execution.</item>
11
+ <item>RFC-0677: extended with --artifact filtering, violation parsing (outputFormat: json/plain), passed/allPassed fields.</item>
12
+ </CHANGE_SUMMARY>
13
+ */
14
+
15
+ import { exec } from "node:child_process";
16
+ import { promisify } from "node:util";
17
+ import type {
18
+ ForgeCommandInput,
19
+ ForgeCommandResult,
20
+ ForgeRuntimeContext,
21
+ } from "../../../src/types.ts";
22
+ import { resolveActiveProfile, resolveLifecycleFlags } from "./profile-resolve.ts";
23
+
24
+ const execAsync = promisify(exec);
25
+
26
+ export interface ValidateViolation {
27
+ file: string;
28
+ line?: number;
29
+ column?: number;
30
+ severity: "error" | "warning";
31
+ message: string;
32
+ }
33
+
34
+ export interface ForgeValidateArtifactResult {
35
+ id: string;
36
+ command: string;
37
+ exitCode: number;
38
+ passed: boolean;
39
+ violations: ValidateViolation[];
40
+ stdout: string;
41
+ stderr: string;
42
+ }
43
+
44
+ export interface ForgeValidateResult {
45
+ command: "forge.validate";
46
+ profileId: string;
47
+ artifacts: ForgeValidateArtifactResult[];
48
+ allPassed: boolean;
49
+ }
50
+
51
+ export function parseViolations(
52
+ output: string,
53
+ outputFormat: "plain" | "json" | undefined,
54
+ violationPattern: string | undefined,
55
+ ): ValidateViolation[] {
56
+ if (outputFormat === "json") {
57
+ try {
58
+ const parsed = JSON.parse(output) as unknown;
59
+ if (Array.isArray(parsed)) {
60
+ return parsed
61
+ .filter((v): v is Record<string, unknown> => typeof v === "object" && v !== null)
62
+ .map((v) => ({
63
+ file: String(v["file"] ?? ""),
64
+ line: typeof v["line"] === "number" ? v["line"] : undefined,
65
+ column: typeof v["column"] === "number" ? v["column"] : undefined,
66
+ severity: (v["severity"] === "warning" ? "warning" : "error") as "error" | "warning",
67
+ message: String(v["message"] ?? ""),
68
+ }))
69
+ .filter((v) => v.file || v.message);
70
+ }
71
+ return [];
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ if (outputFormat === "plain" && violationPattern) {
78
+ try {
79
+ const regex = new RegExp(violationPattern, "gm");
80
+ const violations: ValidateViolation[] = [];
81
+ let match: RegExpExecArray | null;
82
+ const MAX_MATCHES = 1000;
83
+ let matchCount = 0;
84
+ while ((match = regex.exec(output)) !== null && matchCount < MAX_MATCHES) {
85
+ matchCount++;
86
+ const groups = match.groups ?? {};
87
+ violations.push({
88
+ file: String(groups["file"] ?? ""),
89
+ line: groups["line"] ? Number(groups["line"]) : undefined,
90
+ column: groups["column"] ? Number(groups["column"]) : undefined,
91
+ severity: (groups["severity"] === "warning" ? "warning" : "error") as "error" | "warning",
92
+ message: String(groups["message"] ?? match[0]),
93
+ });
94
+ if (regex.lastIndex === match.index) regex.lastIndex++;
95
+ }
96
+ return violations;
97
+ } catch {
98
+ return [];
99
+ }
100
+ }
101
+
102
+ return [];
103
+ }
104
+
105
+ export async function runValidate(
106
+ input: ForgeCommandInput,
107
+ context: ForgeRuntimeContext,
108
+ ): Promise<ForgeCommandResult<ForgeValidateResult>> {
109
+ const { workspaceRoot, logger } = context;
110
+ const { dryRun, profileIdOverride } = resolveLifecycleFlags(input, context);
111
+ const artifactFilter =
112
+ typeof input.flags["artifact"] === "string" ? (input.flags["artifact"] as string) : undefined;
113
+
114
+ const resolved = resolveActiveProfile(workspaceRoot, context.forgeRoot, profileIdOverride);
115
+ if (!resolved) {
116
+ return {
117
+ data: {
118
+ command: "forge.validate",
119
+ profileId: "",
120
+ artifacts: [],
121
+ allPassed: false,
122
+ },
123
+ exitCode: 1,
124
+ summary: "No active profile found. Set `profile` in forge.yaml or use --profile <id>.",
125
+ nextSteps: [{ action: "Set profile in forge.yaml or use --profile <id>", kind: "required" }],
126
+ };
127
+ }
128
+
129
+ const { profile } = resolved;
130
+
131
+ if (!profile.artifacts || profile.artifacts.length === 0) {
132
+ return {
133
+ data: {
134
+ command: "forge.validate",
135
+ profileId: profile.id,
136
+ artifacts: [],
137
+ allPassed: false,
138
+ },
139
+ exitCode: 1,
140
+ summary: `Profile ${profile.id} does not declare any artifacts.`,
141
+ nextSteps: [{ action: `Add artifacts section to profile ${profile.id}`, kind: "required" }],
142
+ };
143
+ }
144
+
145
+ if (artifactFilter) {
146
+ const found = profile.artifacts.find((a) => a.id === artifactFilter);
147
+ if (!found) {
148
+ return {
149
+ data: {
150
+ command: "forge.validate",
151
+ profileId: profile.id,
152
+ artifacts: [],
153
+ allPassed: false,
154
+ },
155
+ exitCode: 1,
156
+ summary: `Artifact ${artifactFilter} not declared in profile ${profile.id}.`,
157
+ nextSteps: [{ action: `Check artifacts in profile ${profile.id}`, kind: "required" }],
158
+ };
159
+ }
160
+ }
161
+
162
+ const artifactsToValidate = artifactFilter
163
+ ? profile.artifacts.filter((a) => a.id === artifactFilter)
164
+ : profile.artifacts;
165
+
166
+ if (dryRun) {
167
+ const commands = artifactsToValidate
168
+ .filter((a) => a.validate)
169
+ .map((a) => ({ artifactId: a.id, command: a.validate!.command }));
170
+ logger.info(`[dry-run] forge.validate — profile: ${profile.id}`);
171
+ for (const cmd of commands) {
172
+ logger.info(` ${cmd.artifactId}: ${cmd.command}`);
173
+ }
174
+ return {
175
+ data: {
176
+ command: "forge.validate",
177
+ profileId: profile.id,
178
+ artifacts: commands.map((c) => ({
179
+ id: c.artifactId,
180
+ command: c.command,
181
+ exitCode: 0,
182
+ passed: true,
183
+ violations: [],
184
+ stdout: "",
185
+ stderr: "",
186
+ })),
187
+ allPassed: true,
188
+ },
189
+ summary: `[dry-run] ${commands.length} validate command(s) resolved`,
190
+ };
191
+ }
192
+
193
+ const results: ForgeValidateArtifactResult[] = [];
194
+
195
+ for (const artifact of artifactsToValidate) {
196
+ if (!artifact.validate) {
197
+ logger.warn(`Skipping artifact ${artifact.id}: no validate command declared`);
198
+ results.push({
199
+ id: artifact.id,
200
+ command: "",
201
+ exitCode: 0,
202
+ passed: true,
203
+ violations: [],
204
+ stdout: "",
205
+ stderr: "skipped: no validate command",
206
+ });
207
+ continue;
208
+ }
209
+
210
+ const cmd = artifact.validate.command;
211
+ const outputFormat = artifact.validate.outputFormat;
212
+ const violationPattern = artifact.validate.violationPattern;
213
+ try {
214
+ const { stdout, stderr } = await execAsync(cmd, { cwd: workspaceRoot, timeout: 30_000 });
215
+ const violations = parseViolations(stdout + "\n" + stderr, outputFormat, violationPattern);
216
+ results.push({
217
+ id: artifact.id,
218
+ command: cmd,
219
+ exitCode: 0,
220
+ passed: true,
221
+ violations,
222
+ stdout,
223
+ stderr,
224
+ });
225
+ logger.success(` ${artifact.id}: OK`);
226
+ } catch (err) {
227
+ const e = err as { code?: number; stdout?: string; stderr?: string; message: string };
228
+ const rawStdout = e.stdout ?? "";
229
+ const rawStderr = e.stderr ?? e.message;
230
+ const violations = parseViolations(
231
+ rawStdout + "\n" + rawStderr,
232
+ outputFormat,
233
+ violationPattern,
234
+ );
235
+ results.push({
236
+ id: artifact.id,
237
+ command: cmd,
238
+ exitCode: e.code ?? 1,
239
+ passed: false,
240
+ violations,
241
+ stdout: rawStdout,
242
+ stderr: rawStderr,
243
+ });
244
+ logger.error(` ${artifact.id}: FAILED (exit ${e.code ?? 1})`);
245
+ }
246
+ }
247
+
248
+ const allPassed = results.every((r) => r.passed);
249
+
250
+ return {
251
+ data: {
252
+ command: "forge.validate",
253
+ profileId: profile.id,
254
+ artifacts: results,
255
+ allPassed,
256
+ },
257
+ exitCode: allPassed ? 0 : 1,
258
+ summary: allPassed
259
+ ? `forge.validate: all ${results.length} artifact(s) passed`
260
+ : `forge.validate: ${results.filter((r) => !r.passed).length} artifact(s) failed`,
261
+ };
262
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Lazy dynamic-import bridge for @warpgogol/werkstatt/fingerprint and @warpgogol/werkstatt-shared/share/fs. These workspace packages are not published to npm — when forge is installed standalone from npm, they are unavailable and handlers that depend on them must degrade gracefully.</purpose>
4
+ <non-goals>
5
+ <item>Do not re-export types from @warpgogol/* — only runtime functions.</item>
6
+ <item>Do not use static imports — that would break npm installs without workspace deps.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0682: extract dynamic-import helper so forge publishes without workspace:* deps.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ export type CollectFilesFn = (
15
+ root: string,
16
+ options?: {
17
+ extensions?: string[];
18
+ ignore?: (name: string) => boolean;
19
+ },
20
+ ) => Promise<string[]>;
21
+
22
+ export type ByteHashFileFn = (filePath: string) => Promise<string>;
23
+
24
+ interface WorkspaceDeps {
25
+ collectFiles: CollectFilesFn;
26
+ byteHashFile: ByteHashFileFn;
27
+ }
28
+
29
+ let cached: WorkspaceDeps | null = null;
30
+ let loadError: string | null = null;
31
+
32
+ export async function loadWorkspaceDeps(): Promise<WorkspaceDeps> {
33
+ if (cached) return cached;
34
+ if (loadError) throw new Error(loadError);
35
+
36
+ try {
37
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
38
+ // @ts-ignore — workspace dep, may not be installed when forge is used standalone from npm
39
+ const shareMod = await import("@warpgogol/werkstatt-shared/share/fs");
40
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
41
+ // @ts-ignore — workspace dep, may not be installed when forge is used standalone from npm
42
+ const fpMod = await import("@warpgogol/werkstatt/fingerprint");
43
+ cached = {
44
+ collectFiles: shareMod.collectFiles as CollectFilesFn,
45
+ byteHashFile: fpMod.byteHashFile as ByteHashFileFn,
46
+ };
47
+ return cached;
48
+ } catch (err) {
49
+ loadError =
50
+ `@warpgogol/werkstatt/fingerprint and @warpgogol/werkstatt-shared/share are required for this command but not installed. ` +
51
+ `When using forge standalone from npm, install them separately or use forge within the warpgogol workspace. ` +
52
+ `Error: ${(err as Error).message}`;
53
+ throw new Error(loadError);
54
+ }
55
+ }
@@ -0,0 +1,13 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Expose the forge core kernel module through a stable package subpath.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement handler logic.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0374: initial core module barrel.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ export { forgeCoreModule } from "./core.module.ts";
@@ -0,0 +1,90 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Register exploration note commands with the forge kernel registry.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement handler logic here — delegate to handlers/.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0710: initial forgeExplorationModule registering exploration.list, exploration.show, exploration.archive commands.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import type { ForgeModule } from "../../src/forge-module.ts";
14
+
15
+ export const forgeExplorationModule: ForgeModule = {
16
+ name: "forge-exploration",
17
+ version: "0.1.0",
18
+ async register(registry) {
19
+ const { runExplorationList } = await import("./handlers/list.ts");
20
+ const { runExplorationShow } = await import("./handlers/show.ts");
21
+ const { runExplorationArchive } = await import("./handlers/archive.ts");
22
+
23
+ registry.registerCommand({
24
+ name: "exploration.list",
25
+ description:
26
+ "List all exploration notes in docs/explorations/. Returns id, title, status, and createdAt for each note. " +
27
+ "Use --status <status> to filter by status (open, explored, archived). " +
28
+ "Returns an empty list with exit code 0 if the directory is empty or missing.",
29
+ scope: "workspace",
30
+ mutatesState: false,
31
+ writes: [],
32
+ reads: ["docs/explorations/*.md"],
33
+ cacheable: false,
34
+ flags: {
35
+ status: {
36
+ kind: "string",
37
+ description: "Filter by status (open, explored, archived).",
38
+ },
39
+ },
40
+ execute: runExplorationList,
41
+ });
42
+
43
+ registry.registerCommand({
44
+ name: "exploration.show",
45
+ description:
46
+ "Show the full content of a single exploration note. Use --id <slug> to specify the note slug " +
47
+ "(kebab-case, lowercase, latin-only). Returns the note's frontmatter and body. " +
48
+ "Returns exit code 1 if the note is not found.",
49
+ scope: "workspace",
50
+ mutatesState: false,
51
+ writes: [],
52
+ reads: ["docs/explorations/*.md"],
53
+ cacheable: false,
54
+ flags: {
55
+ id: {
56
+ kind: "string",
57
+ required: true,
58
+ description: "The slug of the exploration note to show (e.g. 'my-idea').",
59
+ },
60
+ },
61
+ execute: runExplorationShow,
62
+ });
63
+
64
+ registry.registerCommand({
65
+ name: "exploration.archive",
66
+ description:
67
+ "Archive an exploration note by setting its status to 'archived'. Use --id <slug> to specify the note. " +
68
+ "Use --rfc <RFC-XXXX> to add an RFC id to the note's 'related' field. " +
69
+ "Idempotent — if the note is already archived, returns exit code 0 with no changes. " +
70
+ "Returns exit code 1 if the note is not found or the slug is invalid (non-kebab-case).",
71
+ scope: "workspace",
72
+ mutatesState: true,
73
+ writes: ["docs/explorations/*.md"],
74
+ reads: ["docs/explorations/*.md"],
75
+ cacheable: false,
76
+ flags: {
77
+ id: {
78
+ kind: "string",
79
+ required: true,
80
+ description: "The slug of the exploration note to archive (e.g. 'my-idea').",
81
+ },
82
+ rfc: {
83
+ kind: "string",
84
+ description: "RFC id to add to the note's 'related' field (e.g. 'RFC-0710').",
85
+ },
86
+ },
87
+ execute: runExplorationArchive,
88
+ });
89
+ },
90
+ };