@warpgogol/forge 4.0.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +20 -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
@@ -15,13 +15,8 @@
15
15
 
16
16
  import type { ForgeModule } from "../../src/forge-module.ts";
17
17
 
18
- export const forgeRfcModule: ForgeModule = {
19
- name: "rfc",
20
- version: "0.1.0",
21
- runtime: "autonomous",
22
-
23
- async register(registry) {
24
- const {
18
+ export async function createForgeRfcModule(): Promise<ForgeModule> {
19
+ const {
25
20
  runRfcList,
26
21
  runRfcCreate,
27
22
  runRfcNextId,
@@ -41,8 +36,15 @@ export const forgeRfcModule: ForgeModule = {
41
36
  const { runRfcArchive } = await import("./handlers/archive.ts");
42
37
  const { runRfcPipelineStatus } = await import("./handlers/pipeline-status.ts");
43
38
  const { runRfcImplementStamp } = await import("./handlers/implement-stamp.ts");
44
- // ── rfc.list ─────────────────────────────────────────────────────────────
45
- registry.registerCommand({
39
+ // ── rfc.list ─────────────────────────────────────────────────────────────;
40
+ return {
41
+ name: "rfc",
42
+ version: "0.1.0",
43
+ runtime: "autonomous",
44
+
45
+ declarations: [],
46
+ commands: [
47
+ {
46
48
  name: "rfc.list",
47
49
  description:
48
50
  "List all RFCs. Filter with --status, --kind, --owner flags. " +
@@ -67,10 +69,8 @@ export const forgeRfcModule: ForgeModule = {
67
69
  },
68
70
  reads: ["docs/rfcs/**/*.md"],
69
71
  execute: runRfcList,
70
- });
71
-
72
- // ── rfc.create ───────────────────────────────────────────────────────────
73
- registry.registerCommand({
72
+ },
73
+ {
74
74
  name: "rfc.create",
75
75
  description:
76
76
  "Create a new RFC draft from the template. " +
@@ -99,10 +99,8 @@ export const forgeRfcModule: ForgeModule = {
99
99
  },
100
100
  },
101
101
  execute: runRfcCreate,
102
- });
103
-
104
- // ── rfc.next-id ───────────────────────────────────────────────────────────
105
- registry.registerCommand({
102
+ },
103
+ {
106
104
  name: "rfc.next-id",
107
105
  description:
108
106
  "Return the next free RFC number (max existing + 1) by scanning docs/rfcs/ recursively including archive/.",
@@ -110,10 +108,8 @@ export const forgeRfcModule: ForgeModule = {
110
108
  flags: {},
111
109
  reads: ["docs/rfcs/**/*.md"],
112
110
  execute: runRfcNextId,
113
- });
114
-
115
- // ── rfc.validate ─────────────────────────────────────────────────────────
116
- registry.registerCommand({
111
+ },
112
+ {
117
113
  name: "rfc.validate",
118
114
  contract: "rfc",
119
115
  rules: [],
@@ -128,10 +124,8 @@ export const forgeRfcModule: ForgeModule = {
128
124
  },
129
125
  reads: ["docs/rfcs/**/*.md"],
130
126
  execute: runRfcValidate,
131
- });
132
-
133
- // ── rfc.command-lifecycle.validate ───────────────────────────────────────
134
- registry.registerCommand({
127
+ },
128
+ {
135
129
  name: "rfc.command-lifecycle.validate",
136
130
  contract: "rfc",
137
131
  rules: [],
@@ -145,10 +139,8 @@ export const forgeRfcModule: ForgeModule = {
145
139
  reads: ["docs/rfcs/**/*.md"],
146
140
  cacheable: false,
147
141
  execute: runRfcCommandLifecycleValidate,
148
- });
149
-
150
- // ── rfc.check ────────────────────────────────────────────────────────────
151
- registry.registerCommand({
142
+ },
143
+ {
152
144
  name: "rfc.check",
153
145
  contract: "rfc",
154
146
  rules: [],
@@ -166,10 +158,8 @@ export const forgeRfcModule: ForgeModule = {
166
158
  },
167
159
  reads: ["docs/rfcs/**/*.md"],
168
160
  execute: runRfcCheck,
169
- });
170
-
171
- // ── rfc.index.generate ───────────────────────────────────────────────────
172
- registry.registerCommand({
161
+ },
162
+ {
173
163
  name: "rfc.index.generate",
174
164
  description:
175
165
  "Emit a machine-readable relationship index of all RFCs (id, status, dates, " +
@@ -186,10 +176,8 @@ export const forgeRfcModule: ForgeModule = {
186
176
  reads: ["docs/rfcs/**/*.md"],
187
177
  cacheable: false,
188
178
  execute: runRfcIndexGenerate,
189
- });
190
-
191
- // ── rfc.index.validate ───────────────────────────────────────────────────
192
- registry.registerCommand({
179
+ },
180
+ {
193
181
  name: "rfc.index.validate",
194
182
  contract: "rfc",
195
183
  rules: [],
@@ -201,10 +189,8 @@ export const forgeRfcModule: ForgeModule = {
201
189
  flags: {},
202
190
  reads: ["docs/rfcs/**/*.md", "docs/rfcs/index.yaml"],
203
191
  execute: runRfcIndexValidate,
204
- });
205
-
206
- // ── rfc.graph ────────────────────────────────────────────────────────────
207
- registry.registerCommand({
192
+ },
193
+ {
208
194
  name: "rfc.graph",
209
195
  description:
210
196
  "Print one RFC's relationship neighbours (supersedes/supersededBy/amends/" +
@@ -215,10 +201,8 @@ export const forgeRfcModule: ForgeModule = {
215
201
  },
216
202
  reads: ["docs/rfcs/**/*.md"],
217
203
  execute: runRfcGraph,
218
- });
219
-
220
- // ── rfc.acceptance.run ───────────────────────────────────────────────────
221
- registry.registerCommand({
204
+ },
205
+ {
222
206
  name: "rfc.acceptance.run",
223
207
  description:
224
208
  "RFC-0268: execute the acceptance: probes declared in an RFC's frontmatter and report " +
@@ -236,10 +220,8 @@ export const forgeRfcModule: ForgeModule = {
236
220
  reads: ["docs/rfcs/**/*.md"],
237
221
  cacheable: false,
238
222
  execute: runRfcAcceptanceRun,
239
- });
240
-
241
- // ── rfc.verification.emit ────────────────────────────────────────────────
242
- registry.registerCommand({
223
+ },
224
+ {
243
225
  name: "rfc.verification.emit",
244
226
  description:
245
227
  "RFC-0330: execute acceptance probes for target RFC(s) and write per-RFC verification " +
@@ -258,10 +240,8 @@ export const forgeRfcModule: ForgeModule = {
258
240
  },
259
241
  },
260
242
  execute: runRfcVerificationEmit,
261
- });
262
-
263
- // ── rfc.verification.refresh ────────────────────────────────────────────
264
- registry.registerCommand({
243
+ },
244
+ {
265
245
  name: "rfc.verification.refresh",
266
246
  description:
267
247
  "RFC-0999: re-run acceptance probes for implemented RFC(s) and update verification " +
@@ -285,10 +265,8 @@ export const forgeRfcModule: ForgeModule = {
285
265
  },
286
266
  },
287
267
  execute: runRfcVerificationRefresh,
288
- });
289
-
290
- // ── rfc.dna.trace.validate ───────────────────────────────────────────────
291
- registry.registerCommand({
268
+ },
269
+ {
292
270
  name: "rfc.dna.trace.validate",
293
271
  contract: "rfc",
294
272
  rules: [],
@@ -305,10 +283,8 @@ export const forgeRfcModule: ForgeModule = {
305
283
  },
306
284
  reads: ["docs/rfcs/**/*.md", "docs/architecture-dna.md"],
307
285
  execute: runRfcDnaTraceValidate,
308
- });
309
-
310
- // ── rfc.dna.trace.generate ───────────────────────────────────────────────
311
- registry.registerCommand({
286
+ },
287
+ {
312
288
  name: "rfc.dna.trace.generate",
313
289
  description:
314
290
  "RFC-0331: generate docs/rfcs/dna-trace.generated.yaml — the machine-readable " +
@@ -320,10 +296,8 @@ export const forgeRfcModule: ForgeModule = {
320
296
  cacheable: false,
321
297
  flags: {},
322
298
  execute: runRfcDnaTraceGenerate,
323
- });
324
-
325
- // ── rfc.decision-log.generate ────────────────────────────────────────────
326
- registry.registerCommand({
299
+ },
300
+ {
327
301
  name: "rfc.decision-log.generate",
328
302
  description:
329
303
  "RFC-0329: generate docs/rfcs/decision-log.generated.yaml and .md aggregating every " +
@@ -341,10 +315,8 @@ export const forgeRfcModule: ForgeModule = {
341
315
  },
342
316
  },
343
317
  execute: runRfcDecisionLogGenerate,
344
- });
345
-
346
- // ── rfc.supersede.propose ─────────────────────────────────────────────────
347
- registry.registerCommand({
318
+ },
319
+ {
348
320
  name: "rfc.supersede.propose",
349
321
  description:
350
322
  "RFC-0334: escalate a blocked implementation by creating a draft superseding RFC " +
@@ -373,10 +345,8 @@ export const forgeRfcModule: ForgeModule = {
373
345
  },
374
346
  },
375
347
  execute: runRfcSupersedePropose,
376
- });
377
-
378
- // ── rfc.archive ───────────────────────────────────────────────────────────
379
- registry.registerCommand({
348
+ },
349
+ {
380
350
  name: "rfc.archive",
381
351
  description:
382
352
  "Move terminal-status RFC files (implemented, rejected, superseded) into " +
@@ -400,10 +370,8 @@ export const forgeRfcModule: ForgeModule = {
400
370
  },
401
371
  },
402
372
  execute: runRfcArchive,
403
- });
404
-
405
- // ── rfc.pipeline.status ──────────────────────────────────────────────────
406
- registry.registerCommand({
373
+ },
374
+ {
407
375
  name: "rfc.pipeline.status",
408
376
  description:
409
377
  "Report the pipeline status of RFCs — which steps (audit, enhance, plan, implement) " +
@@ -416,10 +384,8 @@ export const forgeRfcModule: ForgeModule = {
416
384
  },
417
385
  reads: ["docs/rfcs/**/*.md"],
418
386
  execute: runRfcPipelineStatus,
419
- });
420
-
421
- // ── rfc.implement.stamp ──────────────────────────────────────────────────
422
- registry.registerCommand({
387
+ },
388
+ {
423
389
  name: "rfc.implement.stamp",
424
390
  description:
425
391
  "RFC-0476: the exclusive atomic path for accepted → implemented transitions. " +
@@ -450,6 +416,10 @@ export const forgeRfcModule: ForgeModule = {
450
416
  },
451
417
  },
452
418
  execute: runRfcImplementStamp,
453
- });
454
- },
455
- };
419
+ }
420
+ ],
421
+ pipelines: [
422
+
423
+ ]};
424
+ }
425
+ ;
package/os/rfc/types.ts CHANGED
@@ -541,6 +541,7 @@ export interface RfcImplementStampData {
541
541
  stampedAt: string;
542
542
  criteriaChecked: number;
543
543
  evidencePath?: string;
544
+ metricsPath?: string;
544
545
  }
545
546
 
546
547
  export type RfcImplementStampRule =
@@ -0,0 +1,208 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-1053: metrics.aggregate command handler — scans docs/metrics/ files and
5
+ produces per-skill statistics (invocations, avgFindings, avgFixIterations,
6
+ avgDurationMs). Supports --skill, --since, --until filters. Returns empty
7
+ result when no metrics files exist.
8
+ </purpose>
9
+ <non-goals>
10
+ <item>Does not write files — read-only aggregation.</item>
11
+ <item>Does not index metrics files — scans on each invocation.</item>
12
+ </non-goals>
13
+ </MODULE_CONTRACT>
14
+ <CHANGE_SUMMARY>
15
+ <item>RFC-1053: initial metrics.aggregate command handler.</item>
16
+ </CHANGE_SUMMARY>
17
+ */
18
+
19
+ import { readdir, readFile } from "node:fs/promises";
20
+ import { join } from "node:path";
21
+ import { parse as yamlParse } from "yaml";
22
+
23
+ import { METRICS_DIR, type MetricsAggregateResult, type SkillAggregate, type RfcMetrics, type SessionMetrics } from "../types.ts";
24
+ import type { ForgeCommandInput, ForgeCommandResult, ForgeRuntimeContext } from "../../../src/types.ts";
25
+
26
+ async function readYamlFile<T>(filePath: string): Promise<T | null> {
27
+ try {
28
+ const content = await readFile(filePath, "utf-8");
29
+ return yamlParse(content) as T;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export async function runMetricsAggregate(
36
+ input: ForgeCommandInput,
37
+ context: ForgeRuntimeContext,
38
+ ): Promise<ForgeCommandResult<MetricsAggregateResult>> {
39
+ const { workspaceRoot, outputFormat } = context;
40
+ const skillFilter = input.flags["skill"] as string | undefined;
41
+ const since = input.flags["since"] as string | undefined;
42
+ const until = input.flags["until"] as string | undefined;
43
+
44
+ const rfcMetricsDir = join(workspaceRoot, METRICS_DIR, "rfcs");
45
+ const sessionMetricsDir = join(workspaceRoot, METRICS_DIR, "sessions");
46
+
47
+ let rfcFiles: string[] = [];
48
+ let sessionFiles: string[] = [];
49
+
50
+ try {
51
+ rfcFiles = await readdir(rfcMetricsDir);
52
+ } catch {
53
+ // No RFC metrics directory yet
54
+ }
55
+
56
+ try {
57
+ sessionFiles = await readdir(sessionMetricsDir);
58
+ } catch {
59
+ // No session metrics directory yet
60
+ }
61
+
62
+ const rfcMetricsList: RfcMetrics[] = [];
63
+ for (const file of rfcFiles) {
64
+ if (!file.endsWith(".metrics.yaml")) continue;
65
+ const metrics = await readYamlFile<RfcMetrics>(join(rfcMetricsDir, file));
66
+ if (metrics) rfcMetricsList.push(metrics);
67
+ }
68
+
69
+ const sessionMetricsList: SessionMetrics[] = [];
70
+ for (const file of sessionFiles) {
71
+ if (!file.endsWith(".metrics.yaml")) continue;
72
+ const metrics = await readYamlFile<SessionMetrics>(join(sessionMetricsDir, file));
73
+ if (metrics) sessionMetricsList.push(metrics);
74
+ }
75
+
76
+ // Apply date filters
77
+ const sinceDate = since ? new Date(since) : null;
78
+ const untilDate = until ? new Date(until) : null;
79
+
80
+ const filteredRfcs = rfcMetricsList.filter((m) => {
81
+ if (!m.generatedAt) return true;
82
+ const d = new Date(m.generatedAt);
83
+ if (isNaN(d.getTime())) return true;
84
+ if (sinceDate && d < sinceDate) return false;
85
+ if (untilDate && d > untilDate) return false;
86
+ return true;
87
+ });
88
+
89
+ const filteredSessions = sessionMetricsList.filter((m) => {
90
+ if (!m.generatedAt) return true;
91
+ const d = new Date(m.generatedAt);
92
+ if (isNaN(d.getTime())) return true;
93
+ if (sinceDate && d < sinceDate) return false;
94
+ if (untilDate && d > untilDate) return false;
95
+ return true;
96
+ });
97
+
98
+ // Aggregate per-skill
99
+ const skillData = new Map<
100
+ string,
101
+ {
102
+ invocations: number;
103
+ findingsSum: number;
104
+ findingsCount: number;
105
+ fixIterationsSum: number;
106
+ fixIterationsCount: number;
107
+ durationSum: number;
108
+ durationCount: number;
109
+ }
110
+ >();
111
+
112
+ function recordSkill(skill: string, data: Partial<{
113
+ findings: number;
114
+ fixIterations: number;
115
+ durationMs: number;
116
+ }>) {
117
+ let entry = skillData.get(skill);
118
+ if (!entry) {
119
+ entry = {
120
+ invocations: 0,
121
+ findingsSum: 0,
122
+ findingsCount: 0,
123
+ fixIterationsSum: 0,
124
+ fixIterationsCount: 0,
125
+ durationSum: 0,
126
+ durationCount: 0,
127
+ };
128
+ skillData.set(skill, entry);
129
+ }
130
+ entry.invocations++;
131
+ if (data.findings !== undefined) {
132
+ entry.findingsSum += data.findings;
133
+ entry.findingsCount++;
134
+ }
135
+ if (data.fixIterations !== undefined) {
136
+ entry.fixIterationsSum += data.fixIterations;
137
+ entry.fixIterationsCount++;
138
+ }
139
+ if (data.durationMs !== undefined) {
140
+ entry.durationSum += data.durationMs;
141
+ entry.durationCount++;
142
+ }
143
+ }
144
+
145
+ // From RFC metrics: pipeline steps (skill invocations), review findings, fix iterations
146
+ for (const rfc of filteredRfcs) {
147
+ for (const step of rfc.pipeline) {
148
+ if (step.skill) {
149
+ recordSkill(step.skill, {});
150
+ }
151
+ }
152
+ if (rfc.review && rfc.review.findingsCount !== undefined) {
153
+ recordSkill("fo-review", { findings: rfc.review.findingsCount });
154
+ }
155
+ if (rfc.fix) {
156
+ recordSkill("fo-fix", { fixIterations: rfc.fix.iterations });
157
+ }
158
+ }
159
+
160
+ // From session metrics: skill invocations with durations
161
+ for (const session of filteredSessions) {
162
+ for (const inv of session.skills) {
163
+ recordSkill(inv.skill, {
164
+ durationMs: inv.approximateDurationMs ?? undefined,
165
+ });
166
+ }
167
+ }
168
+
169
+ let perSkill: SkillAggregate[] = [];
170
+ for (const [skill, data] of skillData) {
171
+ if (skillFilter && skill !== skillFilter) continue;
172
+ perSkill.push({
173
+ skill,
174
+ invocations: data.invocations,
175
+ avgFindings: data.findingsCount > 0 ? data.findingsSum / data.findingsCount : null,
176
+ avgFixIterations: data.fixIterationsCount > 0 ? data.fixIterationsSum / data.fixIterationsCount : null,
177
+ avgDurationMs: data.durationCount > 0 ? data.durationSum / data.durationCount : null,
178
+ });
179
+ }
180
+
181
+ // Sort by invocations descending
182
+ perSkill.sort((a, b) => b.invocations - a.invocations);
183
+
184
+ if (skillFilter && perSkill.length === 0) {
185
+ perSkill = [];
186
+ }
187
+
188
+ const result: MetricsAggregateResult = {
189
+ command: "metrics.aggregate",
190
+ status: "ok",
191
+ skill: (skillFilter as string) ?? "all",
192
+ since: since ?? null,
193
+ until: until ?? null,
194
+ perSkill,
195
+ totalRfcs: filteredRfcs.length,
196
+ totalSessions: filteredSessions.length,
197
+ };
198
+
199
+ if (outputFormat === "pretty") {
200
+ context.logger.info(`[metrics.aggregate] ${perSkill.length} skill(s), ${filteredRfcs.length} RFC(s), ${filteredSessions.length} session(s)`);
201
+ }
202
+
203
+ return {
204
+ data: result,
205
+ exitCode: 0,
206
+ summary: `[metrics.aggregate] ${perSkill.length} skill(s) across ${filteredRfcs.length} RFC(s) and ${filteredSessions.length} session(s)`,
207
+ };
208
+ }