@warpgogol/forge 3.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.
- package/AGENTS.md +18 -3
- package/bin/cli.ts +15 -8
- package/os/adr/adr.module.ts +24 -23
- package/os/adr/index.ts +1 -1
- package/os/audit/audit.module.ts +13 -8
- package/os/audit/index.ts +1 -1
- package/os/compass/compass.module.ts +22 -19
- package/os/core/core.module.ts +842 -864
- package/os/core/handlers/package-health.ts +21 -0
- package/os/core/index.ts +1 -1
- package/os/exploration/exploration.module.ts +19 -16
- package/os/exploration/index.ts +2 -2
- package/os/mission/index.ts +1 -1
- package/os/mission/mission.module.ts +13 -8
- package/os/naming/index.ts +1 -1
- package/os/naming/naming-convention.ts +1 -0
- package/os/naming/naming.module.ts +13 -7
- package/os/notes/index.ts +2 -2
- package/os/notes/notes.module.ts +20 -17
- package/os/plan/index.ts +1 -1
- package/os/plan/plan.module.ts +13 -8
- package/os/plugin/plugin.module.ts +10 -8
- package/os/program/program.module.ts +22 -20
- package/os/rfc/handlers/implement-stamp.ts +17 -1
- package/os/rfc/index.ts +1 -1
- package/os/rfc/rfc-0000-template.md +3 -2
- package/os/rfc/rfc.module.ts +54 -84
- package/os/rfc/types.ts +1 -0
- package/os/session/handlers/metrics-aggregate.ts +208 -0
- package/os/session/handlers/metrics-rfc.ts +339 -0
- package/os/session/handlers/metrics-session.ts +209 -0
- package/os/session/handlers/save.ts +19 -0
- package/os/session/index.ts +16 -1
- package/os/session/session.module.ts +133 -107
- package/os/session/types.ts +99 -0
- package/os/spec/spec.module.ts +28 -29
- package/os/werkstatt/werkstatt.module.ts +12 -9
- package/os/workflow/index.ts +1 -1
- package/os/workflow/workflow.module.ts +18 -12
- package/package.json +3 -1
- package/src/forge-module.ts +24 -10
- package/src/index.ts +12 -12
- package/src/onboarding/scaffold.ts +6 -4
- package/src/tests/metrics-rfc-1053.test.ts +355 -0
- package/src/types/werkstatt-engine-shims.d.ts +126 -48
- package/src/types/werkstatt-shared-shims.d.ts +6 -0
package/src/forge-module.ts
CHANGED
|
@@ -15,11 +15,28 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ForgeCommandDefinition } from "./types.ts";
|
|
17
17
|
|
|
18
|
-
// ForgeModule is structurally compatible with
|
|
19
|
-
// Forge does NOT import from
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// ForgeModule is structurally compatible with ModuleExport from
|
|
19
|
+
// @warpgogol/werkstatt-engine/runtime/desired-state. Forge does NOT import from
|
|
20
|
+
// werkstatt-engine — TypeScript structural typing ensures compatibility.
|
|
21
|
+
// If the ModuleExport interface changes, the build fails at the point where
|
|
22
|
+
// forge modules are imported into kernel.config.ts.
|
|
22
23
|
|
|
24
|
+
export interface ForgePipelineStep {
|
|
25
|
+
command: string;
|
|
26
|
+
args?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ForgePipelineDeclaration {
|
|
30
|
+
name: string;
|
|
31
|
+
steps: ForgePipelineStep[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* RFC-1038: ForgeModuleRegistry is kept for the CLI's standalone registry
|
|
36
|
+
* implementation. The kernel no longer uses it — modules export commands/
|
|
37
|
+
* pipelines arrays directly. The CLI uses it to collect commands from
|
|
38
|
+
* ForgeModule.commands[] into its own registry.
|
|
39
|
+
*/
|
|
23
40
|
export interface ForgeModuleRegistry {
|
|
24
41
|
registerCommand(command: ForgeCommandDefinition): void;
|
|
25
42
|
registerPipeline(name: string, steps: ForgePipelineStep[]): void;
|
|
@@ -29,13 +46,10 @@ export interface ForgeModule {
|
|
|
29
46
|
name: string;
|
|
30
47
|
version: string;
|
|
31
48
|
runtime: "autonomous" | "werkstatt-adapter";
|
|
32
|
-
|
|
49
|
+
declarations: never[];
|
|
50
|
+
commands: ForgeCommandDefinition[];
|
|
51
|
+
pipelines: ForgePipelineDeclaration[];
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
// Re-export canonical types for convenience
|
|
36
55
|
export type { ForgeCommandDefinition, ForgeCommandResult, ForgeFlagSpec } from "./types.ts";
|
|
37
|
-
|
|
38
|
-
export interface ForgePipelineStep {
|
|
39
|
-
command: string;
|
|
40
|
-
args?: string[];
|
|
41
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -173,18 +173,18 @@ export type {
|
|
|
173
173
|
} from "./compass/types.ts";
|
|
174
174
|
|
|
175
175
|
// OS modules
|
|
176
|
-
export {
|
|
177
|
-
export {
|
|
178
|
-
export {
|
|
179
|
-
export {
|
|
176
|
+
export { createForgeCoreModule } from "../os/core/core.module.ts";
|
|
177
|
+
export { createForgeRfcModule } from "../os/rfc/rfc.module.ts";
|
|
178
|
+
export { createForgeWorkflowModule } from "../os/workflow/workflow.module.ts";
|
|
179
|
+
export { createForgeNamingModule } from "../os/naming/naming.module.ts";
|
|
180
180
|
export { forgeCompassModule } from "../os/compass/compass.module.ts";
|
|
181
181
|
export { forgeWerkstattModule } from "../os/werkstatt/werkstatt.module.ts";
|
|
182
|
-
export {
|
|
183
|
-
export {
|
|
184
|
-
export {
|
|
185
|
-
export {
|
|
186
|
-
export {
|
|
187
|
-
export {
|
|
188
|
-
export {
|
|
189
|
-
export {
|
|
182
|
+
export { createForgeSpecModule } from "../os/spec/spec.module.ts";
|
|
183
|
+
export { createForgeAdrModule } from "../os/adr/adr.module.ts";
|
|
184
|
+
export { createForgePlanModule } from "../os/plan/plan.module.ts";
|
|
185
|
+
export { createForgeAuditModule } from "../os/audit/audit.module.ts";
|
|
186
|
+
export { createForgeMissionModule } from "../os/mission/mission.module.ts";
|
|
187
|
+
export { createForgeExplorationModule } from "../os/exploration/exploration.module.ts";
|
|
188
|
+
export { createForgeNotesModule } from "../os/notes/notes.module.ts";
|
|
189
|
+
export { createForgeProgramModule } from "../os/program/program.module.ts";
|
|
190
190
|
export { forgePluginModule } from "../os/plugin/plugin.module.ts";
|
|
@@ -112,16 +112,18 @@ export const forge${capitalize(name)}Module: ForgeModule = {
|
|
|
112
112
|
name: "forge-${name}",
|
|
113
113
|
version: "0.1.0",
|
|
114
114
|
runtime: "autonomous",
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
declarations: [],
|
|
116
|
+
commands: [
|
|
117
|
+
{
|
|
117
118
|
name: "${name}",
|
|
118
119
|
description: "TODO — one-line description",
|
|
119
120
|
scope: "workspace",
|
|
120
121
|
execute() {
|
|
121
122
|
// TODO: implement
|
|
122
123
|
},
|
|
123
|
-
} satisfies ForgeCommandDefinition
|
|
124
|
-
|
|
124
|
+
} satisfies ForgeCommandDefinition,
|
|
125
|
+
],
|
|
126
|
+
pipelines: [],
|
|
125
127
|
};
|
|
126
128
|
`;
|
|
127
129
|
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, mkdir, writeFile, readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
import { generateRfcMetrics } from "../../os/session/handlers/metrics-rfc.ts";
|
|
8
|
+
import { generateSessionMetrics } from "../../os/session/handlers/metrics-session.ts";
|
|
9
|
+
import { runMetricsAggregate } from "../../os/session/handlers/metrics-aggregate.ts";
|
|
10
|
+
import type { AtifMessage } from "../../os/session/atif-parser.ts";
|
|
11
|
+
import type { ForgeCommandInput, ForgeRuntimeContext, ForgeLogger } from "../../src/types.ts";
|
|
12
|
+
|
|
13
|
+
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const noopLogger: ForgeLogger = {
|
|
16
|
+
section: () => {},
|
|
17
|
+
info: () => {},
|
|
18
|
+
warn: () => {},
|
|
19
|
+
error: () => {},
|
|
20
|
+
success: () => {},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
24
|
+
return {
|
|
25
|
+
workspaceRoot,
|
|
26
|
+
logger: noopLogger,
|
|
27
|
+
dryRun: false,
|
|
28
|
+
outputFormat: "json",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function gitInit(workspaceRoot: string): void {
|
|
33
|
+
execFileSync("git", ["init", "--quiet"], { cwd: workspaceRoot });
|
|
34
|
+
execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: workspaceRoot });
|
|
35
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: workspaceRoot });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function gitCommit(workspaceRoot: string, message: string): string {
|
|
39
|
+
execFileSync("git", ["add", "-A"], { cwd: workspaceRoot });
|
|
40
|
+
execFileSync("git", ["commit", "--quiet", "-m", message, "--allow-empty"], { cwd: workspaceRoot });
|
|
41
|
+
const sha = execFileSync("git", ["rev-parse", "HEAD"], { cwd: workspaceRoot }).toString().trim();
|
|
42
|
+
return sha;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
describe("RFC-1053: generateRfcMetrics", () => {
|
|
48
|
+
let tmpDir: string;
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
tmpDir = await mkdtemp(join(tmpdir(), "metrics-rfc-XXXXXX-"));
|
|
52
|
+
gitInit(tmpDir);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("writes metrics YAML to docs/metrics/rfcs/<rfc-id>.metrics.yaml", async () => {
|
|
60
|
+
const rfcId = "RFC-9999";
|
|
61
|
+
const rfcDir = join(tmpDir, "docs", "rfcs");
|
|
62
|
+
await mkdir(rfcDir, { recursive: true });
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(rfcDir, "rfc-9999-test.md"),
|
|
65
|
+
`---
|
|
66
|
+
id: ${rfcId}
|
|
67
|
+
title: "Test RFC"
|
|
68
|
+
status: implemented
|
|
69
|
+
kind: architecture
|
|
70
|
+
scope: workspace
|
|
71
|
+
owners:
|
|
72
|
+
- architecture
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
# ${rfcId}: Test RFC
|
|
76
|
+
|
|
77
|
+
## Acceptance criteria
|
|
78
|
+
|
|
79
|
+
- [x] Criterion 1 (evidence: test:foo)
|
|
80
|
+
- [x] Criterion 2 (evidence: test:bar)
|
|
81
|
+
- [ ] Criterion 3
|
|
82
|
+
`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
gitCommit(tmpDir, `audit: ${rfcId} — test audit`);
|
|
86
|
+
gitCommit(tmpDir, `enhance: ${rfcId} — test enhance`);
|
|
87
|
+
gitCommit(tmpDir, `plan: ${rfcId} — test plan`);
|
|
88
|
+
gitCommit(tmpDir, `implement: ${rfcId} — test implement`);
|
|
89
|
+
|
|
90
|
+
const metricsPath = await generateRfcMetrics(tmpDir, rfcId, noopLogger);
|
|
91
|
+
|
|
92
|
+
expect(metricsPath).toBe("docs/metrics/rfcs/rfc-9999.metrics.yaml");
|
|
93
|
+
const fileContent = await readFile(join(tmpDir, metricsPath), "utf-8");
|
|
94
|
+
expect(fileContent).toContain("rfcId: RFC-9999");
|
|
95
|
+
expect(fileContent).toContain("pipeline:");
|
|
96
|
+
expect(fileContent).toContain("step: audit");
|
|
97
|
+
expect(fileContent).toContain("step: enhance");
|
|
98
|
+
expect(fileContent).toContain("step: plan");
|
|
99
|
+
expect(fileContent).toContain("step: implement");
|
|
100
|
+
expect(fileContent).toContain("skill: fo-idea-audit");
|
|
101
|
+
expect(fileContent).toContain("skill: fo-idea-implement");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("classifies commit prefixes into pipeline steps with correct skills", async () => {
|
|
105
|
+
const rfcId = "RFC-8888";
|
|
106
|
+
const rfcDir = join(tmpDir, "docs", "rfcs");
|
|
107
|
+
await mkdir(rfcDir, { recursive: true });
|
|
108
|
+
await writeFile(
|
|
109
|
+
join(rfcDir, "rfc-8888-test.md"),
|
|
110
|
+
`---
|
|
111
|
+
id: ${rfcId}
|
|
112
|
+
title: "Test"
|
|
113
|
+
status: implemented
|
|
114
|
+
kind: architecture
|
|
115
|
+
scope: workspace
|
|
116
|
+
owners:
|
|
117
|
+
- architecture
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
# ${rfcId}
|
|
121
|
+
|
|
122
|
+
## Acceptance criteria
|
|
123
|
+
|
|
124
|
+
- [x] AC1
|
|
125
|
+
`,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
gitCommit(tmpDir, `review: ${rfcId} — code review`);
|
|
129
|
+
gitCommit(tmpDir, `fix: ${rfcId} — fix issue`);
|
|
130
|
+
|
|
131
|
+
const metricsPath = await generateRfcMetrics(tmpDir, rfcId, noopLogger);
|
|
132
|
+
const fileContent = await readFile(join(tmpDir, metricsPath), "utf-8");
|
|
133
|
+
|
|
134
|
+
expect(fileContent).toContain("step: review");
|
|
135
|
+
expect(fileContent).toContain("skill: fo-review");
|
|
136
|
+
expect(fileContent).toContain("step: fix");
|
|
137
|
+
expect(fileContent).toContain("skill: fo-fix");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("handles RFC with no commits gracefully", async () => {
|
|
141
|
+
const rfcId = "RFC-7777";
|
|
142
|
+
const metricsPath = await generateRfcMetrics(tmpDir, rfcId, noopLogger);
|
|
143
|
+
|
|
144
|
+
expect(metricsPath).toBe("docs/metrics/rfcs/rfc-7777.metrics.yaml");
|
|
145
|
+
const fileContent = await readFile(join(tmpDir, metricsPath), "utf-8");
|
|
146
|
+
expect(fileContent).toContain("rfcId: RFC-7777");
|
|
147
|
+
expect(fileContent).toContain("pipeline: []");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("RFC-1053: generateSessionMetrics", () => {
|
|
152
|
+
let tmpDir: string;
|
|
153
|
+
|
|
154
|
+
beforeEach(async () => {
|
|
155
|
+
tmpDir = await mkdtemp(join(tmpdir(), "metrics-session-XXXXXX-"));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
afterEach(async () => {
|
|
159
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("extracts skill invocations from ATIF messages", async () => {
|
|
163
|
+
const messages: AtifMessage[] = [
|
|
164
|
+
{ role: "user", timestamp: "2026-09-06T10:00:00Z", content: "Please run fo-idea-audit on RFC-1053" },
|
|
165
|
+
{ role: "assistant", timestamp: "2026-09-06T10:05:00Z", content: "Running fo-idea-audit now" },
|
|
166
|
+
{ role: "user", timestamp: "2026-09-06T10:10:00Z", content: "Now run fo-review" },
|
|
167
|
+
{ role: "assistant", timestamp: "2026-09-06T10:15:00Z", content: "Running fo-review" },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const metricsPath = await generateSessionMetrics(
|
|
171
|
+
tmpDir,
|
|
172
|
+
"2026-09-06-10-00-00-abc123",
|
|
173
|
+
messages,
|
|
174
|
+
{ date: "2026-09-06", relatedRfcs: ["RFC-1053"], commits: ["abc1234"] },
|
|
175
|
+
noopLogger,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(metricsPath).toBe("docs/metrics/sessions/2026-09-06-10-00-00-abc123.metrics.yaml");
|
|
179
|
+
const fileContent = await readFile(join(tmpDir, metricsPath), "utf-8");
|
|
180
|
+
expect(fileContent).toContain("sessionId: 2026-09-06-10-00-00-abc123");
|
|
181
|
+
expect(fileContent).toContain("approximateDurations: true");
|
|
182
|
+
expect(fileContent).toContain("fo-idea-audit");
|
|
183
|
+
expect(fileContent).toContain("fo-review");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("computes approximate duration from message timestamps", async () => {
|
|
187
|
+
const messages: AtifMessage[] = [
|
|
188
|
+
{ role: "user", timestamp: "2026-09-06T10:00:00Z", content: "Run fo-review" },
|
|
189
|
+
{ role: "assistant", timestamp: "2026-09-06T10:30:00Z", content: "fo-review done" },
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const metricsPath = await generateSessionMetrics(
|
|
193
|
+
tmpDir,
|
|
194
|
+
"test-session-001",
|
|
195
|
+
messages,
|
|
196
|
+
{ date: "2026-09-06" },
|
|
197
|
+
noopLogger,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const fileContent = await readFile(join(tmpDir, metricsPath), "utf-8");
|
|
201
|
+
expect(fileContent).toContain("approximateDurationMs:");
|
|
202
|
+
// 30 minutes = 1,800,000 ms
|
|
203
|
+
expect(fileContent).toContain("1800000");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("handles empty messages gracefully", async () => {
|
|
207
|
+
const metricsPath = await generateSessionMetrics(
|
|
208
|
+
tmpDir,
|
|
209
|
+
"empty-session",
|
|
210
|
+
[],
|
|
211
|
+
{ date: "2026-09-06" },
|
|
212
|
+
noopLogger,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const fileContent = await readFile(join(tmpDir, metricsPath), "utf-8");
|
|
216
|
+
expect(fileContent).toContain("sessionId: empty-session");
|
|
217
|
+
expect(fileContent).toContain("skills: []");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("RFC-1053: metrics.aggregate", () => {
|
|
222
|
+
let tmpDir: string;
|
|
223
|
+
|
|
224
|
+
beforeEach(async () => {
|
|
225
|
+
tmpDir = await mkdtemp(join(tmpdir(), "metrics-agg-XXXXXX-"));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
afterEach(async () => {
|
|
229
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("returns empty result when no metrics files exist", async () => {
|
|
233
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
234
|
+
const result = await runMetricsAggregate(input, makeContext(tmpDir));
|
|
235
|
+
|
|
236
|
+
expect(result.data?.status).toBe("ok");
|
|
237
|
+
expect(result.data?.perSkill).toEqual([]);
|
|
238
|
+
expect(result.data?.totalRfcs).toBe(0);
|
|
239
|
+
expect(result.data?.totalSessions).toBe(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("aggregates skill data from RFC and session metrics files", async () => {
|
|
243
|
+
// Create RFC metrics
|
|
244
|
+
const rfcMetricsDir = join(tmpDir, "docs", "metrics", "rfcs");
|
|
245
|
+
await mkdir(rfcMetricsDir, { recursive: true });
|
|
246
|
+
await writeFile(
|
|
247
|
+
join(rfcMetricsDir, "rfc-9999.metrics.yaml"),
|
|
248
|
+
`rfcId: RFC-9999
|
|
249
|
+
generatedAt: 2026-09-06T10:00:00Z
|
|
250
|
+
pipeline:
|
|
251
|
+
- step: audit
|
|
252
|
+
skill: fo-idea-audit
|
|
253
|
+
commitSha: abc123
|
|
254
|
+
timestamp: "2026-09-06T09:00:00Z"
|
|
255
|
+
- step: implement
|
|
256
|
+
skill: fo-idea-implement
|
|
257
|
+
commitSha: def456
|
|
258
|
+
timestamp: "2026-09-06T10:00:00Z"
|
|
259
|
+
review:
|
|
260
|
+
findingsCount: 3
|
|
261
|
+
findingsByAxis:
|
|
262
|
+
Structural correctness: 2
|
|
263
|
+
DNA alignment: 1
|
|
264
|
+
verdict: approved
|
|
265
|
+
fix:
|
|
266
|
+
fixesApplied: 2
|
|
267
|
+
commitSha: ghi789
|
|
268
|
+
iterations: 2
|
|
269
|
+
verification: null
|
|
270
|
+
result:
|
|
271
|
+
acceptanceCriteriaTotal: 5
|
|
272
|
+
acceptanceCriteriaMet: 5
|
|
273
|
+
timings:
|
|
274
|
+
firstCommitAt: "2026-09-06T09:00:00Z"
|
|
275
|
+
lastCommitAt: "2026-09-06T10:00:00Z"
|
|
276
|
+
totalDurationMs: 3600000
|
|
277
|
+
`,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Create session metrics
|
|
281
|
+
const sessionMetricsDir = join(tmpDir, "docs", "metrics", "sessions");
|
|
282
|
+
await mkdir(sessionMetricsDir, { recursive: true });
|
|
283
|
+
await writeFile(
|
|
284
|
+
join(sessionMetricsDir, "test-session.metrics.yaml"),
|
|
285
|
+
`sessionId: test-session
|
|
286
|
+
generatedAt: 2026-09-06T11:00:00Z
|
|
287
|
+
date: "2026-09-06"
|
|
288
|
+
durationMs: 1800000
|
|
289
|
+
approximateDurations: true
|
|
290
|
+
documents:
|
|
291
|
+
- rfcId: RFC-9999
|
|
292
|
+
status: implemented
|
|
293
|
+
metricsFile: docs/metrics/rfcs/rfc-9999.metrics.yaml
|
|
294
|
+
skills:
|
|
295
|
+
- skill: fo-idea-audit
|
|
296
|
+
approximateDurationMs: 300000
|
|
297
|
+
- skill: fo-review
|
|
298
|
+
approximateDurationMs: 600000
|
|
299
|
+
insights: null
|
|
300
|
+
commits:
|
|
301
|
+
- abc123
|
|
302
|
+
`,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
306
|
+
const result = await runMetricsAggregate(input, makeContext(tmpDir));
|
|
307
|
+
|
|
308
|
+
expect(result.data?.totalRfcs).toBe(1);
|
|
309
|
+
expect(result.data?.totalSessions).toBe(1);
|
|
310
|
+
|
|
311
|
+
const auditSkill = result.data?.perSkill.find((s) => s.skill === "fo-idea-audit");
|
|
312
|
+
expect(auditSkill).toBeDefined();
|
|
313
|
+
expect(auditSkill?.invocations).toBeGreaterThanOrEqual(1);
|
|
314
|
+
|
|
315
|
+
const reviewSkill = result.data?.perSkill.find((s) => s.skill === "fo-review");
|
|
316
|
+
expect(reviewSkill).toBeDefined();
|
|
317
|
+
expect(reviewSkill?.avgFindings).toBe(3);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("filters by skill name when --skill is provided", async () => {
|
|
321
|
+
const rfcMetricsDir = join(tmpDir, "docs", "metrics", "rfcs");
|
|
322
|
+
await mkdir(rfcMetricsDir, { recursive: true });
|
|
323
|
+
await writeFile(
|
|
324
|
+
join(rfcMetricsDir, "rfc-9999.metrics.yaml"),
|
|
325
|
+
`rfcId: RFC-9999
|
|
326
|
+
generatedAt: 2026-09-06T10:00:00Z
|
|
327
|
+
pipeline:
|
|
328
|
+
- step: audit
|
|
329
|
+
skill: fo-idea-audit
|
|
330
|
+
commitSha: abc123
|
|
331
|
+
timestamp: "2026-09-06T09:00:00Z"
|
|
332
|
+
- step: review
|
|
333
|
+
skill: fo-review
|
|
334
|
+
commitSha: def456
|
|
335
|
+
timestamp: "2026-09-06T10:00:00Z"
|
|
336
|
+
review: null
|
|
337
|
+
fix: null
|
|
338
|
+
verification: null
|
|
339
|
+
result:
|
|
340
|
+
acceptanceCriteriaTotal: 0
|
|
341
|
+
acceptanceCriteriaMet: 0
|
|
342
|
+
timings:
|
|
343
|
+
firstCommitAt: null
|
|
344
|
+
lastCommitAt: null
|
|
345
|
+
totalDurationMs: null
|
|
346
|
+
`,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const input: ForgeCommandInput = { argv: [], flags: { skill: "fo-review" } };
|
|
350
|
+
const result = await runMetricsAggregate(input, makeContext(tmpDir));
|
|
351
|
+
|
|
352
|
+
expect(result.data?.perSkill.length).toBe(1);
|
|
353
|
+
expect(result.data?.perSkill[0]?.skill).toBe("fo-review");
|
|
354
|
+
});
|
|
355
|
+
});
|