@warpgogol/forge 4.0.0 → 4.1.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 (46) hide show
  1. package/AGENTS.md +18 -3
  2. package/bin/cli.ts +15 -8
  3. package/os/adr/adr.module.ts +24 -23
  4. package/os/adr/index.ts +1 -1
  5. package/os/audit/audit.module.ts +13 -8
  6. package/os/audit/index.ts +1 -1
  7. package/os/compass/compass.module.ts +22 -19
  8. package/os/core/core.module.ts +842 -864
  9. package/os/core/handlers/package-health.ts +21 -0
  10. package/os/core/index.ts +1 -1
  11. package/os/exploration/exploration.module.ts +19 -16
  12. package/os/exploration/index.ts +2 -2
  13. package/os/mission/index.ts +1 -1
  14. package/os/mission/mission.module.ts +13 -8
  15. package/os/naming/index.ts +1 -1
  16. package/os/naming/naming-convention.ts +1 -0
  17. package/os/naming/naming.module.ts +13 -7
  18. package/os/notes/index.ts +2 -2
  19. package/os/notes/notes.module.ts +20 -17
  20. package/os/plan/index.ts +1 -1
  21. package/os/plan/plan.module.ts +13 -8
  22. package/os/plugin/plugin.module.ts +10 -8
  23. package/os/program/program.module.ts +22 -20
  24. package/os/rfc/handlers/implement-stamp.ts +17 -1
  25. package/os/rfc/index.ts +1 -1
  26. package/os/rfc/rfc-0000-template.md +3 -2
  27. package/os/rfc/rfc.module.ts +54 -84
  28. package/os/rfc/types.ts +1 -0
  29. package/os/session/handlers/metrics-aggregate.ts +208 -0
  30. package/os/session/handlers/metrics-rfc.ts +339 -0
  31. package/os/session/handlers/metrics-session.ts +209 -0
  32. package/os/session/handlers/save.ts +19 -0
  33. package/os/session/index.ts +16 -1
  34. package/os/session/session.module.ts +133 -107
  35. package/os/session/types.ts +99 -0
  36. package/os/spec/spec.module.ts +28 -29
  37. package/os/werkstatt/werkstatt.module.ts +12 -9
  38. package/os/workflow/index.ts +1 -1
  39. package/os/workflow/workflow.module.ts +18 -12
  40. package/package.json +3 -1
  41. package/src/forge-module.ts +24 -10
  42. package/src/index.ts +12 -12
  43. package/src/onboarding/scaffold.ts +6 -4
  44. package/src/tests/metrics-rfc-1053.test.ts +355 -0
  45. package/src/types/werkstatt-engine-shims.d.ts +126 -48
  46. package/src/types/werkstatt-shared-shims.d.ts +6 -0
@@ -45,938 +45,916 @@ import {
45
45
  } from "../../src/registry.ts";
46
46
  import { loadForgeConfig } from "../../src/config/forge-config.ts";
47
47
 
48
- export const forgeCoreModule: ForgeModule = {
49
- name: "forge-core",
50
- version: "0.1.0",
51
- runtime: "autonomous",
52
- async register(registry) {
53
- const { runScaffold } = await import("../../src/onboarding/scaffold.ts");
54
- const { runSkillValidate } = await import("../../src/validators/skill-validate.ts");
55
- const { runPortValidate } = await import("../../src/validators/port-validate.ts");
56
- const { runDoctor } = await import("../../src/onboarding/doctor.ts");
57
- const { runAgentsGenerate } = await import("../../src/onboarding/agents-generate.ts");
58
- const { runScaffoldProject } = await import("../../src/onboarding/scaffold-project.ts");
59
- const { runUpgrade } = await import("../../src/onboarding/upgrade.ts");
60
- const { runCreate } = await import("../../src/onboarding/create.ts");
61
- const { runProfileValidate } = await import("../../src/onboarding/profile-validate.ts");
62
- const { runDev } = await import("./handlers/dev.ts");
63
- const { runBuild } = await import("./handlers/build.ts");
64
- const { runValidate } = await import("./handlers/validate.ts");
65
- const { runPinnedValidate } = await import("./handlers/pinned-validate.ts");
66
- const { runPinnedInit } = await import("./handlers/pinned-init.ts");
67
-
68
- const scaffoldWrapper = async (
69
- input: ForgeCommandInput,
70
- context: ForgeRuntimeContext,
71
- ): Promise<ForgeCommandResult> => {
72
- const result = runScaffold(input, context);
73
- const nextSteps: ForgeNextStep[] =
48
+ export async function createForgeCoreModule(): Promise<ForgeModule> {
49
+ const { runScaffold } = await import("../../src/onboarding/scaffold.ts");
50
+ const { runSkillValidate } = await import("../../src/validators/skill-validate.ts");
51
+ const { runPortValidate } = await import("../../src/validators/port-validate.ts");
52
+ const { runDoctor } = await import("../../src/onboarding/doctor.ts");
53
+ const { runAgentsGenerate } = await import("../../src/onboarding/agents-generate.ts");
54
+ const { runScaffoldProject } = await import("../../src/onboarding/scaffold-project.ts");
55
+ const { runUpgrade } = await import("../../src/onboarding/upgrade.ts");
56
+ const { runCreate } = await import("../../src/onboarding/create.ts");
57
+ const { runProfileValidate } = await import("../../src/onboarding/profile-validate.ts");
58
+ const { runDev } = await import("./handlers/dev.ts");
59
+ const { runBuild } = await import("./handlers/build.ts");
60
+ const { runValidate } = await import("./handlers/validate.ts");
61
+ const { runPinnedValidate } = await import("./handlers/pinned-validate.ts");
62
+ const { runPinnedInit } = await import("./handlers/pinned-init.ts");
63
+ const { runAssetsList } = await import("./handlers/assets-list.ts");
64
+ const { runAssetsCheck } = await import("./handlers/assets-check.ts");
65
+ const { runReleasePrepare } = await import("./handlers/release-prepare.ts");
66
+ const { runReleasePublish } = await import("./handlers/release-publish.ts");
67
+ const { runKnowledgeCompact } = await import("./handlers/knowledge-compact.ts");
68
+ const { runPackageHealth } = await import("./handlers/package-health.ts");
69
+ const { runForgeAutonomyValidate } = await import("./handlers/forge-autonomy-validate.ts");
70
+
71
+ const scaffoldWrapper = async (
72
+ input: ForgeCommandInput,
73
+ context: ForgeRuntimeContext,
74
+ ): Promise<ForgeCommandResult> => {
75
+ const result = runScaffold(input, context);
76
+ const nextSteps: ForgeNextStep[] =
77
+ result.status === "pass"
78
+ ? [
79
+ { action: "Edit the scaffolded SKILL.md to implement your skill", kind: "required" },
80
+ { action: "Run forge.skill.validate to check compliance", kind: "optional" },
81
+ ]
82
+ : [{ action: "Fix the errors above and re-run forge.port.scaffold", kind: "required" }];
83
+ return {
84
+ data: result,
85
+ nextSteps,
86
+ exitCode: result.status === "pass" ? 0 : 1,
87
+ summary:
74
88
  result.status === "pass"
75
- ? [
76
- { action: "Edit the scaffolded SKILL.md to implement your skill", kind: "required" },
77
- { action: "Run forge.skill.validate to check compliance", kind: "optional" },
78
- ]
79
- : [{ action: "Fix the errors above and re-run forge.port.scaffold", kind: "required" }];
80
- return {
81
- data: result,
82
- nextSteps,
83
- exitCode: result.status === "pass" ? 0 : 1,
84
- summary:
85
- result.status === "pass"
86
- ? `[forge.port.scaffold] OK — ${result.created.length} files created`
87
- : undefined,
88
- };
89
+ ? `[forge.port.scaffold] OK — ${result.created.length} files created`
90
+ : undefined,
89
91
  };
90
-
91
- const skillValidateWrapper = async (
92
- _input: ForgeCommandInput,
93
- context: ForgeRuntimeContext,
94
- ): Promise<ForgeCommandResult> => {
95
- const result = runSkillValidate({ flags: {} }, context);
96
- return {
97
- data: result,
98
- exitCode: result.status === "pass" ? 0 : 1,
99
- summary: result.status === "pass" ? `[forge.skill.validate] OK — 0 violations` : undefined,
100
- };
92
+ };
93
+
94
+ const skillValidateWrapper = async (
95
+ _input: ForgeCommandInput,
96
+ context: ForgeRuntimeContext,
97
+ ): Promise<ForgeCommandResult> => {
98
+ const result = runSkillValidate({ flags: {} }, context);
99
+ return {
100
+ data: result,
101
+ exitCode: result.status === "pass" ? 0 : 1,
102
+ summary: result.status === "pass" ? `[forge.skill.validate] OK — 0 violations` : undefined,
101
103
  };
102
-
103
- const portValidateWrapper = async (
104
- input: ForgeCommandInput,
105
- context: ForgeRuntimeContext,
106
- ): Promise<ForgeCommandResult> => {
107
- const result = runPortValidate(input, context);
108
- return {
109
- data: result,
110
- exitCode: result.status === "pass" ? 0 : 1,
111
- summary: result.status === "pass" ? `[forge.port.validate] OK — 0 violations` : undefined,
112
- };
104
+ };
105
+
106
+ const portValidateWrapper = async (
107
+ input: ForgeCommandInput,
108
+ context: ForgeRuntimeContext,
109
+ ): Promise<ForgeCommandResult> => {
110
+ const result = runPortValidate(input, context);
111
+ return {
112
+ data: result,
113
+ exitCode: result.status === "pass" ? 0 : 1,
114
+ summary: result.status === "pass" ? `[forge.port.validate] OK — 0 violations` : undefined,
113
115
  };
114
- const skillListWrapper = async (
115
- _input: ForgeCommandInput,
116
- context: ForgeRuntimeContext,
117
- ): Promise<
118
- ForgeCommandResult<{
119
- command: string;
120
- skills: (ForgeSkillEntry | PackSkillEntry)[];
121
- count: number;
122
- }>
123
- > => {
124
- const forgeSkills = FORGE_SKILLS;
125
- // RFC-0539: Include pack skills from declared skillPacks
126
- let packSkills: PackSkillEntry[] = [];
127
- try {
128
- const config = loadForgeConfig(context.workspaceRoot);
129
- packSkills = discoverPackSkills(context.workspaceRoot, config);
130
- } catch {
131
- // Config not found or invalid — skip pack skills
116
+ };
117
+ const skillListWrapper = async (
118
+ _input: ForgeCommandInput,
119
+ context: ForgeRuntimeContext,
120
+ ): Promise<
121
+ ForgeCommandResult<{
122
+ command: string;
123
+ skills: (ForgeSkillEntry | PackSkillEntry)[];
124
+ count: number;
125
+ }>
126
+ > => {
127
+ const forgeSkills = FORGE_SKILLS;
128
+ // RFC-0539: Include pack skills from declared skillPacks
129
+ let packSkills: PackSkillEntry[] = [];
130
+ try {
131
+ const config = loadForgeConfig(context.workspaceRoot);
132
+ packSkills = discoverPackSkills(context.workspaceRoot, config);
133
+ } catch {
134
+ // Config not found or invalid — skip pack skills
135
+ }
136
+ const skills = [...forgeSkills, ...packSkills];
137
+ const result = {
138
+ data: { command: "forge.skill.list", skills, count: skills.length },
139
+ exitCode: 0,
140
+ summary: `forge.skill.list: ${skills.length} skill(s) registered (${forgeSkills.length} forge + ${packSkills.length} pack)`,
141
+ };
142
+ if (context.outputFormat === "pretty") {
143
+ context.logger.section(`Forge Skills (${skills.length})`);
144
+ for (const skill of forgeSkills) {
145
+ context.logger.info(
146
+ `${skill.name} [${skill.category}] — ${skill.invocation}/${skill.concerns}`,
147
+ );
132
148
  }
133
- const skills = [...forgeSkills, ...packSkills];
134
- const result = {
135
- data: { command: "forge.skill.list", skills, count: skills.length },
136
- exitCode: 0,
137
- summary: `forge.skill.list: ${skills.length} skill(s) registered (${forgeSkills.length} forge + ${packSkills.length} pack)`,
138
- };
139
- if (context.outputFormat === "pretty") {
140
- context.logger.section(`Forge Skills (${skills.length})`);
141
- for (const skill of forgeSkills) {
142
- context.logger.info(
143
- `${skill.name} [${skill.category}] — ${skill.invocation}/${skill.concerns}`,
144
- );
145
- }
146
- for (const skill of packSkills) {
147
- context.logger.info(`${skill.name} [pack:${skill.pack}]`);
148
- }
149
+ for (const skill of packSkills) {
150
+ context.logger.info(`${skill.name} [pack:${skill.pack}]`);
149
151
  }
150
- return result;
152
+ }
153
+ return result;
154
+ };
155
+
156
+ const doctorWrapper = async (
157
+ input: ForgeCommandInput,
158
+ context: ForgeRuntimeContext,
159
+ ): Promise<ForgeCommandResult> => {
160
+ const result = await runDoctor(input, context);
161
+ const allPass = result.data?.allPass === true;
162
+ const nextSteps: ForgeNextStep[] = allPass
163
+ ? []
164
+ : [{ action: "Fix the violations above and re-run forge.doctor", kind: "required" }];
165
+ return { ...result, nextSteps };
166
+ };
167
+
168
+ const createWrapper = async (
169
+ input: ForgeCommandInput,
170
+ context: ForgeRuntimeContext,
171
+ ): Promise<ForgeCommandResult> => {
172
+ return runCreate(input, context);
173
+ };
174
+
175
+ const { runInit } = await import("../../src/onboarding/init.ts");
176
+ const initWrapper = async (
177
+ input: ForgeCommandInput,
178
+ context: ForgeRuntimeContext,
179
+ ): Promise<ForgeCommandResult> => {
180
+ const result = runInit(
181
+ {
182
+ flags: input.flags as {
183
+ aiLanguage?: string;
184
+ documentationLanguage?: string;
185
+ from?: string;
186
+ },
187
+ },
188
+ context,
189
+ );
190
+ return {
191
+ data: result,
192
+ exitCode: result.status === "pass" ? 0 : 1,
193
+ summary:
194
+ result.status === "pass"
195
+ ? `[forge.init] OK — ${result.created.length} created, ${result.skipped.length} skipped`
196
+ : undefined,
151
197
  };
152
-
153
- const doctorWrapper = async (
154
- input: ForgeCommandInput,
155
- context: ForgeRuntimeContext,
156
- ): Promise<ForgeCommandResult> => {
157
- const result = await runDoctor(input, context);
158
- const allPass = result.data?.allPass === true;
159
- const nextSteps: ForgeNextStep[] = allPass
198
+ };
199
+
200
+ const profileValidateWrapper = async (
201
+ input: ForgeCommandInput,
202
+ context: ForgeRuntimeContext,
203
+ ): Promise<ForgeCommandResult> => {
204
+ const result = await runProfileValidate(input, context);
205
+ const nextSteps: ForgeNextStep[] =
206
+ result.data?.valid === true
160
207
  ? []
161
- : [{ action: "Fix the violations above and re-run forge.doctor", kind: "required" }];
162
- return { ...result, nextSteps };
163
- };
164
-
165
- registry.registerCommand({
166
- name: "forge.doctor",
167
- description: "Diagnose forge state in an existing project.",
168
- scope: "workspace",
169
- supportsAllSites: false,
170
- flags: {
171
- strict: {
172
- kind: "boolean",
173
- description: "Elevate domain invariant warnings to errors (RFC-0640).",
208
+ : [{ action: "Fix the profile validation errors above", kind: "required" }];
209
+ return { ...result, nextSteps };
210
+ };
211
+
212
+ const { runDeterminismCheck } = await import("./handlers/determinism-check.ts");
213
+
214
+ return {
215
+ name: "forge-core",
216
+ version: "0.1.0",
217
+ runtime: "autonomous",
218
+ declarations: [],
219
+ commands: [
220
+ {
221
+ name: "forge.doctor",
222
+ description: "Diagnose forge state in an existing project.",
223
+ scope: "workspace",
224
+ supportsAllSites: false,
225
+ flags: {
226
+ strict: {
227
+ kind: "boolean",
228
+ description: "Elevate domain invariant warnings to errors (RFC-0640).",
229
+ },
174
230
  },
231
+ cacheable: false,
232
+ execute: doctorWrapper,
175
233
  },
176
- cacheable: false,
177
- execute: doctorWrapper,
178
- });
179
- registry.registerCommand({
180
- name: "forge.port.scaffold",
181
- description: "Generate a skeleton for a new forge skill or command.",
182
- scope: "workspace",
183
- supportsAllSites: false,
184
- flags: {
185
- name: { kind: "string", description: "Skill or command name (kebab-case)." },
186
- type: { kind: "string", description: "Type: 'skill' or 'command' (default: skill)." },
234
+ {
235
+ name: "forge.port.scaffold",
236
+ description: "Generate a skeleton for a new forge skill or command.",
237
+ scope: "workspace",
238
+ supportsAllSites: false,
239
+ flags: {
240
+ name: { kind: "string", description: "Skill or command name (kebab-case)." },
241
+ type: { kind: "string", description: "Type: 'skill' or 'command' (default: skill)." },
242
+ },
243
+ cacheable: false,
244
+ execute: scaffoldWrapper,
187
245
  },
188
- cacheable: false,
189
- execute: scaffoldWrapper,
190
- });
191
- registry.registerCommand({
192
- name: "forge.skill.validate",
193
- contract: "forge",
194
- rules: [],
195
- description:
196
- "Validate all forge skills against frontmatter contract and invariants SKILL-01..SKILL-20.",
197
- scope: "workspace",
198
- supportsAllSites: false,
199
- flags: {},
200
- reads: ["packages/forge/src/**/*.ts", "packages/forge/os/**/*.ts"],
201
- execute: skillValidateWrapper,
202
- });
203
- registry.registerCommand({
204
- name: "forge.skill.list",
205
- description: "List all registered forge skills.",
206
- scope: "workspace",
207
- supportsAllSites: false,
208
- flags: {},
209
- reads: ["packages/forge/src/registry.ts"],
210
- execute: skillListWrapper,
211
- });
212
- registry.registerCommand({
213
- name: "forge.port.validate",
214
- contract: "forge",
215
- rules: [],
216
- description: "Validate a single ported skill or command for compliance with forge contracts.",
217
- scope: "workspace",
218
- supportsAllSites: false,
219
- flags: {
220
- name: { kind: "string", description: "Skill or command name to validate." },
246
+ {
247
+ name: "forge.skill.validate",
248
+ contract: "forge",
249
+ rules: [],
250
+ description:
251
+ "Validate all forge skills against frontmatter contract and invariants SKILL-01..SKILL-20.",
252
+ scope: "workspace",
253
+ supportsAllSites: false,
254
+ flags: {},
255
+ reads: ["packages/forge/src/**/*.ts", "packages/forge/os/**/*.ts"],
256
+ execute: skillValidateWrapper,
221
257
  },
222
- reads: ["packages/forge/src/**/*.ts", "packages/forge/os/**/*.ts"],
223
- execute: portValidateWrapper,
224
- });
225
- registry.registerCommand({
226
- name: "forge.agents.generate",
227
- description: "Regenerate AGENTS.md deterministically from forge.yaml and the skill registry.",
228
- scope: "workspace",
229
- supportsAllSites: false,
230
- flags: {},
231
- cacheable: false,
232
- execute: runAgentsGenerate,
233
- });
234
- // ── forge.upgrade ───────────────────────────────────────────────────────
235
- registry.registerCommand({
236
- name: "forge.upgrade",
237
- description:
238
- "Additive sync for npm consumers: refresh .agents/skills/ from installed forge, add missing binding defaults, update forge.syncedVersion, run doctor.",
239
- scope: "workspace",
240
- supportsAllSites: false,
241
- flags: {
242
- "dry-run": {
243
- kind: "boolean",
244
- description: "Preview what would change without writing files.",
245
- },
246
- "update-npm": {
247
- kind: "boolean",
248
- description:
249
- "Update @warpgogol/forge from npm before syncing. Skipped in monorepo (local package).",
250
- },
258
+ {
259
+ name: "forge.skill.list",
260
+ description: "List all registered forge skills.",
261
+ scope: "workspace",
262
+ supportsAllSites: false,
263
+ flags: {},
264
+ reads: ["packages/forge/src/registry.ts"],
265
+ execute: skillListWrapper,
251
266
  },
252
- writes: [".agents/skills/**", "forge.yaml"],
253
- reads: ["forge.yaml", "packages/forge/skills/**", "packages/forge/package.json"],
254
- cacheable: false,
255
- execute: runUpgrade,
256
- });
257
- registry.registerCommand({
258
- name: "forge.scaffold",
259
- description:
260
- "Create a working pnpm + Turborepo monorepo from a stack profile in an empty directory.",
261
- scope: "workspace",
262
- supportsAllSites: false,
263
- flags: {
264
- profile: {
265
- kind: "string",
266
- required: true,
267
- description: "Stack profile id (e.g. phaser-turborepo).",
268
- },
269
- name: { kind: "string", required: true, description: "Project name (kebab-case)." },
270
- template: {
271
- kind: "string",
272
- required: false,
273
- description: "Template id for multi-template profiles (e.g. react, html).",
274
- },
267
+ {
268
+ name: "forge.port.validate",
269
+ contract: "forge",
270
+ rules: [],
271
+ description:
272
+ "Validate a single ported skill or command for compliance with forge contracts.",
273
+ scope: "workspace",
274
+ supportsAllSites: false,
275
+ flags: {
276
+ name: { kind: "string", description: "Skill or command name to validate." },
277
+ },
278
+ reads: ["packages/forge/src/**/*.ts", "packages/forge/os/**/*.ts"],
279
+ execute: portValidateWrapper,
275
280
  },
276
- cacheable: false,
277
- execute: runScaffoldProject,
278
- });
279
-
280
- // ── forge.create ─────────────────────────────────────────────────────────
281
- const createWrapper = async (
282
- input: ForgeCommandInput,
283
- context: ForgeRuntimeContext,
284
- ): Promise<ForgeCommandResult> => {
285
- return runCreate(input, context);
286
- };
287
- registry.registerCommand({
288
- name: "forge.create",
289
- description:
290
- "Create a forge project in-place: scaffold + init + binding defaults. The target directory must be empty (only .git/ tolerated). Usage: forge create --in-place --profile <profile-id> [--name <name>] [--package-manager pnpm]",
291
- scope: "workspace",
292
- supportsAllSites: false,
293
- flags: {
294
- "in-place": {
295
- kind: "boolean",
296
- required: true,
297
- description: "Scaffold in the current directory (no subdirectory). Required.",
298
- },
299
- profile: {
300
- kind: "string",
301
- required: true,
302
- description:
303
- "Stack profile id. Required. Available: forge-shell, phaser-turborepo, godot-csharp, knowledge-typescript-turborepo, typescript-turborepo.",
304
- },
305
- name: {
306
- kind: "string",
307
- required: false,
308
- description: "Project name override. If omitted, derived from the current folder name.",
309
- },
310
- "package-manager": {
311
- kind: "string",
312
- description: "Package manager to write into forge.yaml (default: pnpm).",
313
- },
314
- template: {
315
- kind: "string",
316
- description: "Template id for multi-template profiles (e.g. react, html).",
317
- },
281
+ {
282
+ name: "forge.agents.generate",
283
+ description:
284
+ "Regenerate AGENTS.md deterministically from forge.yaml and the skill registry.",
285
+ scope: "workspace",
286
+ supportsAllSites: false,
287
+ flags: {},
288
+ cacheable: false,
289
+ execute: runAgentsGenerate,
318
290
  },
319
- cacheable: false,
320
- execute: createWrapper,
321
- });
322
-
323
- // ── forge.init (hidden, for recovery after partial create failure) ────────
324
- const { runInit } = await import("../../src/onboarding/init.ts");
325
- const initWrapper = async (
326
- input: ForgeCommandInput,
327
- context: ForgeRuntimeContext,
328
- ): Promise<ForgeCommandResult> => {
329
- const result = runInit(
330
- {
331
- flags: input.flags as {
332
- aiLanguage?: string;
333
- documentationLanguage?: string;
334
- from?: string;
291
+ {
292
+ name: "forge.upgrade",
293
+ description:
294
+ "Additive sync for npm consumers: refresh .agents/skills/ from installed forge, add missing binding defaults, update forge.syncedVersion, run doctor.",
295
+ scope: "workspace",
296
+ supportsAllSites: false,
297
+ flags: {
298
+ "dry-run": {
299
+ kind: "boolean",
300
+ description: "Preview what would change without writing files.",
301
+ },
302
+ "update-npm": {
303
+ kind: "boolean",
304
+ description:
305
+ "Update @warpgogol/forge from npm before syncing. Skipped in monorepo (local package).",
335
306
  },
336
307
  },
337
- context,
338
- );
339
- return {
340
- data: result,
341
- exitCode: result.status === "pass" ? 0 : 1,
342
- summary:
343
- result.status === "pass"
344
- ? `[forge.init] OK — ${result.created.length} created, ${result.skipped.length} skipped`
345
- : undefined,
346
- };
347
- };
348
- registry.registerCommand({
349
- name: "forge.init",
350
- description:
351
- "Deploy forge into a project: create forge.yaml, PREFERENCES.md, copy skills, create docs dirs. Hidden recovery command — normally called by forge.create.",
352
- scope: "workspace",
353
- supportsAllSites: false,
354
- hidden: true,
355
- flags: {
356
- "ai-language": { kind: "string", description: "AI communication language (default: en)." },
357
- "documentation-language": {
358
- kind: "string",
359
- description: "Documentation language (default: en).",
360
- },
361
- from: { kind: "string", description: "Detect stack from existing project path." },
308
+ writes: [".agents/skills/**", "forge.yaml"],
309
+ reads: ["forge.yaml", "packages/forge/skills/**", "packages/forge/package.json"],
310
+ cacheable: false,
311
+ execute: runUpgrade,
362
312
  },
363
- cacheable: false,
364
- execute: initWrapper,
365
- });
366
-
367
- // ── forge.profile.validate (RFC-0640) ────────────────────────────────────
368
- const profileValidateWrapper = async (
369
- input: ForgeCommandInput,
370
- context: ForgeRuntimeContext,
371
- ): Promise<ForgeCommandResult> => {
372
- const result = await runProfileValidate(input, context);
373
- const nextSteps: ForgeNextStep[] =
374
- result.data?.valid === true
375
- ? []
376
- : [{ action: "Fix the profile validation errors above", kind: "required" }];
377
- return { ...result, nextSteps };
378
- };
379
- registry.registerCommand({
380
- name: "forge.profile.validate",
381
- contract: "forge",
382
- rules: [],
383
- description:
384
- "Validate profile YAML files under packages/forge/profiles/ against the stack-profile schema (RFC-0640).",
385
- scope: "workspace",
386
- supportsAllSites: false,
387
- flags: {
388
- id: {
389
- kind: "string",
390
- description: "Validate only the profile with this id.",
313
+ {
314
+ name: "forge.scaffold",
315
+ description:
316
+ "Create a working pnpm + Turborepo monorepo from a stack profile in an empty directory.",
317
+ scope: "workspace",
318
+ supportsAllSites: false,
319
+ flags: {
320
+ profile: {
321
+ kind: "string",
322
+ required: true,
323
+ description: "Stack profile id (e.g. phaser-turborepo).",
324
+ },
325
+ name: { kind: "string", required: true, description: "Project name (kebab-case)." },
326
+ template: {
327
+ kind: "string",
328
+ required: false,
329
+ description: "Template id for multi-template profiles (e.g. react, html).",
330
+ },
391
331
  },
332
+ cacheable: false,
333
+ execute: runScaffoldProject,
392
334
  },
393
- reads: ["packages/forge/profiles/*.yaml"],
394
- cacheable: false,
395
- execute: profileValidateWrapper,
396
- });
397
-
398
- // ── forge.dev (RFC-0674) ──────────────────────────────────────────────────
399
- registry.registerCommand({
400
- name: "forge.dev",
401
- description:
402
- "Start the dev/preview server declared in the active stack profile. Use --dry-run to print the resolved command without executing.",
403
- scope: "workspace",
404
- supportsAllSites: false,
405
- longRunning: true,
406
- flags: {
407
- "dry-run": {
408
- kind: "boolean",
409
- description: "Print the resolved dev server command without executing.",
410
- },
411
- profile: {
412
- kind: "string",
413
- description: "Override the active profile id.",
335
+ {
336
+ name: "forge.create",
337
+ description:
338
+ "Create a forge project in-place: scaffold + init + binding defaults. The target directory must be empty (only .git/ tolerated). Usage: forge create --in-place --profile <profile-id> [--name <name>] [--package-manager pnpm]",
339
+ scope: "workspace",
340
+ supportsAllSites: false,
341
+ flags: {
342
+ "in-place": {
343
+ kind: "boolean",
344
+ required: true,
345
+ description: "Scaffold in the current directory (no subdirectory). Required.",
346
+ },
347
+ profile: {
348
+ kind: "string",
349
+ required: true,
350
+ description:
351
+ "Stack profile id. Required. Available: forge-shell, phaser-turborepo, godot-csharp, knowledge-typescript-turborepo, typescript-turborepo.",
352
+ },
353
+ name: {
354
+ kind: "string",
355
+ required: false,
356
+ description: "Project name override. If omitted, derived from the current folder name.",
357
+ },
358
+ "package-manager": {
359
+ kind: "string",
360
+ description: "Package manager to write into forge.yaml (default: pnpm).",
361
+ },
362
+ template: {
363
+ kind: "string",
364
+ description: "Template id for multi-template profiles (e.g. react, html).",
365
+ },
414
366
  },
367
+ cacheable: false,
368
+ execute: createWrapper,
415
369
  },
416
- reads: ["forge.yaml", "packages/forge/profiles/*.yaml"],
417
- cacheable: false,
418
- execute: runDev,
419
- });
420
-
421
- // ── forge.build (RFC-0674) ────────────────────────────────────────────────
422
- registry.registerCommand({
423
- name: "forge.build",
424
- description:
425
- "Execute produce commands for all artifacts declared in the active stack profile. Use --dry-run to print resolved commands.",
426
- scope: "workspace",
427
- supportsAllSites: false,
428
- flags: {
429
- "dry-run": {
430
- kind: "boolean",
431
- description: "Print resolved produce commands without executing.",
432
- },
433
- profile: {
434
- kind: "string",
435
- description: "Override the active profile id.",
370
+ {
371
+ name: "forge.init",
372
+ description:
373
+ "Deploy forge into a project: create forge.yaml, PREFERENCES.md, copy skills, create docs dirs. Hidden recovery command — normally called by forge.create.",
374
+ scope: "workspace",
375
+ supportsAllSites: false,
376
+ hidden: true,
377
+ flags: {
378
+ "ai-language": {
379
+ kind: "string",
380
+ description: "AI communication language (default: en).",
381
+ },
382
+ "documentation-language": {
383
+ kind: "string",
384
+ description: "Documentation language (default: en).",
385
+ },
386
+ from: { kind: "string", description: "Detect stack from existing project path." },
436
387
  },
388
+ cacheable: false,
389
+ execute: initWrapper,
437
390
  },
438
- reads: ["forge.yaml", "packages/forge/profiles/*.yaml"],
439
- cacheable: false,
440
- execute: runBuild,
441
- });
442
-
443
- // ── forge.validate (RFC-0674, RFC-0677) ───────────────────────────────────
444
- registry.registerCommand({
445
- name: "forge.validate",
446
- contract: "forge",
447
- rules: [],
448
- description:
449
- "Execute validate commands for all artifacts declared in the active stack profile. Use --dry-run to print resolved commands. Use --artifact to validate a single artifact.",
450
- scope: "workspace",
451
- supportsAllSites: false,
452
- flags: {
453
- "dry-run": {
454
- kind: "boolean",
455
- description: "Print resolved validate commands without executing.",
456
- },
457
- profile: {
458
- kind: "string",
459
- description: "Override the active profile id.",
460
- },
461
- artifact: {
462
- kind: "string",
463
- description: "Validate only the specified artifact id.",
391
+ {
392
+ name: "forge.profile.validate",
393
+ contract: "forge",
394
+ rules: [],
395
+ description:
396
+ "Validate profile YAML files under packages/forge/profiles/ against the stack-profile schema (RFC-0640).",
397
+ scope: "workspace",
398
+ supportsAllSites: false,
399
+ flags: {
400
+ id: {
401
+ kind: "string",
402
+ description: "Validate only the profile with this id.",
403
+ },
464
404
  },
405
+ reads: ["packages/forge/profiles/*.yaml"],
406
+ cacheable: false,
407
+ execute: profileValidateWrapper,
465
408
  },
466
- reads: ["forge.yaml", "packages/forge/profiles/*.yaml"],
467
- cacheable: false,
468
- execute: runValidate,
469
- });
470
-
471
- // ── forge.determinism.check (RFC-0678) ─────────────────────────────────────
472
- const { runDeterminismCheck } = await import("./handlers/determinism-check.ts");
473
- registry.registerCommand({
474
- name: "forge.determinism.check",
475
- contract: "forge",
476
- rules: [],
477
- description:
478
- "Verify artifact determinism by building twice and comparing output hashes. Reads determinism.inputs glob patterns from the active stack profile. Use --dry-run to print resolved inputs, --artifact to check a single artifact.",
479
- scope: "workspace",
480
- supportsAllSites: false,
481
- flags: {
482
- "dry-run": {
483
- kind: "boolean",
484
- description: "Print resolved determinism inputs without executing builds.",
485
- },
486
- profile: {
487
- kind: "string",
488
- description: "Override the active profile id.",
489
- },
490
- artifact: {
491
- kind: "string",
492
- description: "Check only the specified artifact id.",
409
+ {
410
+ name: "forge.dev",
411
+ description:
412
+ "Start the dev/preview server declared in the active stack profile. Use --dry-run to print the resolved command without executing.",
413
+ scope: "workspace",
414
+ supportsAllSites: false,
415
+ longRunning: true,
416
+ flags: {
417
+ "dry-run": {
418
+ kind: "boolean",
419
+ description: "Print the resolved dev server command without executing.",
420
+ },
421
+ profile: {
422
+ kind: "string",
423
+ description: "Override the active profile id.",
424
+ },
493
425
  },
426
+ reads: ["forge.yaml", "packages/forge/profiles/*.yaml"],
427
+ cacheable: false,
428
+ execute: runDev,
494
429
  },
495
- reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "dist/.determinism-cache.json"],
496
- writes: ["dist/.determinism-cache.json"],
497
- cacheable: false,
498
- execute: runDeterminismCheck,
499
- });
500
-
501
- // ── forge.assets.list (RFC-0679) ───────────────────────────────────────────
502
- const { runAssetsList } = await import("./handlers/assets-list.ts");
503
- registry.registerCommand({
504
- name: "forge.assets.list",
505
- description:
506
- "List all assets declared in the active stack profile, grouped by type. Use --dry-run to skip hashing, --type to filter by asset type.",
507
- scope: "workspace",
508
- supportsAllSites: false,
509
- flags: {
510
- "dry-run": {
511
- kind: "boolean",
512
- description: "List assets without computing hashes.",
513
- },
514
- profile: {
515
- kind: "string",
516
- description: "Override the active profile id.",
517
- },
518
- type: {
519
- kind: "string",
520
- description: "Filter assets by type id (e.g. video, audio, image).",
430
+ {
431
+ name: "forge.build",
432
+ description:
433
+ "Execute produce commands for all artifacts declared in the active stack profile. Use --dry-run to print resolved commands.",
434
+ scope: "workspace",
435
+ supportsAllSites: false,
436
+ flags: {
437
+ "dry-run": {
438
+ kind: "boolean",
439
+ description: "Print resolved produce commands without executing.",
440
+ },
441
+ profile: {
442
+ kind: "string",
443
+ description: "Override the active profile id.",
444
+ },
521
445
  },
446
+ reads: ["forge.yaml", "packages/forge/profiles/*.yaml"],
447
+ cacheable: false,
448
+ execute: runBuild,
522
449
  },
523
- reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "assets/**"],
524
- cacheable: false,
525
- execute: runAssetsList,
526
- });
527
-
528
- // ── forge.assets.check (RFC-0679) ──────────────────────────────────────────
529
- const { runAssetsCheck } = await import("./handlers/assets-check.ts");
530
- registry.registerCommand({
531
- name: "forge.assets.check",
532
- contract: "forge",
533
- rules: [],
534
- description:
535
- "Check for missing, orphaned, and unreferenced assets. Use --strict to fail on orphaned assets, --dry-run to skip hashing.",
536
- scope: "workspace",
537
- supportsAllSites: false,
538
- flags: {
539
- "dry-run": {
540
- kind: "boolean",
541
- description: "Check file existence without computing hashes.",
542
- },
543
- strict: {
544
- kind: "boolean",
545
- description: "Exit non-zero when orphaned assets are found.",
546
- },
547
- profile: {
548
- kind: "string",
549
- description: "Override the active profile id.",
450
+ {
451
+ name: "forge.validate",
452
+ contract: "forge",
453
+ rules: [],
454
+ description:
455
+ "Execute validate commands for all artifacts declared in the active stack profile. Use --dry-run to print resolved commands. Use --artifact to validate a single artifact.",
456
+ scope: "workspace",
457
+ supportsAllSites: false,
458
+ flags: {
459
+ "dry-run": {
460
+ kind: "boolean",
461
+ description: "Print resolved validate commands without executing.",
462
+ },
463
+ profile: {
464
+ kind: "string",
465
+ description: "Override the active profile id.",
466
+ },
467
+ artifact: {
468
+ kind: "string",
469
+ description: "Validate only the specified artifact id.",
470
+ },
550
471
  },
472
+ reads: ["forge.yaml", "packages/forge/profiles/*.yaml"],
473
+ cacheable: false,
474
+ execute: runValidate,
551
475
  },
552
- reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "assets/**"],
553
- cacheable: false,
554
- execute: runAssetsCheck,
555
- });
556
-
557
- // ── forge.release.prepare (RFC-0680) ────────────────────────────────────────
558
- const { runReleasePrepare } = await import("./handlers/release-prepare.ts");
559
- registry.registerCommand({
560
- name: "forge.release.prepare",
561
- description:
562
- "Bundle built artifacts into a release package with a manifest. Use --dry-run to preview without writing.",
563
- scope: "workspace",
564
- supportsAllSites: false,
565
- flags: {
566
- "dry-run": {
567
- kind: "boolean",
568
- description: "Print the resolved release steps without executing.",
569
- },
570
- profile: {
571
- kind: "string",
572
- description: "Override the active profile id.",
476
+ {
477
+ name: "forge.determinism.check",
478
+ contract: "forge",
479
+ rules: [],
480
+ description:
481
+ "Verify artifact determinism by building twice and comparing output hashes. Reads determinism.inputs glob patterns from the active stack profile. Use --dry-run to print resolved inputs, --artifact to check a single artifact.",
482
+ scope: "workspace",
483
+ supportsAllSites: false,
484
+ flags: {
485
+ "dry-run": {
486
+ kind: "boolean",
487
+ description: "Print resolved determinism inputs without executing builds.",
488
+ },
489
+ profile: {
490
+ kind: "string",
491
+ description: "Override the active profile id.",
492
+ },
493
+ artifact: {
494
+ kind: "string",
495
+ description: "Check only the specified artifact id.",
496
+ },
573
497
  },
498
+ reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "dist/.determinism-cache.json"],
499
+ writes: ["dist/.determinism-cache.json"],
500
+ cacheable: false,
501
+ execute: runDeterminismCheck,
574
502
  },
575
- reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "dist/**"],
576
- writes: ["release/**"],
577
- cacheable: false,
578
- execute: runReleasePrepare,
579
- });
580
-
581
- // ── forge.release.publish (RFC-0680) ───────────────────────────────────────
582
- const { runReleasePublish } = await import("./handlers/release-publish.ts");
583
- registry.registerCommand({
584
- name: "forge.release.publish",
585
- description:
586
- "Publish a prepared release to the declared target (local, R2, S3). Use --dry-run to preview without uploading.",
587
- scope: "workspace",
588
- supportsAllSites: false,
589
- flags: {
590
- "dry-run": {
591
- kind: "boolean",
592
- description: "Print the resolved publish target and file list without uploading.",
593
- },
594
- profile: {
595
- kind: "string",
596
- description: "Override the active profile id.",
503
+ {
504
+ name: "forge.assets.list",
505
+ description:
506
+ "List all assets declared in the active stack profile, grouped by type. Use --dry-run to skip hashing, --type to filter by asset type.",
507
+ scope: "workspace",
508
+ supportsAllSites: false,
509
+ flags: {
510
+ "dry-run": {
511
+ kind: "boolean",
512
+ description: "List assets without computing hashes.",
513
+ },
514
+ profile: {
515
+ kind: "string",
516
+ description: "Override the active profile id.",
517
+ },
518
+ type: {
519
+ kind: "string",
520
+ description: "Filter assets by type id (e.g. video, audio, image).",
521
+ },
597
522
  },
523
+ reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "assets/**"],
524
+ cacheable: false,
525
+ execute: runAssetsList,
598
526
  },
599
- reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "release/**"],
600
- writes: ["release/published/**"],
601
- cacheable: false,
602
- execute: runReleasePublish,
603
- });
604
-
605
- // ── forge.skill.knowledge.compact (RFC-0662) ──────────────────────────────
606
- const { runKnowledgeCompact } = await import("./handlers/knowledge-compact.ts");
607
- registry.registerCommand({
608
- name: "forge.skill.knowledge.compact",
609
- description:
610
- "Compact skill knowledge files: archive expired/superseded/aged L0 entries, mark stale L2 principles. " +
611
- "Usage: forge.skill.knowledge.compact --all-skills [--dry-run] [--json] | --skill <name> [--dry-run] [--json]",
612
- scope: "workspace",
613
- supportsAllSites: false,
614
- flags: {
615
- skill: {
616
- kind: "string",
617
- description:
618
- "Compact a single skill's declared knowledge files. Mutually exclusive with --all-skills.",
619
- },
620
- "all-skills": {
621
- kind: "boolean",
622
- description:
623
- "Compact all forge and pack skills declaring knowledge: files. (Named --all-skills because the CLI reserves --all.)",
624
- },
625
- "dry-run": {
626
- kind: "boolean",
627
- description: "Report planned mutations; write nothing.",
628
- },
629
- json: {
630
- kind: "boolean",
631
- description: "Machine-readable report.",
632
- },
633
- "retention-days": {
634
- kind: "string",
635
- description: "L0 entries older than this many days are archived (default: 90).",
636
- },
637
- "stale-days": {
638
- kind: "string",
639
- description:
640
- "L2 entries with lastConfirmedAt older than this become stale (default: 90).",
527
+ {
528
+ name: "forge.assets.check",
529
+ contract: "forge",
530
+ rules: [],
531
+ description:
532
+ "Check for missing, orphaned, and unreferenced assets. Use --strict to fail on orphaned assets, --dry-run to skip hashing.",
533
+ scope: "workspace",
534
+ supportsAllSites: false,
535
+ flags: {
536
+ "dry-run": {
537
+ kind: "boolean",
538
+ description: "Check file existence without computing hashes.",
539
+ },
540
+ strict: {
541
+ kind: "boolean",
542
+ description: "Exit non-zero when orphaned assets are found.",
543
+ },
544
+ profile: {
545
+ kind: "string",
546
+ description: "Override the active profile id.",
547
+ },
641
548
  },
549
+ reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "assets/**"],
550
+ cacheable: false,
551
+ execute: runAssetsCheck,
642
552
  },
643
- writes: ["packages/forge/skills/**/*.md", "packages/forge/skills/**/*.archive.md"],
644
- reads: [
645
- "packages/forge/skills/**/*.md",
646
- "packages/forge/skills/**/*.archive.md",
647
- "forge.yaml",
648
- ],
649
- cacheable: false,
650
- execute: runKnowledgeCompact,
651
- });
652
-
653
- // ── pinned.validate ─────────────────────────────────────────────────────
654
- registry.registerCommand({
655
- name: "pinned.validate",
656
- contract: "pinned",
657
- rules: [],
658
- description:
659
- "Validate working tree against .forge/pinned.yaml manifest. " +
660
- "Checks staged changes (default) or last commit (--mode ci) for " +
661
- "delete/move/modify operations on pinned files. " +
662
- "Use --allow-pinned-override <path> for audited escape hatch.",
663
- scope: "workspace",
664
- reads: [".forge/pinned.yaml", ".forge/pinned-audit.log"],
665
- writes: [".forge/pinned-audit.log"],
666
- cacheable: false,
667
- flags: {
668
- "allow-pinned-override": {
669
- kind: "string[]",
670
- description:
671
- "Path(s) to exempt from pinned-files validation on this invocation. " +
672
- "Each override is logged to .forge/pinned-audit.log.",
673
- },
674
- mode: {
675
- kind: "string",
676
- description:
677
- "Validation mode: 'staged' (pre-commit, default) or 'ci' (last-commit diff).",
553
+ {
554
+ name: "forge.release.prepare",
555
+ description:
556
+ "Bundle built artifacts into a release package with a manifest. Use --dry-run to preview without writing.",
557
+ scope: "workspace",
558
+ supportsAllSites: false,
559
+ flags: {
560
+ "dry-run": {
561
+ kind: "boolean",
562
+ description: "Print the resolved release steps without executing.",
563
+ },
564
+ profile: {
565
+ kind: "string",
566
+ description: "Override the active profile id.",
567
+ },
678
568
  },
679
- json: {
680
- kind: "boolean",
681
- description: "Output structured JSON result.",
569
+ reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "dist/**"],
570
+ writes: ["release/**"],
571
+ cacheable: false,
572
+ execute: runReleasePrepare,
573
+ },
574
+ {
575
+ name: "forge.release.publish",
576
+ description:
577
+ "Publish a prepared release to the declared target (local, R2, S3). Use --dry-run to preview without uploading.",
578
+ scope: "workspace",
579
+ supportsAllSites: false,
580
+ flags: {
581
+ "dry-run": {
582
+ kind: "boolean",
583
+ description: "Print the resolved publish target and file list without uploading.",
584
+ },
585
+ profile: {
586
+ kind: "string",
587
+ description: "Override the active profile id.",
588
+ },
682
589
  },
590
+ reads: ["forge.yaml", "packages/forge/profiles/*.yaml", "release/**"],
591
+ writes: ["release/published/**"],
592
+ cacheable: false,
593
+ execute: runReleasePublish,
683
594
  },
684
- execute: runPinnedValidate,
685
- });
686
-
687
- // ── pinned.init ─────────────────────────────────────────────────────────
688
- registry.registerCommand({
689
- name: "pinned.init",
690
- description:
691
- "Initialize .forge/pinned.yaml with default foundation entries, " +
692
- "install pre-commit hook, and add audit log to .gitignore. " +
693
- "Use --ci to also generate .github/workflows/pinned-check.yml.",
694
- scope: "workspace",
695
- mutatesState: true,
696
- writes: [
697
- ".forge/pinned.yaml",
698
- ".git/hooks/pre-commit",
699
- ".gitignore",
700
- ".github/workflows/pinned-check.yml",
701
- ],
702
- reads: [".forge/pinned.yaml", ".git/hooks/pre-commit", ".gitignore"],
703
- cacheable: false,
704
- flags: {
705
- ci: {
706
- kind: "boolean",
707
- description: "Also generate .github/workflows/pinned-check.yml CI workflow.",
595
+ {
596
+ name: "forge.skill.knowledge.compact",
597
+ description:
598
+ "Compact skill knowledge files: archive expired/superseded/aged L0 entries, mark stale L2 principles. " +
599
+ "Usage: forge.skill.knowledge.compact --all-skills [--dry-run] [--json] | --skill <name> [--dry-run] [--json]",
600
+ scope: "workspace",
601
+ supportsAllSites: false,
602
+ flags: {
603
+ skill: {
604
+ kind: "string",
605
+ description:
606
+ "Compact a single skill's declared knowledge files. Mutually exclusive with --all-skills.",
607
+ },
608
+ "all-skills": {
609
+ kind: "boolean",
610
+ description:
611
+ "Compact all forge and pack skills declaring knowledge: files. (Named --all-skills because the CLI reserves --all.)",
612
+ },
613
+ "dry-run": {
614
+ kind: "boolean",
615
+ description: "Report planned mutations; write nothing.",
616
+ },
617
+ json: {
618
+ kind: "boolean",
619
+ description: "Machine-readable report.",
620
+ },
621
+ "retention-days": {
622
+ kind: "string",
623
+ description: "L0 entries older than this many days are archived (default: 90).",
624
+ },
625
+ "stale-days": {
626
+ kind: "string",
627
+ description:
628
+ "L2 entries with lastConfirmedAt older than this become stale (default: 90).",
629
+ },
708
630
  },
631
+ writes: ["packages/forge/skills/**/*.md", "packages/forge/skills/**/*.archive.md"],
632
+ reads: [
633
+ "packages/forge/skills/**/*.md",
634
+ "packages/forge/skills/**/*.archive.md",
635
+ "forge.yaml",
636
+ ],
637
+ cacheable: false,
638
+ execute: runKnowledgeCompact,
709
639
  },
710
- execute: runPinnedInit,
711
- });
712
-
713
- // ── forge.package.health ───────────────────────────────────────────────────
714
- const { runPackageHealth } = await import("./handlers/package-health.ts");
715
- registry.registerCommand({
716
- name: "forge.package.health",
717
- description:
718
- "Validate all published packages (private: false) for standalone extraction readiness. " +
719
- "Checks engines.node, embedded .github/workflows/ci.yml, extract.config.yaml, " +
720
- "and devDependencies completeness (no hoisted tool deps). " +
721
- "Exits non-zero on errors. Use --json for machine-readable output.",
722
- scope: "workspace",
723
- supportsAllSites: false,
724
- reads: [
725
- "packages/*/package.json",
726
- "packages/*/.github/workflows/ci.yml",
727
- "packages/*/extract.config.yaml",
728
- ],
729
- cacheable: false,
730
- flags: {
731
- json: {
732
- kind: "boolean",
733
- description: "Output structured JSON result.",
640
+ {
641
+ name: "pinned.validate",
642
+ contract: "pinned",
643
+ rules: [],
644
+ description:
645
+ "Validate working tree against .forge/pinned.yaml manifest. " +
646
+ "Checks staged changes (default) or last commit (--mode ci) for " +
647
+ "delete/move/modify operations on pinned files. " +
648
+ "Use --allow-pinned-override <path> for audited escape hatch.",
649
+ scope: "workspace",
650
+ reads: [".forge/pinned.yaml", ".forge/pinned-audit.log"],
651
+ writes: [".forge/pinned-audit.log"],
652
+ cacheable: false,
653
+ flags: {
654
+ "allow-pinned-override": {
655
+ kind: "string[]",
656
+ description:
657
+ "Path(s) to exempt from pinned-files validation on this invocation. " +
658
+ "Each override is logged to .forge/pinned-audit.log.",
659
+ },
660
+ mode: {
661
+ kind: "string",
662
+ description:
663
+ "Validation mode: 'staged' (pre-commit, default) or 'ci' (last-commit diff).",
664
+ },
665
+ json: {
666
+ kind: "boolean",
667
+ description: "Output structured JSON result.",
668
+ },
734
669
  },
670
+ execute: runPinnedValidate,
735
671
  },
736
- execute: runPackageHealth,
737
- });
738
-
739
- // ── forge.autonomy.validate (RFC-0940) ─────────────────────────────────────
740
- const { runForgeAutonomyValidate } = await import("./handlers/forge-autonomy-validate.ts");
741
- registry.registerCommand({
742
- name: "forge.autonomy.validate",
743
- contract: "forge",
744
- rules: [],
745
- description:
746
- "Scan packages/forge/os/** for forbidden @warpgogol/werkstatt-engine imports outside os/werkstatt/. " +
747
- "Enforces FORGE-AUTONOMY-01 (RFC-0940, DNA-64). @warpgogol/werkstatt-shared is exempt. " +
748
- "Type-only imports (import type) are exempt.",
749
- scope: "workspace",
750
- supportsAllSites: false,
751
- reads: ["packages/forge/os/**/*.ts"],
752
- cacheable: false,
753
- flags: {},
754
- execute: runForgeAutonomyValidate,
755
- });
756
-
757
- // ── docs.archive ─────────────────────────────────────────────────────────
758
- registry.registerCommand({
759
- name: "docs.archive",
760
- description:
761
- "Umbrella command that runs rfc.archive, adr.archive, plan.archive, " +
762
- "audit.archive, session.archive, and mission.archive in sequence. " +
763
- "Passes --dry-run and --status through to all sub-commands. Not atomic " +
764
- "— if one sub-command fails, prior moves are not rolled back. " +
765
- "Re-running is safe (idempotent).",
766
- scope: "workspace",
767
- mutatesState: true,
768
- writes: [
769
- "docs/rfcs/*.md",
770
- "docs/rfcs/archive/**",
771
- "docs/adrs/*.md",
772
- "docs/adrs/archive/**",
773
- "docs/plans/*.md",
774
- "docs/plans/archive/**",
775
- "docs/audits/*.md",
776
- "docs/audits/archive/**",
777
- "docs/sessions/*.md",
778
- "docs/sessions/archive/**",
779
- "missions/*",
780
- "missions/archive/**",
781
- ],
782
- reads: [
783
- "docs/rfcs/**/*.md",
784
- "docs/adrs/**/*.md",
785
- "docs/plans/**/*.md",
786
- "docs/audits/**/*.md",
787
- "docs/sessions/**/*.md",
788
- "missions/**",
789
- ],
790
- cacheable: false,
791
- flags: {
792
- "dry-run": {
793
- kind: "boolean",
794
- description: "Preview what would be moved without touching the filesystem.",
672
+ {
673
+ name: "pinned.init",
674
+ description:
675
+ "Initialize .forge/pinned.yaml with default foundation entries, " +
676
+ "install pre-commit hook, and add audit log to .gitignore. " +
677
+ "Use --ci to also generate .github/workflows/pinned-check.yml.",
678
+ scope: "workspace",
679
+ mutatesState: true,
680
+ writes: [
681
+ ".forge/pinned.yaml",
682
+ ".git/hooks/pre-commit",
683
+ ".gitignore",
684
+ ".github/workflows/pinned-check.yml",
685
+ ],
686
+ reads: [".forge/pinned.yaml", ".git/hooks/pre-commit", ".gitignore"],
687
+ cacheable: false,
688
+ flags: {
689
+ ci: {
690
+ kind: "boolean",
691
+ description: "Also generate .github/workflows/pinned-check.yml CI workflow.",
692
+ },
795
693
  },
796
- status: {
797
- kind: "string",
798
- description: "Filter to a single terminal status (implemented, rejected, superseded).",
694
+ execute: runPinnedInit,
695
+ },
696
+ {
697
+ name: "forge.package.health",
698
+ description:
699
+ "Validate all published packages (private: false) for standalone extraction readiness. " +
700
+ "Checks engines.node, embedded .github/workflows/ci.yml, extract.config.yaml, " +
701
+ "and devDependencies completeness (no hoisted tool deps). " +
702
+ "Exits non-zero on errors. Use --json for machine-readable output.",
703
+ scope: "workspace",
704
+ supportsAllSites: false,
705
+ reads: [
706
+ "packages/*/package.json",
707
+ "packages/*/.github/workflows/ci.yml",
708
+ "packages/*/extract.config.yaml",
709
+ ],
710
+ cacheable: false,
711
+ flags: {
712
+ json: {
713
+ kind: "boolean",
714
+ description: "Output structured JSON result.",
715
+ },
799
716
  },
717
+ execute: runPackageHealth,
800
718
  },
801
- execute: async (input, context) => {
802
- const { logger, outputFormat } = context;
803
- const dryRun = context.dryRun || input.flags["dry-run"] === true;
804
- const results: Record<string, unknown> = {};
805
- let totalMoved = 0;
806
- let totalSkipped = 0;
807
-
808
- const { runRfcArchive } = await import("../rfc/handlers/archive.ts");
809
- const { runAdrArchive } = await import("../adr/handlers/archive.ts");
810
- const { runPlanArchive } = await import("../plan/handlers/archive.ts");
811
- const { runAuditArchive } = await import("../audit/handlers/archive.ts");
812
- const { runSessionArchive } = await import("../session/handlers/archive.ts");
813
- const { runMissionArchive } = await import("../mission/handlers/archive.ts");
814
-
815
- type ArchiveHandler = (
816
- input: ForgeCommandInput,
817
- context: ForgeRuntimeContext,
818
- ) => Promise<ForgeCommandResult>;
819
-
820
- const subCommands: Array<{ name: string; fn: ArchiveHandler }> = [
821
- { name: "rfc.archive", fn: runRfcArchive as ArchiveHandler },
822
- { name: "adr.archive", fn: runAdrArchive as ArchiveHandler },
823
- { name: "plan.archive", fn: runPlanArchive as ArchiveHandler },
824
- { name: "audit.archive", fn: runAuditArchive as ArchiveHandler },
825
- { name: "session.archive", fn: runSessionArchive as ArchiveHandler },
826
- { name: "mission.archive", fn: runMissionArchive as ArchiveHandler },
827
- ];
828
-
829
- for (const { name: cmdName, fn } of subCommands) {
830
- try {
831
- const result = await fn(input, context);
832
- const data = (result as { data?: { moved?: unknown[]; skipped?: unknown[] } })?.data;
833
- if (data) {
834
- totalMoved += Array.isArray(data.moved) ? data.moved.length : 0;
835
- totalSkipped += Array.isArray(data.skipped) ? data.skipped.length : 0;
719
+ {
720
+ name: "forge.autonomy.validate",
721
+ contract: "forge",
722
+ rules: [],
723
+ description:
724
+ "Scan packages/forge/os/** for forbidden @warpgogol/werkstatt-engine imports outside os/werkstatt/. " +
725
+ "Enforces FORGE-AUTONOMY-01 (RFC-0940, DNA-64). @warpgogol/werkstatt-shared is exempt. " +
726
+ "Type-only imports (import type) are exempt.",
727
+ scope: "workspace",
728
+ supportsAllSites: false,
729
+ reads: ["packages/forge/os/**/*.ts"],
730
+ cacheable: false,
731
+ flags: {},
732
+ execute: runForgeAutonomyValidate,
733
+ },
734
+ {
735
+ name: "docs.archive",
736
+ description:
737
+ "Umbrella command that runs rfc.archive, adr.archive, plan.archive, " +
738
+ "audit.archive, session.archive, and mission.archive in sequence. " +
739
+ "Passes --dry-run and --status through to all sub-commands. Not atomic " +
740
+ "— if one sub-command fails, prior moves are not rolled back. " +
741
+ "Re-running is safe (idempotent).",
742
+ scope: "workspace",
743
+ mutatesState: true,
744
+ writes: [
745
+ "docs/rfcs/*.md",
746
+ "docs/rfcs/archive/**",
747
+ "docs/adrs/*.md",
748
+ "docs/adrs/archive/**",
749
+ "docs/plans/*.md",
750
+ "docs/plans/archive/**",
751
+ "docs/audits/*.md",
752
+ "docs/audits/archive/**",
753
+ "docs/sessions/*.md",
754
+ "docs/sessions/archive/**",
755
+ "missions/*",
756
+ "missions/archive/**",
757
+ ],
758
+ reads: [
759
+ "docs/rfcs/**/*.md",
760
+ "docs/adrs/**/*.md",
761
+ "docs/plans/**/*.md",
762
+ "docs/audits/**/*.md",
763
+ "docs/sessions/**/*.md",
764
+ "missions/**",
765
+ ],
766
+ cacheable: false,
767
+ flags: {
768
+ "dry-run": {
769
+ kind: "boolean",
770
+ description: "Preview what would be moved without touching the filesystem.",
771
+ },
772
+ status: {
773
+ kind: "string",
774
+ description: "Filter to a single terminal status (implemented, rejected, superseded).",
775
+ },
776
+ },
777
+ execute: async (input, context) => {
778
+ const { logger, outputFormat } = context;
779
+ const dryRun = context.dryRun || input.flags["dry-run"] === true;
780
+ const results: Record<string, unknown> = {};
781
+ let totalMoved = 0;
782
+ let totalSkipped = 0;
783
+
784
+ const { runRfcArchive } = await import("../rfc/handlers/archive.ts");
785
+ const { runAdrArchive } = await import("../adr/handlers/archive.ts");
786
+ const { runPlanArchive } = await import("../plan/handlers/archive.ts");
787
+ const { runAuditArchive } = await import("../audit/handlers/archive.ts");
788
+ const { runSessionArchive } = await import("../session/handlers/archive.ts");
789
+ const { runMissionArchive } = await import("../mission/handlers/archive.ts");
790
+
791
+ type ArchiveHandler = (
792
+ input: ForgeCommandInput,
793
+ context: ForgeRuntimeContext,
794
+ ) => Promise<ForgeCommandResult>;
795
+
796
+ const subCommands: Array<{ name: string; fn: ArchiveHandler }> = [
797
+ { name: "rfc.archive", fn: runRfcArchive as ArchiveHandler },
798
+ { name: "adr.archive", fn: runAdrArchive as ArchiveHandler },
799
+ { name: "plan.archive", fn: runPlanArchive as ArchiveHandler },
800
+ { name: "audit.archive", fn: runAuditArchive as ArchiveHandler },
801
+ { name: "session.archive", fn: runSessionArchive as ArchiveHandler },
802
+ { name: "mission.archive", fn: runMissionArchive as ArchiveHandler },
803
+ ];
804
+
805
+ for (const { name: cmdName, fn } of subCommands) {
806
+ try {
807
+ const result = await fn(input, context);
808
+ const data = (result as { data?: { moved?: unknown[]; skipped?: unknown[] } })?.data;
809
+ if (data) {
810
+ totalMoved += Array.isArray(data.moved) ? data.moved.length : 0;
811
+ totalSkipped += Array.isArray(data.skipped) ? data.skipped.length : 0;
812
+ }
813
+ results[cmdName] = data ?? result;
814
+ } catch (err) {
815
+ results[cmdName] = { error: String((err as Error).message) };
836
816
  }
837
- results[cmdName] = data ?? result;
838
- } catch (err) {
839
- results[cmdName] = { error: String((err as Error).message) };
840
817
  }
841
- }
842
-
843
- // ── RFC-0711: post-loop spec.live.merge for implemented RFCs with liveSpec ──
844
- const { listRfcFiles, readAndParseRfc } = await import("../rfc/frontmatter-io.ts");
845
- const { RFC_DIR } = await import("../rfc/types.ts");
846
- const { runSpecLiveMerge } = await import("../spec/live-spec-merge.ts");
847
- const rfcDirPath = path.join(context.workspaceRoot, RFC_DIR);
848
- const statusFilter = input.flags["status"] as string | undefined;
849
- const allRfcFiles = await listRfcFiles(rfcDirPath);
850
- const liveMergeResults: Array<{
851
- id: string;
852
- domain: string;
853
- operation: string;
854
- conflicts: number;
855
- }> = [];
856
- let liveMergeSkipped = 0;
857
-
858
- for (const rfcFile of allRfcFiles) {
859
- const parsed = await readAndParseRfc(rfcDirPath, rfcFile);
860
- if (!parsed || "error" in parsed) continue;
861
- const fm = parsed.parsed.frontmatter;
862
- const rfcStatus = String(fm["status"] ?? "").trim();
863
- const liveSpec = fm["liveSpec"];
864
- const rfcId = String(fm["id"] ?? "");
865
818
 
866
- if (!liveSpec || rfcStatus !== "implemented") {
867
- if (liveSpec && rfcStatus === "rejected") {
868
- liveMergeSkipped++;
819
+ // ── RFC-0711: post-loop spec.live.merge for implemented RFCs with liveSpec ──
820
+ const { listRfcFiles, readAndParseRfc } = await import("../rfc/frontmatter-io.ts");
821
+ const { RFC_DIR } = await import("../rfc/types.ts");
822
+ const { runSpecLiveMerge } = await import("../spec/live-spec-merge.ts");
823
+ const rfcDirPath = path.join(context.workspaceRoot, RFC_DIR);
824
+ const statusFilter = input.flags["status"] as string | undefined;
825
+ const allRfcFiles = await listRfcFiles(rfcDirPath);
826
+ const liveMergeResults: Array<{
827
+ id: string;
828
+ domain: string;
829
+ operation: string;
830
+ conflicts: number;
831
+ }> = [];
832
+ let liveMergeSkipped = 0;
833
+
834
+ for (const rfcFile of allRfcFiles) {
835
+ const parsed = await readAndParseRfc(rfcDirPath, rfcFile);
836
+ if (!parsed || "error" in parsed) continue;
837
+ const fm = parsed.parsed.frontmatter;
838
+ const rfcStatus = String(fm["status"] ?? "").trim();
839
+ const liveSpec = fm["liveSpec"];
840
+ const rfcId = String(fm["id"] ?? "");
841
+
842
+ if (!liveSpec || rfcStatus !== "implemented") {
843
+ if (liveSpec && rfcStatus === "rejected") {
844
+ liveMergeSkipped++;
845
+ if (outputFormat === "pretty") {
846
+ logger.info(` spec.live.merge: skipping ${rfcId} (status: rejected)`);
847
+ }
848
+ }
849
+ continue;
850
+ }
851
+ if (statusFilter && rfcStatus !== statusFilter) continue;
852
+
853
+ try {
854
+ const mergeInput: ForgeCommandInput = {
855
+ argv: [],
856
+ flags: { id: rfcId, "dry-run": dryRun },
857
+ };
858
+ const mergeResult = await runSpecLiveMerge(mergeInput, context);
859
+ const mergeData = mergeResult.data as
860
+ { domain?: string; operation?: string; conflicts?: unknown[] } | undefined;
861
+ if (mergeData) {
862
+ liveMergeResults.push({
863
+ id: rfcId,
864
+ domain: String(mergeData.domain ?? ""),
865
+ operation: String(mergeData.operation ?? ""),
866
+ conflicts: Array.isArray(mergeData.conflicts) ? mergeData.conflicts.length : 0,
867
+ });
868
+ }
869
+ } catch (err) {
869
870
  if (outputFormat === "pretty") {
870
- logger.info(` spec.live.merge: skipping ${rfcId} (status: rejected)`);
871
+ logger.error(
872
+ ` spec.live.merge: failed for ${rfcId}: ${String((err as Error).message)}`,
873
+ );
871
874
  }
872
875
  }
873
- continue;
874
876
  }
875
- if (statusFilter && rfcStatus !== statusFilter) continue;
876
877
 
877
- try {
878
- const mergeInput: ForgeCommandInput = {
879
- argv: [],
880
- flags: { id: rfcId, "dry-run": dryRun },
878
+ if (liveMergeResults.length > 0 || liveMergeSkipped > 0) {
879
+ results["spec.live.merge"] = {
880
+ merged: liveMergeResults,
881
+ skipped: liveMergeSkipped,
882
+ dryRun,
881
883
  };
882
- const mergeResult = await runSpecLiveMerge(mergeInput, context);
883
- const mergeData = mergeResult.data as
884
- { domain?: string; operation?: string; conflicts?: unknown[] } | undefined;
885
- if (mergeData) {
886
- liveMergeResults.push({
887
- id: rfcId,
888
- domain: String(mergeData.domain ?? ""),
889
- operation: String(mergeData.operation ?? ""),
890
- conflicts: Array.isArray(mergeData.conflicts) ? mergeData.conflicts.length : 0,
891
- });
892
- }
893
- } catch (err) {
894
884
  if (outputFormat === "pretty") {
895
- logger.error(
896
- ` spec.live.merge: failed for ${rfcId}: ${String((err as Error).message)}`,
897
- );
898
- }
899
- }
900
- }
901
-
902
- if (liveMergeResults.length > 0 || liveMergeSkipped > 0) {
903
- results["spec.live.merge"] = {
904
- merged: liveMergeResults,
905
- skipped: liveMergeSkipped,
906
- dryRun,
907
- };
908
- if (outputFormat === "pretty") {
909
- for (const r of liveMergeResults) {
910
- logger.info(
911
- ` spec.live.merge: ${r.id} → ${r.domain} (${r.operation})${r.conflicts > 0 ? ` [${r.conflicts} conflicts]` : ""}${dryRun ? " [dry-run]" : ""}`,
912
- );
885
+ for (const r of liveMergeResults) {
886
+ logger.info(
887
+ ` spec.live.merge: ${r.id} → ${r.domain} (${r.operation})${r.conflicts > 0 ? ` [${r.conflicts} conflicts]` : ""}${dryRun ? " [dry-run]" : ""}`,
888
+ );
889
+ }
913
890
  }
914
891
  }
915
- }
916
892
 
917
- // Post-loop lockfile refresh — safety net for mission.archive's own refresh.
918
- // If mission.archive moved workpieces but its pnpm install / git commit failed
919
- // silently, the lockfile stays dirty. This catches it and stages the lockfile
920
- // so the caller (or the next commit) includes it.
921
- if (!dryRun && totalMoved > 0) {
922
- try {
923
- const { execSync } = await import("node:child_process");
924
- const lockfileStatus = execSync("git status --porcelain pnpm-lock.yaml", {
925
- cwd: context.workspaceRoot,
926
- encoding: "utf-8",
927
- stdio: ["pipe", "pipe", "pipe"],
928
- }).trim();
929
- if (lockfileStatus) {
930
- execSync("pnpm install", {
893
+ // Post-loop lockfile refresh — safety net for mission.archive's own refresh.
894
+ // If mission.archive moved workpieces but its pnpm install / git commit failed
895
+ // silently, the lockfile stays dirty. This catches it and stages the lockfile
896
+ // so the caller (or the next commit) includes it.
897
+ if (!dryRun && totalMoved > 0) {
898
+ try {
899
+ const { execSync } = await import("node:child_process");
900
+ const lockfileStatus = execSync("git status --porcelain pnpm-lock.yaml", {
931
901
  cwd: context.workspaceRoot,
902
+ encoding: "utf-8",
932
903
  stdio: ["pipe", "pipe", "pipe"],
933
- timeout: 120_000,
934
- });
935
- execSync("git add pnpm-lock.yaml", {
936
- cwd: context.workspaceRoot,
937
- stdio: ["pipe", "pipe", "pipe"],
938
- });
904
+ }).trim();
905
+ if (lockfileStatus) {
906
+ execSync("pnpm install", {
907
+ cwd: context.workspaceRoot,
908
+ stdio: ["pipe", "pipe", "pipe"],
909
+ timeout: 120_000,
910
+ });
911
+ execSync("git add pnpm-lock.yaml", {
912
+ cwd: context.workspaceRoot,
913
+ stdio: ["pipe", "pipe", "pipe"],
914
+ });
915
+ if (outputFormat === "pretty") {
916
+ logger.info(" docs.archive: staged pnpm-lock.yaml refresh (safety net)");
917
+ }
918
+ }
919
+ } catch (refreshErr) {
939
920
  if (outputFormat === "pretty") {
940
- logger.info(" docs.archive: staged pnpm-lock.yaml refresh (safety net)");
921
+ logger.warn(
922
+ ` docs.archive: lockfile safety-net refresh failed — run 'pnpm install' manually: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`,
923
+ );
941
924
  }
942
925
  }
943
- } catch (refreshErr) {
944
- if (outputFormat === "pretty") {
945
- logger.warn(
946
- ` docs.archive: lockfile safety-net refresh failed — run 'pnpm install' manually: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`,
947
- );
948
- }
949
926
  }
950
- }
951
927
 
952
- if (outputFormat === "pretty") {
953
- if (dryRun) {
954
- logger.info(
955
- `[dry-run] docs.archive: would move ${totalMoved} file(s), skip ${totalSkipped} across all domains`,
956
- );
957
- } else {
958
- logger.success(
959
- `docs.archive: moved ${totalMoved} file(s), skipped ${totalSkipped} across all domains`,
960
- );
928
+ if (outputFormat === "pretty") {
929
+ if (dryRun) {
930
+ logger.info(
931
+ `[dry-run] docs.archive: would move ${totalMoved} file(s), skip ${totalSkipped} across all domains`,
932
+ );
933
+ } else {
934
+ logger.success(
935
+ `docs.archive: moved ${totalMoved} file(s), skipped ${totalSkipped} across all domains`,
936
+ );
937
+ }
961
938
  }
962
- }
963
939
 
964
- return {
965
- data: {
966
- command: "docs.archive",
967
- status: "ok",
968
- totalMoved,
969
- totalSkipped,
970
- results,
971
- dryRun,
972
- liveSpecMerges: liveMergeResults.length,
973
- liveSpecSkipped: liveMergeSkipped,
974
- },
975
- summary: dryRun
976
- ? `[dry-run] Would move ${totalMoved} file(s), skip ${totalSkipped}`
977
- : `Moved ${totalMoved} file(s), skipped ${totalSkipped}`,
978
- };
940
+ return {
941
+ data: {
942
+ command: "docs.archive",
943
+ status: "ok",
944
+ totalMoved,
945
+ totalSkipped,
946
+ results,
947
+ dryRun,
948
+ liveSpecMerges: liveMergeResults.length,
949
+ liveSpecSkipped: liveMergeSkipped,
950
+ },
951
+ summary: dryRun
952
+ ? `[dry-run] Would move ${totalMoved} file(s), skip ${totalSkipped}`
953
+ : `Moved ${totalMoved} file(s), skipped ${totalSkipped}`,
954
+ };
955
+ },
979
956
  },
980
- });
981
- },
982
- };
957
+ ],
958
+ pipelines: [],
959
+ };
960
+ }