@warpgogol/forge 2.7.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/src/config/forge-config.d.ts +1 -1
  5. package/dist/src/config/forge-config.d.ts.map +1 -1
  6. package/dist/src/config/forge-config.js +2 -2
  7. package/dist/src/config/forge-config.js.map +1 -1
  8. package/dist/src/onboarding/agents-generate.d.ts +2 -1
  9. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  10. package/dist/src/onboarding/agents-generate.js +24 -4
  11. package/dist/src/onboarding/agents-generate.js.map +1 -1
  12. package/dist/src/profiles/profile-schema.d.ts +2 -0
  13. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  14. package/dist/src/profiles/profile-schema.js +1 -0
  15. package/dist/src/profiles/profile-schema.js.map +1 -1
  16. package/dist/src/profiles/stack-profile.d.ts +1 -0
  17. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  18. package/dist/src/profiles/stack-profile.js +2 -0
  19. package/dist/src/profiles/stack-profile.js.map +1 -1
  20. package/os/adr/adr.module.ts +158 -0
  21. package/os/adr/frontmatter-io.ts +80 -0
  22. package/os/adr/handlers/archive.ts +244 -0
  23. package/os/adr/handlers/implement-stamp.ts +331 -0
  24. package/os/adr/handlers/list-create.ts +207 -0
  25. package/os/adr/handlers/validate.test.ts +232 -0
  26. package/os/adr/handlers/validate.ts +429 -0
  27. package/os/adr/index.ts +33 -0
  28. package/os/adr/types.ts +141 -0
  29. package/os/audit/audit.module.ts +48 -0
  30. package/os/audit/frontmatter-io.ts +84 -0
  31. package/os/audit/handlers/archive.ts +239 -0
  32. package/os/audit/index.ts +23 -0
  33. package/os/audit/types.ts +29 -0
  34. package/os/compass/compass.module.ts +186 -0
  35. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  36. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  37. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  38. package/os/compass/handlers/compass-inventory.ts +521 -0
  39. package/os/compass/handlers/git-revision.ts +137 -0
  40. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  41. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  42. package/os/compass/index.ts +33 -0
  43. package/os/core/core.module.ts +948 -0
  44. package/os/core/handlers/assets-check.ts +122 -0
  45. package/os/core/handlers/assets-helpers.ts +174 -0
  46. package/os/core/handlers/assets-list.ts +99 -0
  47. package/os/core/handlers/build.ts +155 -0
  48. package/os/core/handlers/determinism-check.ts +354 -0
  49. package/os/core/handlers/dev.ts +127 -0
  50. package/os/core/handlers/invariant-engine.test.ts +681 -0
  51. package/os/core/handlers/knowledge-compact.ts +248 -0
  52. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  53. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  54. package/os/core/handlers/note-link-validate.test.ts +117 -0
  55. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  56. package/os/core/handlers/package-health.test.ts +226 -0
  57. package/os/core/handlers/package-health.ts +231 -0
  58. package/os/core/handlers/pinned-check.ts +200 -0
  59. package/os/core/handlers/pinned-init.ts +333 -0
  60. package/os/core/handlers/pinned-types.ts +50 -0
  61. package/os/core/handlers/pinned-validate.ts +301 -0
  62. package/os/core/handlers/profile-resolve.ts +92 -0
  63. package/os/core/handlers/release-prepare.ts +294 -0
  64. package/os/core/handlers/release-publish.ts +211 -0
  65. package/os/core/handlers/validate.ts +262 -0
  66. package/os/core/handlers/workspace-deps.ts +55 -0
  67. package/os/core/index.ts +13 -0
  68. package/os/exploration/exploration.module.ts +90 -0
  69. package/os/exploration/frontmatter-io.ts +77 -0
  70. package/os/exploration/handlers/archive.ts +159 -0
  71. package/os/exploration/handlers/list.ts +77 -0
  72. package/os/exploration/handlers/show.ts +107 -0
  73. package/os/exploration/index.ts +25 -0
  74. package/os/exploration/types.ts +65 -0
  75. package/os/mission/handlers/archive.test.ts +391 -0
  76. package/os/mission/handlers/archive.ts +453 -0
  77. package/os/mission/index.ts +1 -0
  78. package/os/mission/mission.module.ts +48 -0
  79. package/os/mission/types.ts +41 -0
  80. package/os/naming/index.ts +14 -0
  81. package/os/naming/naming-convention.test.ts +163 -0
  82. package/os/naming/naming-convention.ts +378 -0
  83. package/os/naming/naming.module.ts +36 -0
  84. package/os/notes/index.ts +10 -0
  85. package/os/notes/notes.module.ts +109 -0
  86. package/os/plan/frontmatter-io.ts +82 -0
  87. package/os/plan/handlers/archive.ts +238 -0
  88. package/os/plan/index.ts +23 -0
  89. package/os/plan/plan.module.ts +48 -0
  90. package/os/plan/types.ts +29 -0
  91. package/os/program/discovery.ts +294 -0
  92. package/os/program/handlers/complete.ts +419 -0
  93. package/os/program/handlers/lease.ts +433 -0
  94. package/os/program/handlers/seal.ts +332 -0
  95. package/os/program/handlers/validate.ts +213 -0
  96. package/os/program/lease.ts +156 -0
  97. package/os/program/program.module.ts +212 -0
  98. package/os/program/schemas.ts +218 -0
  99. package/os/program/state.ts +264 -0
  100. package/os/rfc/acceptance.ts +344 -0
  101. package/os/rfc/decision-log.ts +310 -0
  102. package/os/rfc/dna-trace.ts +280 -0
  103. package/os/rfc/frontmatter-io.test.ts +132 -0
  104. package/os/rfc/frontmatter-io.ts +130 -0
  105. package/os/rfc/handlers/archive.ts +300 -0
  106. package/os/rfc/handlers/check.ts +238 -0
  107. package/os/rfc/handlers/implement-stamp.ts +515 -0
  108. package/os/rfc/handlers/index-graph.ts +237 -0
  109. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  110. package/os/rfc/handlers/lifecycle.ts +249 -0
  111. package/os/rfc/handlers/list-create.ts +296 -0
  112. package/os/rfc/handlers/pipeline-status.ts +173 -0
  113. package/os/rfc/handlers/shared.ts +121 -0
  114. package/os/rfc/handlers/supersede-propose.ts +241 -0
  115. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  116. package/os/rfc/handlers/validate-rules.ts +984 -0
  117. package/os/rfc/handlers/validate.ts +166 -0
  118. package/os/rfc/handlers.ts +19 -0
  119. package/os/rfc/index.ts +97 -0
  120. package/os/rfc/rfc.module.ts +416 -0
  121. package/os/rfc/types.ts +596 -0
  122. package/os/rfc/verification-evidence.ts +244 -0
  123. package/os/session/atif-parser.ts +166 -0
  124. package/os/session/frontmatter-io.ts +163 -0
  125. package/os/session/handlers/archive.ts +288 -0
  126. package/os/session/handlers/list.ts +174 -0
  127. package/os/session/handlers/save.ts +348 -0
  128. package/os/session/handlers/validate.ts +250 -0
  129. package/os/session/index.ts +50 -0
  130. package/os/session/session.module.ts +126 -0
  131. package/os/session/types.ts +203 -0
  132. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  133. package/os/spec/live-spec-list.ts +78 -0
  134. package/os/spec/live-spec-merge.test.ts +261 -0
  135. package/os/spec/live-spec-merge.ts +413 -0
  136. package/os/spec/live-spec-show.ts +117 -0
  137. package/os/spec/live-spec-types.ts +93 -0
  138. package/os/spec/live-spec-validate.ts +170 -0
  139. package/os/spec/spec-materialize.ts +390 -0
  140. package/os/spec/spec-schema.ts +174 -0
  141. package/os/spec/spec-status.ts +284 -0
  142. package/os/spec/spec-validate.ts +513 -0
  143. package/os/spec/spec.module.ts +135 -0
  144. package/os/werkstatt/handlers/lock.ts +168 -0
  145. package/os/werkstatt/handlers/schema.ts +47 -0
  146. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  147. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  148. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  149. package/os/werkstatt/index.ts +39 -0
  150. package/os/werkstatt/werkstatt.module.ts +62 -0
  151. package/os/workflow/handlers.ts +304 -0
  152. package/os/workflow/index.ts +28 -0
  153. package/os/workflow/types.ts +99 -0
  154. package/os/workflow/workflow.module.ts +54 -0
  155. package/package.json +3 -3
  156. package/profiles/godot-csharp.yaml +5 -4
  157. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  158. package/profiles/phaser-turborepo.yaml +5 -5
  159. package/profiles/root-agents-godot.md +133 -0
  160. package/skills/_shared/fo-session-summary.md +132 -16
  161. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  162. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  163. package/skills/fo/fo-handoff/SKILL.md +5 -0
  164. package/skills/fo/fo-idea/SKILL.md +1 -1
  165. package/skills/fo/fo-review/SKILL.md +1 -1
  166. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  167. package/skills/fo/fo-session-save/SKILL.md +8 -0
  168. package/src/cli-output.ts +59 -0
  169. package/src/config/forge-config.ts +524 -0
  170. package/src/forge-module.ts +39 -0
  171. package/src/index.ts +172 -0
  172. package/src/knowledge/budgets.ts +202 -0
  173. package/src/knowledge/compact.ts +405 -0
  174. package/src/knowledge/index.ts +53 -0
  175. package/src/knowledge/parse.ts +227 -0
  176. package/src/knowledge/promote.ts +156 -0
  177. package/src/knowledge/schema.ts +112 -0
  178. package/src/knowledge/serialize.ts +72 -0
  179. package/src/migration-adapters/git-utils.ts +71 -0
  180. package/src/migration-adapters/ignored-files.ts +221 -0
  181. package/src/migration-adapters/index.ts +21 -0
  182. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  183. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  184. package/src/migration-adapters/registry.ts +61 -0
  185. package/src/migration-adapters/types.ts +68 -0
  186. package/src/onboarding/agents-generate.ts +443 -0
  187. package/src/onboarding/create.ts +427 -0
  188. package/src/onboarding/doctor.ts +1297 -0
  189. package/src/onboarding/init.ts +337 -0
  190. package/src/onboarding/invariant-engine.ts +448 -0
  191. package/src/onboarding/memory-scaffold.ts +189 -0
  192. package/src/onboarding/nested-agents-generate.ts +93 -0
  193. package/src/onboarding/nested-agents-templates.ts +233 -0
  194. package/src/onboarding/profile-validate.ts +139 -0
  195. package/src/onboarding/scaffold-project.ts +299 -0
  196. package/src/onboarding/scaffold.ts +143 -0
  197. package/src/onboarding/upgrade.ts +558 -0
  198. package/src/onboarding/workspace-discovery.ts +167 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +51 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +217 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/fs-atomic.ts +93 -0
  256. package/src/utils/fs-idempotent.ts +41 -0
  257. package/src/utils/fs-trash-sync.ts +37 -0
  258. package/src/utils/fs-trash.ts +24 -0
  259. package/src/utils/fs.ts +74 -0
  260. package/src/utils/generated-marker.ts +166 -0
  261. package/src/utils/hash.ts +19 -0
  262. package/src/utils/index.ts +29 -0
  263. package/src/utils/string-utils.ts +19 -0
  264. package/src/validators/note-frontmatter-validate.ts +142 -0
  265. package/src/validators/note-link-validate.ts +145 -0
  266. package/src/validators/note-orphan-detect.ts +146 -0
  267. package/src/validators/port-validate.ts +97 -0
  268. package/src/validators/skill-validate.ts +831 -0
package/src/types.ts ADDED
@@ -0,0 +1,221 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Canonical forge runtime types — autonomous, no @warpgogol/* dependencies.
4
+ Structurally compatible with @warpgogol/site-kernel types so that forge modules
5
+ can be registered in a Warpgogol kernel without direct dependency.</purpose>
6
+ <non-goals>
7
+ <item>Do not import from @warpgogol/site-kernel or any @warpgogol/* package.</item>
8
+ <item>Do not add Warpgogol-specific fields to ForgeRuntimeContext — keep it minimal and portable.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>Initial canonical forge types: ForgeCommandInput, ForgeCommandResult, ForgeRuntimeContext, ForgeFlagSpec, Diagnostic, CommandRegistry, ForgeLogger.</item>
13
+ <item>RFC-0518: add GateMetadata, GateSeverity, GatePhase, GateConditional types and optional gate field to ForgeCommandMetadata (structurally compatible with @warpgogol/site-kernel).</item>
14
+ </CHANGE_SUMMARY>
15
+ */
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Command input / output
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export type ForgeFlagValue = boolean | string | string[];
22
+
23
+ export interface ForgeCommandInput {
24
+ argv: string[];
25
+ flags: Record<string, ForgeFlagValue>;
26
+ }
27
+
28
+ export interface ForgeCommandTiming {
29
+ durationMs?: number;
30
+ startedAt?: string;
31
+ endedAt?: string;
32
+ }
33
+
34
+ export interface ForgeNextStep {
35
+ action: string;
36
+ kind: "required" | "optional";
37
+ }
38
+
39
+ export interface ForgeCommandResult<TData = unknown> {
40
+ data?: TData;
41
+ nextSteps?: ForgeNextStep[];
42
+ exitCode?: number;
43
+ summary?: string;
44
+ timing?: ForgeCommandTiming;
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Flag specs (RFC-0260 lineage)
49
+ // ---------------------------------------------------------------------------
50
+
51
+ export interface ForgeFlagSpec {
52
+ kind: "boolean" | "string" | "string[]";
53
+ required?: boolean;
54
+ default?: ForgeFlagValue;
55
+ description: string;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Command metadata / definition
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export type ForgeCommandScope = "app" | "workspace";
63
+
64
+ export interface ForgeCommandMetadata {
65
+ description: string;
66
+ scope: ForgeCommandScope;
67
+ mutatesState?: boolean;
68
+ requiresNetwork?: boolean;
69
+ supportsAllSites?: boolean;
70
+ timeoutMs?: number;
71
+ expectedDurationMs?: number;
72
+ longRunning?: boolean;
73
+ cacheable?: boolean;
74
+ hidden?: boolean;
75
+ /**
76
+ * RFC-0518: declarative gate metadata. Optional. Structurally compatible
77
+ * with GateMetadata from @warpgogol/site-kernel. Does NOT affect execution.
78
+ */
79
+ gate?: GateMetadata;
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // RFC-0518: declarative gate metadata (structurally compatible with @warpgogol/site-kernel)
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export type GateSeverity = "error" | "warning" | "mixed";
87
+ export type GatePhase = "author" | "postbuild" | "workspace" | "mission" | "release";
88
+
89
+ export interface GateConditional {
90
+ kind: "entitlement" | "flag" | "config";
91
+ ref: string;
92
+ description: string;
93
+ }
94
+
95
+ export interface GateMetadata {
96
+ severity: GateSeverity;
97
+ phase: GatePhase;
98
+ conditional?: GateConditional;
99
+ surfaces?: string[];
100
+ rules?: string[];
101
+ blocks?: string[];
102
+ }
103
+
104
+ export interface ForgeRegisteredCommandInfo extends ForgeCommandMetadata {
105
+ name: string;
106
+ provider: "workspace" | "site";
107
+ siteName?: string;
108
+ module?: string;
109
+ flags?: Record<string, ForgeFlagSpec>;
110
+ reads?: string[];
111
+ writes?: string[];
112
+ hidden?: boolean;
113
+ }
114
+
115
+ export interface ForgeCommandDefinition<TData = unknown> extends ForgeCommandMetadata {
116
+ name: string;
117
+ flags?: Record<string, ForgeFlagSpec>;
118
+ reads?: string[];
119
+ writes?: string[];
120
+ execute(
121
+ input: ForgeCommandInput,
122
+ context: ForgeRuntimeContext,
123
+ ): Promise<void | ForgeCommandResult<TData>> | void | ForgeCommandResult<TData>;
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Diagnostic (RFC-0203 lineage)
128
+ // ---------------------------------------------------------------------------
129
+
130
+ export type DiagnosticSeverity = "error" | "warning" | "info";
131
+
132
+ export interface DiagnosticEvidence {
133
+ kind: "rule" | "rendered" | "source" | "config" | "cache" | "runtime";
134
+ ruleFile?: string;
135
+ ruleId?: string;
136
+ file?: string;
137
+ url?: string;
138
+ snippet?: string;
139
+ }
140
+
141
+ export interface Diagnostic {
142
+ ruleId: string;
143
+ severity: DiagnosticSeverity;
144
+ message: string;
145
+ file?: string;
146
+ line?: number;
147
+ column?: number;
148
+ fixHint?: string;
149
+ evidence?: DiagnosticEvidence[];
150
+ data?: Record<string, unknown>;
151
+ id?: string;
152
+ blockId?: string;
153
+ suggestion?: string;
154
+ }
155
+
156
+ export interface CheckResult {
157
+ command: string;
158
+ status: "pass" | "warn" | "fail";
159
+ diagnostics: Diagnostic[];
160
+ summary: { error: number; warning: number; info: number };
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Logger — minimal, structurally compatible with KernelLogger
165
+ // ---------------------------------------------------------------------------
166
+
167
+ export interface ForgeLogger {
168
+ section(message: string, details?: unknown): void;
169
+ info(message: string, details?: unknown): void;
170
+ warn(message: string, details?: unknown): void;
171
+ error(message: string, details?: unknown): void;
172
+ success(message: string, details?: unknown): void;
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // CommandRegistry — interface injection for command discovery
177
+ // ---------------------------------------------------------------------------
178
+
179
+ export interface CommandRegistry {
180
+ listCommandNames(): string[];
181
+ listCommands(): ForgeRegisteredCommandInfo[];
182
+ getCommand(name: string): ForgeCommandDefinition | undefined;
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Runtime context — minimal, portable
187
+ // ---------------------------------------------------------------------------
188
+
189
+ export type ForgeOutputFormat = "pretty" | "json";
190
+
191
+ export interface ForgeSiteContext {
192
+ name: string;
193
+ directory: string;
194
+ toolsDirectory: string;
195
+ configPath?: string;
196
+ packageName?: string;
197
+ }
198
+
199
+ export interface ForgeRuntimeContext {
200
+ workspaceRoot: string;
201
+ logger: ForgeLogger;
202
+ dryRun: boolean;
203
+ outputFormat: ForgeOutputFormat;
204
+ /**
205
+ * Optional command registry — provided by the runner (forge CLI or site-kernel).
206
+ * Forge commands that need to discover other commands (e.g. rfc lifecycle validation)
207
+ * access this through the context instead of importing a kernel-specific function.
208
+ */
209
+ commandRegistry?: CommandRegistry;
210
+ /** Optional target site/app context — provided when running in app-scoped mode. */
211
+ site?: ForgeSiteContext;
212
+ /** True only when --site <name> was passed explicitly by the caller. */
213
+ siteExplicit?: boolean;
214
+ /**
215
+ * Optional forge root override — provided when a command is called from
216
+ * another command (e.g. forge.create delegates to forge.init and forge.scaffold
217
+ * in a new directory where resolveForgeRoot would fail). When set, handlers
218
+ * use this instead of calling resolveForgeRoot(workspaceRoot).
219
+ */
220
+ forgeRoot?: string;
221
+ }
@@ -0,0 +1,93 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Atomic file-write primitive — canonical forge utility. Writes to a temp
4
+ file then renames, with bounded retry for Windows EPERM/EBUSY.</purpose>
5
+ <non-goals>
6
+ <item>Do not provide cross-process locks — convergent atomic writes are sufficient.</item>
7
+ <item>Do not fall back to non-atomic writes — fail loudly.</item>
8
+ </non-goals>
9
+ </MODULE_CONTRACT>
10
+ <CHANGE_SUMMARY>
11
+ <item>Moved from @warpgogol/site-kernel/fs-atomic to forge as canonical source (dependency inversion).</item>
12
+ </CHANGE_SUMMARY>
13
+ */
14
+
15
+ import { writeFile, rename as fsRename, unlink } from "node:fs/promises";
16
+ import { existsSync } from "node:fs";
17
+ import { dirname, join, basename } from "node:path";
18
+ import { randomBytes } from "node:crypto";
19
+
20
+ export interface WriteFileAtomicOptions {
21
+ retries?: number;
22
+ }
23
+
24
+ let renameImpl: typeof fsRename = fsRename;
25
+
26
+ export function __setRenameImplForTests(impl: typeof fsRename | undefined): void {
27
+ renameImpl = impl ?? fsRename;
28
+ }
29
+
30
+ const DEFAULT_RETRIES = 10;
31
+ const RETRY_BACKOFF_MS = 50;
32
+ const RETRYABLE_CODES = new Set(["EPERM", "EBUSY"]);
33
+
34
+ function isRetryableError(error: unknown): boolean {
35
+ if (!error || typeof error !== "object") return false;
36
+ const code = (error as NodeJS.ErrnoException).code;
37
+ return typeof code === "string" && RETRYABLE_CODES.has(code);
38
+ }
39
+
40
+ function sleep(ms: number): Promise<void> {
41
+ return new Promise((resolve) => setTimeout(resolve, ms));
42
+ }
43
+
44
+ function makeTempPath(filePath: string): string {
45
+ const dir = dirname(filePath);
46
+ const name = basename(filePath);
47
+ const random = randomBytes(6).toString("hex");
48
+ return join(dir, `${name}.${random}.tmp`);
49
+ }
50
+
51
+ export async function writeFileAtomic(
52
+ filePath: string,
53
+ content: string | Uint8Array,
54
+ options?: WriteFileAtomicOptions,
55
+ ): Promise<void> {
56
+ const retries = options?.retries ?? DEFAULT_RETRIES;
57
+ const tempPath = makeTempPath(filePath);
58
+ let tempWritten = false;
59
+
60
+ try {
61
+ await writeFile(tempPath, content);
62
+ tempWritten = true;
63
+
64
+ let attempt = 0;
65
+ for (;;) {
66
+ try {
67
+ await renameImpl(tempPath, filePath);
68
+ return;
69
+ } catch (error) {
70
+ if (attempt >= retries || !isRetryableError(error)) {
71
+ throw error;
72
+ }
73
+ attempt += 1;
74
+ if (existsSync(filePath)) {
75
+ try {
76
+ await unlink(filePath);
77
+ } catch {
78
+ // If unlink also fails, the retry loop will try again.
79
+ }
80
+ }
81
+ await sleep(RETRY_BACKOFF_MS * attempt);
82
+ }
83
+ }
84
+ } finally {
85
+ if (tempWritten) {
86
+ try {
87
+ await unlink(tempPath);
88
+ } catch {
89
+ // Expected in the success path (temp file already renamed away).
90
+ }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Idempotent file-write primitive — canonical forge utility. Writes only
4
+ when file content differs, delegating to writeFileAtomic for the actual write.
5
+ Reduces unnecessary disk writes and git churn in Compass and Werkstatt commands.</purpose>
6
+ <non-goals>
7
+ <item>Do not provide cross-process locks — convergent atomic writes are sufficient.</item>
8
+ <item>Do not fall back to non-atomic writes — fail loudly.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-0556: moved from @warpgogol/site-kernel/fs-idempotent to forge as canonical source (dependency inversion).</item>
13
+ <item>RFC-0603: extended to accept Uint8Array (Buffer) content for idempotent binary file writes — PNG preview images.</item>
14
+ </CHANGE_SUMMARY>
15
+ */
16
+
17
+ import { readFile } from "node:fs/promises";
18
+ import { writeFileAtomic } from "./fs-atomic.ts";
19
+
20
+ export async function writeFileIfChanged(
21
+ filePath: string,
22
+ content: string | Uint8Array,
23
+ ): Promise<"written" | "unchanged"> {
24
+ try {
25
+ if (typeof content === "string") {
26
+ const existing = await readFile(filePath, "utf8");
27
+ if (existing === content) {
28
+ return "unchanged";
29
+ }
30
+ } else {
31
+ const existing = await readFile(filePath);
32
+ if (Buffer.compare(existing, Buffer.from(content)) === 0) {
33
+ return "unchanged";
34
+ }
35
+ }
36
+ } catch {
37
+ // File does not exist — proceed to write.
38
+ }
39
+ await writeFileAtomic(filePath, content);
40
+ return "written";
41
+ }
@@ -0,0 +1,37 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Synchronous trash-can deletion — shells out to `trash-put` (trash-cli)
4
+ on Linux/macOS and PowerShell Recycle Bin on Windows. Used by synchronous
5
+ code paths that cannot use the async `trash` npm package.</purpose>
6
+ <non-goals>
7
+ <item>Do not use for async code paths — use trashPath from fs-trash.ts instead.</item>
8
+ <item>Requires `trash-put` (trash-cli) installed on Linux/macOS. On Windows, uses PowerShell Recycle Bin API (no external binary needed).</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>Initial implementation: sync wrapper for `trash-put` (Linux/macOS) and PowerShell Recycle Bin (Windows).</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import { existsSync, statSync } from "node:fs";
17
+ import { execFileSync } from "node:child_process";
18
+
19
+ export function trashSync(targetPath: string): void {
20
+ if (!existsSync(targetPath)) return;
21
+ if (process.platform === "win32") {
22
+ const isDir = statSync(targetPath).isDirectory();
23
+ const method = isDir ? "DeleteDirectory" : "DeleteFile";
24
+ const escaped = targetPath.replace(/'/g, "''");
25
+ execFileSync(
26
+ "powershell",
27
+ [
28
+ "-NoProfile",
29
+ "-Command",
30
+ `Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::${method}('${escaped}', 'OnlyErrorDialogs', 'SendToRecycleBin')`,
31
+ ],
32
+ { stdio: "pipe" },
33
+ );
34
+ } else {
35
+ execFileSync("trash-put", [targetPath], { stdio: "pipe" });
36
+ }
37
+ }
@@ -0,0 +1,24 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Trash-can deletion primitive — moves files/directories to the OS
4
+ trash/recycle bin instead of permanently deleting them. Uses the `trash`
5
+ npm package which implements the FreeDesktop.org Trash specification on
6
+ Linux (no external `trash-put` binary required) and the Recycle Bin on
7
+ Windows.</purpose>
8
+ <non-goals>
9
+ <item>Do not implement trash eviction or retention policies — the OS manages that.</item>
10
+ <item>Do not use for ephemeral cleanup (lock files, temp files, atomic write leftovers) — those are system-internal and should use fs.unlink directly.</item>
11
+ </non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY>
14
+ <item>Initial implementation: wraps npm `trash` package with glob disabled for safe path handling.</item>
15
+ </CHANGE_SUMMARY>
16
+ */
17
+
18
+ import { existsSync } from "node:fs";
19
+ import trash from "trash";
20
+
21
+ export async function trashPath(targetPath: string): Promise<void> {
22
+ if (!existsSync(targetPath)) return;
23
+ await trash(targetPath, { glob: false });
24
+ }
@@ -0,0 +1,74 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Trivial filesystem utilities — inlined from @warpgogol/share to avoid dependency.</purpose>
4
+ <non-goals>
5
+ <item>Do not add non-filesystem utilities here — use dedicated utility modules.</item>
6
+ <item>Do not introduce @warpgogol/* imports — this package must remain dependency-free.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>Initial inline of collectFiles, fileExists from @warpgogol/werkstatt-shared/share/fs.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import { readdir, stat } from "node:fs/promises";
15
+ import { join } from "node:path";
16
+
17
+ export interface CollectFilesOptions {
18
+ extensions?: string[];
19
+ ignore?: (name: string) => boolean;
20
+ withDirs?: boolean;
21
+ }
22
+
23
+ function defaultIgnore(name: string): boolean {
24
+ return name.startsWith("-") || name.startsWith("old-");
25
+ }
26
+
27
+ export async function collectFiles(
28
+ root: string,
29
+ options: CollectFilesOptions = {},
30
+ ): Promise<string[]> {
31
+ const { extensions, ignore = defaultIgnore, withDirs = false } = options;
32
+ const results: string[] = [];
33
+
34
+ // fs.walk.lint: allow — this is the canonical collectFiles implementation,
35
+ // inlined from @warpgogol/werkstatt-shared/share/fs to keep @warpgogol/forge dependency-free (RFC-0303).
36
+ async function walk(dir: string): Promise<void> {
37
+ let entries;
38
+ try {
39
+ entries = await readdir(dir, { withFileTypes: true });
40
+ } catch {
41
+ return;
42
+ }
43
+
44
+ for (const entry of entries) {
45
+ if (ignore(entry.name)) continue;
46
+
47
+ const full = join(dir, entry.name);
48
+
49
+ if (entry.isDirectory()) {
50
+ if (withDirs) results.push(full);
51
+ await walk(full);
52
+ continue;
53
+ }
54
+
55
+ if (!entry.isFile()) continue;
56
+
57
+ if (extensions && !extensions.some((ext) => entry.name.endsWith(ext))) continue;
58
+
59
+ results.push(full);
60
+ }
61
+ }
62
+
63
+ await walk(root);
64
+ return results;
65
+ }
66
+
67
+ export async function fileExists(path: string): Promise<boolean> {
68
+ try {
69
+ await stat(path);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
@@ -0,0 +1,166 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Canonical GENERATED_MARKER constant and advisory header builder for
4
+ generated-file governance. Moved from @warpgogol/site-kernel to forge as canonical
5
+ source (dependency inversion).</purpose>
6
+ <non-goals>
7
+ <item>Do not perform file I/O — operates on in-memory content strings only.</item>
8
+ </non-goals>
9
+ </MODULE_CONTRACT>
10
+ <CHANGE_SUMMARY>
11
+ <item>Moved from @warpgogol/site-kernel/generated-marker to forge as canonical source.</item>
12
+ <item>Added optional commandPrefix to GeneratedHeaderInput — defaults to "forge" for autonomous mode; site-kernel passes "pnpm exec werkstatt run".</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ export const GENERATED_MARKER =
17
+ "GENERATED. Do not change this line unless the file contains project specific changes.";
18
+
19
+ export function hasGeneratedMarker(content: string): boolean {
20
+ return content.includes(GENERATED_MARKER);
21
+ }
22
+
23
+ export interface StripGeneratedMarkerResult {
24
+ changed: boolean;
25
+ content: string;
26
+ }
27
+
28
+ function escapeRegex(str: string): string {
29
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+ }
31
+
32
+ const DO_NOT_EDIT_LINE = "DO NOT EDIT THIS FILE. Changes are overwritten on the next build.";
33
+
34
+ export function stripGeneratedMarker(content: string): StripGeneratedMarkerResult {
35
+ const raw = GENERATED_MARKER;
36
+ let result = content;
37
+ let changed = false;
38
+
39
+ const applyAll = (patterns: RegExp[]): void => {
40
+ for (const pattern of patterns) {
41
+ const next = result.replace(pattern, "");
42
+ if (next !== result) {
43
+ changed = true;
44
+ result = next;
45
+ }
46
+ }
47
+ };
48
+
49
+ applyAll([
50
+ new RegExp(`<!--[ \\t]*\\n[\\s\\S]*?${escapeRegex(raw)}[\\s\\S]*?-->\\n?`, "g"),
51
+ new RegExp(`/\\*[ \\t]*\\n[\\s\\S]*?${escapeRegex(raw)}[\\s\\S]*?\\*/\\n?`, "g"),
52
+ ]);
53
+
54
+ for (const token of ["//", "#"]) {
55
+ const t = escapeRegex(token);
56
+ const advisoryLine = (suffix: string): string => `(?:[ \\t]*${t}[ \\t]*${suffix}\\n)?`;
57
+ const pattern = new RegExp(
58
+ `^[ \\t]*${t}[ \\t]*${escapeRegex(raw)}\\n` +
59
+ advisoryLine(escapeRegex(DO_NOT_EDIT_LINE)) +
60
+ advisoryLine("Owner command:.*") +
61
+ advisoryLine("Edit instead:.*") +
62
+ advisoryLine("Regenerate:.*"),
63
+ "gm",
64
+ );
65
+ applyAll([pattern]);
66
+ }
67
+
68
+ applyAll([
69
+ new RegExp(`^\\s*<!--\\s*${escapeRegex(raw)}\\s*-->\\n?`, "gm"),
70
+ new RegExp(`^\\s*//\\s*${escapeRegex(raw)}\\n?`, "gm"),
71
+ new RegExp(`^\\s*/\\*\\s*${escapeRegex(raw)}\\s*\\*/\\n?`, "gm"),
72
+ new RegExp(`^\\s*#\\s*${escapeRegex(raw)}\\n?`, "gm"),
73
+ new RegExp(`^\\s*${escapeRegex(raw)}\\n?`, "gm"),
74
+ ]);
75
+
76
+ if (result.includes(raw)) {
77
+ const next = result.split(raw).join("");
78
+ if (next !== result) {
79
+ changed = true;
80
+ result = next;
81
+ }
82
+ }
83
+
84
+ return { changed, content: result };
85
+ }
86
+
87
+ export interface GeneratedHeaderInput {
88
+ filePath: string;
89
+ ownerCommand: string;
90
+ templatePath?: string;
91
+ site?: string;
92
+ /**
93
+ * Command prefix for the regenerate hint. Defaults to "forge" (autonomous mode).
94
+ * Site-kernel callers pass "pnpm exec werkstatt run" for Warpgogol context.
95
+ */
96
+ commandPrefix?: string;
97
+ }
98
+
99
+ type GeneratedHeaderCommentStyle = "line-slash" | "line-hash" | "block-html" | "block-css";
100
+
101
+ function commentStyleForPath(filePath: string): GeneratedHeaderCommentStyle {
102
+ const lower = filePath.toLowerCase().replace(/\\/g, "/");
103
+ const base = lower.slice(lower.lastIndexOf("/") + 1);
104
+ if (
105
+ base === ".gitattributes" ||
106
+ base === "_headers" ||
107
+ base === "_redirects" ||
108
+ base.endsWith(".txt") ||
109
+ base.endsWith(".yml") ||
110
+ base.endsWith(".yaml")
111
+ ) {
112
+ return "line-hash";
113
+ }
114
+ if (base.endsWith(".md")) return "block-html";
115
+ if (base.endsWith(".css")) return "block-css";
116
+ return "line-slash";
117
+ }
118
+
119
+ export function buildGeneratedHeader(input: GeneratedHeaderInput): string {
120
+ const style = commentStyleForPath(input.filePath);
121
+ const prefix = input.commandPrefix ?? "forge";
122
+ const regenerateCommand = input.site
123
+ ? `${prefix} ${input.ownerCommand} --site ${input.site}`
124
+ : `${prefix} ${input.ownerCommand}`;
125
+ const editInstead = input.templatePath
126
+ ? `Edit instead: ${input.templatePath}`
127
+ : `Edit instead: the ${input.ownerCommand} generator source (not this file).`;
128
+
129
+ const lines = [
130
+ GENERATED_MARKER,
131
+ DO_NOT_EDIT_LINE,
132
+ `Owner command: ${input.ownerCommand}`,
133
+ editInstead,
134
+ `Regenerate: ${regenerateCommand}`,
135
+ ];
136
+
137
+ switch (style) {
138
+ case "line-slash":
139
+ return `${lines.map((line) => `// ${line}`).join("\n")}\n`;
140
+ case "line-hash":
141
+ return `${lines.map((line) => `# ${line}`).join("\n")}\n`;
142
+ case "block-css":
143
+ return `/*\n${lines.map((line) => ` ${line}`).join("\n")}\n*/\n`;
144
+ case "block-html":
145
+ return `<!--\n${lines.map((line) => ` ${line}`).join("\n")}\n-->\n`;
146
+ }
147
+ }
148
+
149
+ export function isGeneratedMarkerTextCandidate(filePath: string): boolean {
150
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
151
+ const TEXT_EXTS = new Set([
152
+ ".txt",
153
+ ".xml",
154
+ ".md",
155
+ ".yml",
156
+ ".yaml",
157
+ ".css",
158
+ ".js",
159
+ ".mjs",
160
+ ".ts",
161
+ ".json",
162
+ ".html",
163
+ ".astro",
164
+ ]);
165
+ return TEXT_EXTS.has(ext);
166
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Trivial hash utility — inlined from @warpgogol/fingerprint to avoid dependency.</purpose>
4
+ <non-goals>
5
+ <item>Do not add non-hash utilities here — use dedicated utility modules.</item>
6
+ <item>Do not introduce @warpgogol/* imports — this package must remain dependency-free.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>Initial inline of byteHash from @warpgogol/fingerprint/primitives.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import { createHash } from "node:crypto";
15
+
16
+ export function byteHash(bytes: Uint8Array | string): string {
17
+ const input = typeof bytes === "string" ? Buffer.from(bytes, "utf8") : bytes;
18
+ return `sha256:${createHash("sha256").update(input).digest("hex")}`;
19
+ }