forgehive 0.7.7 → 0.7.8
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/README.md +5 -3
- package/dist/cli.js +9 -31
- package/docs/user-guide.md +8 -3
- package/forgehive/commands/fh-refactor.md +116 -0
- package/forgehive/commands/review-party.md +42 -23
- package/forgehive/party/defaults.yaml +8 -0
- package/package.json +2 -2
- package/docs/superpowers/plans/2026-05-11-nextgen-v04-v05.md +0 -1708
- package/docs/superpowers/plans/2026-05-11-v05-nextgen-gaps.md +0 -2364
- package/docs/superpowers/plans/2026-05-14-sprint-planner-v2.md +0 -1058
- package/docs/superpowers/plans/2026-05-14-v07-nextgen.md +0 -1980
|
@@ -1,1980 +0,0 @@
|
|
|
1
|
-
# forgehive v0.7 — NextGen Gap Closure Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Close all competitive gaps identified in the v0.7 analysis — CI/PR integration, codebase map, onboarding docs, changelog, metrics, team sync, background agents, slash commands, and multi-model routing.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Each new feature is a self-contained TypeScript module in `src/` with a corresponding test file in `test/`. The `src/cli.ts` file is wired to call each module. Slash commands are pure Markdown files in `forgehive/commands/`. All tests use `node:test` + `node:assert/strict` with tmpdir isolation.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** TypeScript ESM, Node.js ≥ 18, `node:child_process` (spawnSync/spawn), `node:fs`, `node:path`, `node:os` — no new runtime dependencies.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## File Map
|
|
14
|
-
|
|
15
|
-
**New source files:**
|
|
16
|
-
- `src/ci.ts` — CI report generation + GitHub Actions template
|
|
17
|
-
- `src/map.ts` — codebase structure map (file tree + imports)
|
|
18
|
-
- `src/onboard.ts` — onboarding document generator
|
|
19
|
-
- `src/changelog.ts` — semantic changelog from git log
|
|
20
|
-
- `src/metrics.ts` — developer productivity metrics from git
|
|
21
|
-
- `src/sync.ts` — team context sync via git branch
|
|
22
|
-
- `src/background.ts` — background agent execution via `claude -p`
|
|
23
|
-
|
|
24
|
-
**New test files:**
|
|
25
|
-
- `test/ci.test.ts`
|
|
26
|
-
- `test/map.test.ts`
|
|
27
|
-
- `test/onboard.test.ts`
|
|
28
|
-
- `test/changelog.test.ts`
|
|
29
|
-
- `test/metrics.test.ts`
|
|
30
|
-
- `test/sync.test.ts`
|
|
31
|
-
- `test/background.test.ts`
|
|
32
|
-
|
|
33
|
-
**New command files (Markdown only):**
|
|
34
|
-
- `forgehive/commands/fh-deploy.md`
|
|
35
|
-
- `forgehive/commands/fh-test-this.md`
|
|
36
|
-
- `forgehive/commands/fh-docs.md`
|
|
37
|
-
|
|
38
|
-
**Modified files:**
|
|
39
|
-
- `forgehive/commands/fh-sprint.md` — add MCP Linear/Jira integration
|
|
40
|
-
- `forgehive/party/defaults.yaml` — add `model` field per agent set
|
|
41
|
-
- `src/cli.ts` — wire all new commands + fix version string to 0.7.0
|
|
42
|
-
- `package.json` — version 0.7.0
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## Task 1: CI Report Module (`src/ci.ts`)
|
|
47
|
-
|
|
48
|
-
**Files:**
|
|
49
|
-
- Create: `src/ci.ts`
|
|
50
|
-
- Create: `test/ci.test.ts`
|
|
51
|
-
|
|
52
|
-
- [ ] **Step 1: Write the failing test**
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
// test/ci.test.ts
|
|
56
|
-
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
57
|
-
import assert from "node:assert/strict";
|
|
58
|
-
import fs from "node:fs";
|
|
59
|
-
import path from "node:path";
|
|
60
|
-
import os from "node:os";
|
|
61
|
-
import { generateCiReport, formatCiReport, getGithubActionsTemplate } from "../src/ci.ts";
|
|
62
|
-
|
|
63
|
-
describe("generateCiReport()", () => {
|
|
64
|
-
let tmpDir: string;
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fh-ci-"));
|
|
67
|
-
fs.mkdirSync(path.join(tmpDir, ".forgehive"), { recursive: true });
|
|
68
|
-
fs.mkdirSync(path.join(tmpDir, "src"), { recursive: true });
|
|
69
|
-
});
|
|
70
|
-
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
71
|
-
|
|
72
|
-
it("returns pass status for a clean project", () => {
|
|
73
|
-
fs.writeFileSync(path.join(tmpDir, "src", "index.ts"), 'export const x = 1;\n', "utf8");
|
|
74
|
-
const report = generateCiReport(tmpDir, path.join(tmpDir, ".forgehive"));
|
|
75
|
-
assert.equal(report.status, "pass");
|
|
76
|
-
assert.equal(report.secrets.length, 0);
|
|
77
|
-
assert.equal(report.failedOn, null);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("returns fail status when secrets found", () => {
|
|
81
|
-
fs.writeFileSync(
|
|
82
|
-
path.join(tmpDir, "src", "config.ts"),
|
|
83
|
-
'const token = "ghp_XFAKEX1234567890abcdefghijklmnopqrstu";\n',
|
|
84
|
-
"utf8"
|
|
85
|
-
);
|
|
86
|
-
const report = generateCiReport(tmpDir, path.join(tmpDir, ".forgehive"));
|
|
87
|
-
assert.equal(report.status, "fail");
|
|
88
|
-
assert.ok(report.failedOn !== null);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("formatCiReport json produces valid JSON", () => {
|
|
92
|
-
fs.writeFileSync(path.join(tmpDir, "src", "index.ts"), "export const x = 1;\n", "utf8");
|
|
93
|
-
const report = generateCiReport(tmpDir, path.join(tmpDir, ".forgehive"));
|
|
94
|
-
const json = formatCiReport(report, "json");
|
|
95
|
-
assert.doesNotThrow(() => JSON.parse(json));
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("formatCiReport markdown includes status", () => {
|
|
99
|
-
fs.writeFileSync(path.join(tmpDir, "src", "index.ts"), "export const x = 1;\n", "utf8");
|
|
100
|
-
const report = generateCiReport(tmpDir, path.join(tmpDir, ".forgehive"));
|
|
101
|
-
const md = formatCiReport(report, "markdown");
|
|
102
|
-
assert.ok(md.includes("PASS") || md.includes("FAIL"));
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("getGithubActionsTemplate returns valid YAML string", () => {
|
|
106
|
-
const yaml = getGithubActionsTemplate();
|
|
107
|
-
assert.ok(yaml.includes("forgehive CI"));
|
|
108
|
-
assert.ok(yaml.includes("fh ci"));
|
|
109
|
-
assert.ok(yaml.includes("actions/checkout"));
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
cd /home/stefan/projekte/forgehive
|
|
118
|
-
node --import tsx/esm --test test/ci.test.ts
|
|
119
|
-
```
|
|
120
|
-
Expected: FAIL — `Cannot find module '../src/ci.ts'`
|
|
121
|
-
|
|
122
|
-
- [ ] **Step 3: Write the implementation**
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// src/ci.ts
|
|
126
|
-
import fs from "node:fs";
|
|
127
|
-
import path from "node:path";
|
|
128
|
-
import {
|
|
129
|
-
scanSecrets,
|
|
130
|
-
scanSast,
|
|
131
|
-
scanDeps,
|
|
132
|
-
type SecretFinding,
|
|
133
|
-
type SastFinding,
|
|
134
|
-
type DepVulnerability,
|
|
135
|
-
} from "./security-scan.ts";
|
|
136
|
-
|
|
137
|
-
export interface CiReport {
|
|
138
|
-
timestamp: string;
|
|
139
|
-
project: string;
|
|
140
|
-
status: "pass" | "fail";
|
|
141
|
-
secrets: SecretFinding[];
|
|
142
|
-
sast: SastFinding[];
|
|
143
|
-
deps: DepVulnerability[];
|
|
144
|
-
failedOn: string | null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function generateCiReport(
|
|
148
|
-
projectRoot: string,
|
|
149
|
-
forgehiveDir: string,
|
|
150
|
-
failOn: "critical" | "high" | "any" = "high"
|
|
151
|
-
): CiReport {
|
|
152
|
-
const secrets = scanSecrets(projectRoot);
|
|
153
|
-
const sast = scanSast(projectRoot);
|
|
154
|
-
const deps = scanDeps(projectRoot);
|
|
155
|
-
|
|
156
|
-
let failedOn: string | null = null;
|
|
157
|
-
|
|
158
|
-
if (secrets.length > 0) {
|
|
159
|
-
failedOn = "secrets";
|
|
160
|
-
} else if (
|
|
161
|
-
failOn === "critical" &&
|
|
162
|
-
(sast.some((f) => f.severity === "CRITICAL") ||
|
|
163
|
-
deps.some((d) => d.severity === "critical"))
|
|
164
|
-
) {
|
|
165
|
-
failedOn = "critical-severity";
|
|
166
|
-
} else if (
|
|
167
|
-
failOn === "high" &&
|
|
168
|
-
(sast.some((f) => f.severity === "CRITICAL" || f.severity === "HIGH") ||
|
|
169
|
-
deps.some((d) => d.severity === "critical" || d.severity === "high"))
|
|
170
|
-
) {
|
|
171
|
-
failedOn = "high-severity";
|
|
172
|
-
} else if (failOn === "any" && (sast.length > 0 || deps.length > 0)) {
|
|
173
|
-
failedOn = "findings";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
timestamp: new Date().toISOString(),
|
|
178
|
-
project: path.basename(projectRoot),
|
|
179
|
-
status: failedOn ? "fail" : "pass",
|
|
180
|
-
secrets,
|
|
181
|
-
sast,
|
|
182
|
-
deps,
|
|
183
|
-
failedOn,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function formatCiReport(
|
|
188
|
-
report: CiReport,
|
|
189
|
-
format: "json" | "markdown" = "markdown"
|
|
190
|
-
): string {
|
|
191
|
-
if (format === "json") return JSON.stringify(report, null, 2);
|
|
192
|
-
|
|
193
|
-
const lines: string[] = [];
|
|
194
|
-
lines.push("# forgehive CI Report");
|
|
195
|
-
lines.push(`**Project:** ${report.project}`);
|
|
196
|
-
lines.push(
|
|
197
|
-
`**Status:** ${report.status === "pass" ? "✅ PASS" : "❌ FAIL"}`
|
|
198
|
-
);
|
|
199
|
-
lines.push(`**Time:** ${report.timestamp}`);
|
|
200
|
-
lines.push("");
|
|
201
|
-
|
|
202
|
-
if (report.secrets.length > 0) {
|
|
203
|
-
lines.push("## Secrets Found");
|
|
204
|
-
for (const s of report.secrets)
|
|
205
|
-
lines.push(
|
|
206
|
-
`- **${s.patternName}** in \`${s.file}:${s.line}\` — \`${s.match}\``
|
|
207
|
-
);
|
|
208
|
-
lines.push("");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (report.sast.length > 0) {
|
|
212
|
-
lines.push("## SAST Findings");
|
|
213
|
-
for (const f of report.sast)
|
|
214
|
-
lines.push(
|
|
215
|
-
`- **${f.severity}** \`${f.ruleId}\` in \`${f.file}:${f.line}\` — ${f.snippet.slice(0, 80)}`
|
|
216
|
-
);
|
|
217
|
-
lines.push("");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (report.deps.length > 0) {
|
|
221
|
-
lines.push("## Dependency Vulnerabilities");
|
|
222
|
-
for (const d of report.deps)
|
|
223
|
-
lines.push(
|
|
224
|
-
`- **${d.severity}** ${d.name}@${d.version} — ${d.advisory}`
|
|
225
|
-
);
|
|
226
|
-
lines.push("");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (report.status === "pass") lines.push("✅ All checks passed.");
|
|
230
|
-
|
|
231
|
-
return lines.join("\n");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function getGithubActionsTemplate(): string {
|
|
235
|
-
return `name: forgehive CI
|
|
236
|
-
|
|
237
|
-
on:
|
|
238
|
-
pull_request:
|
|
239
|
-
branches: [main, master]
|
|
240
|
-
push:
|
|
241
|
-
branches: [main, master]
|
|
242
|
-
|
|
243
|
-
jobs:
|
|
244
|
-
security:
|
|
245
|
-
runs-on: ubuntu-latest
|
|
246
|
-
steps:
|
|
247
|
-
- uses: actions/checkout@v4
|
|
248
|
-
- uses: actions/setup-node@v4
|
|
249
|
-
with:
|
|
250
|
-
node-version: '20'
|
|
251
|
-
- run: npm install -g forgehive
|
|
252
|
-
- run: fh ci --format json --fail-on high
|
|
253
|
-
- name: Upload CI report
|
|
254
|
-
if: always()
|
|
255
|
-
uses: actions/upload-artifact@v4
|
|
256
|
-
with:
|
|
257
|
-
name: forgehive-ci-report
|
|
258
|
-
path: .forgehive/ci-report.json
|
|
259
|
-
`;
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
264
|
-
|
|
265
|
-
```bash
|
|
266
|
-
node --import tsx/esm --test test/ci.test.ts
|
|
267
|
-
```
|
|
268
|
-
Expected: 5 passing
|
|
269
|
-
|
|
270
|
-
- [ ] **Step 5: Wire `fh ci` into cli.ts**
|
|
271
|
-
|
|
272
|
-
Add import at top of `src/cli.ts`:
|
|
273
|
-
```typescript
|
|
274
|
-
import { generateCiReport, formatCiReport, getGithubActionsTemplate } from "./ci.ts";
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Add command handler before the `} else {` catch-all block:
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
} else if (command === "ci") {
|
|
281
|
-
const format = rest.includes("--format") ? rest[rest.indexOf("--format") + 1] as "json" | "markdown" : "markdown";
|
|
282
|
-
const failOnArg = rest.includes("--fail-on") ? rest[rest.indexOf("--fail-on") + 1] as "critical" | "high" | "any" : "high";
|
|
283
|
-
const initFlag = rest.includes("--init");
|
|
284
|
-
|
|
285
|
-
if (initFlag) {
|
|
286
|
-
const ghDir = path.join(projectRoot, ".github", "workflows");
|
|
287
|
-
fs.mkdirSync(ghDir, { recursive: true });
|
|
288
|
-
const outPath = path.join(ghDir, "forgehive.yml");
|
|
289
|
-
fs.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
|
|
290
|
-
console.log(`✔ GitHub Actions workflow geschrieben: ${outPath}`);
|
|
291
|
-
} else {
|
|
292
|
-
const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
|
|
293
|
-
const output = formatCiReport(report, format);
|
|
294
|
-
console.log(output);
|
|
295
|
-
if (format === "json") {
|
|
296
|
-
fs.mkdirSync(forgehiveDir, { recursive: true });
|
|
297
|
-
fs.writeFileSync(path.join(forgehiveDir, "ci-report.json"), output, "utf8");
|
|
298
|
-
}
|
|
299
|
-
if (report.status === "fail") process.exit(1);
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Also update the help text at the end of cli.ts to add `ci [--format json|markdown] [--fail-on critical|high|any] [--init]`.
|
|
304
|
-
|
|
305
|
-
- [ ] **Step 6: Commit**
|
|
306
|
-
|
|
307
|
-
```bash
|
|
308
|
-
git add src/ci.ts test/ci.test.ts src/cli.ts
|
|
309
|
-
git commit -m "feat: add fh ci command with GitHub Actions template"
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
---
|
|
313
|
-
|
|
314
|
-
## Task 2: Codebase Map (`src/map.ts`)
|
|
315
|
-
|
|
316
|
-
**Files:**
|
|
317
|
-
- Create: `src/map.ts`
|
|
318
|
-
- Create: `test/map.test.ts`
|
|
319
|
-
|
|
320
|
-
- [ ] **Step 1: Write the failing test**
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
// test/map.test.ts
|
|
324
|
-
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
325
|
-
import assert from "node:assert/strict";
|
|
326
|
-
import fs from "node:fs";
|
|
327
|
-
import path from "node:path";
|
|
328
|
-
import os from "node:os";
|
|
329
|
-
import { generateMap, formatMap } from "../src/map.ts";
|
|
330
|
-
|
|
331
|
-
describe("generateMap()", () => {
|
|
332
|
-
let tmpDir: string;
|
|
333
|
-
beforeEach(() => {
|
|
334
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fh-map-"));
|
|
335
|
-
fs.mkdirSync(path.join(tmpDir, "src"), { recursive: true });
|
|
336
|
-
});
|
|
337
|
-
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
338
|
-
|
|
339
|
-
it("returns file list for a project with source files", () => {
|
|
340
|
-
fs.writeFileSync(path.join(tmpDir, "src", "a.ts"), 'import { b } from "./b.ts";\n', "utf8");
|
|
341
|
-
fs.writeFileSync(path.join(tmpDir, "src", "b.ts"), "export const b = 1;\n", "utf8");
|
|
342
|
-
const map = generateMap(tmpDir);
|
|
343
|
-
assert.ok(map.files.length >= 2);
|
|
344
|
-
assert.ok(map.files.some((f) => f.path.includes("a.ts")));
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("detects imports between files", () => {
|
|
348
|
-
fs.writeFileSync(path.join(tmpDir, "src", "a.ts"), 'import { b } from "./b.ts";\n', "utf8");
|
|
349
|
-
fs.writeFileSync(path.join(tmpDir, "src", "b.ts"), "export const b = 1;\n", "utf8");
|
|
350
|
-
const map = generateMap(tmpDir);
|
|
351
|
-
const aFile = map.files.find((f) => f.path.includes("a.ts"));
|
|
352
|
-
assert.ok(aFile?.imports.some((i) => i.includes("b.ts")));
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("formatMap produces markdown with file tree", () => {
|
|
356
|
-
fs.writeFileSync(path.join(tmpDir, "src", "index.ts"), "export const x = 1;\n", "utf8");
|
|
357
|
-
const map = generateMap(tmpDir);
|
|
358
|
-
const md = formatMap(map);
|
|
359
|
-
assert.ok(md.includes("## Files"));
|
|
360
|
-
assert.ok(md.includes("index.ts"));
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("counts lines per file", () => {
|
|
364
|
-
fs.writeFileSync(path.join(tmpDir, "src", "big.ts"), "const a = 1;\nconst b = 2;\nconst c = 3;\n", "utf8");
|
|
365
|
-
const map = generateMap(tmpDir);
|
|
366
|
-
const bigFile = map.files.find((f) => f.path.includes("big.ts"));
|
|
367
|
-
assert.equal(bigFile?.lines, 3);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
373
|
-
|
|
374
|
-
```bash
|
|
375
|
-
node --import tsx/esm --test test/map.test.ts
|
|
376
|
-
```
|
|
377
|
-
Expected: FAIL — `Cannot find module '../src/map.ts'`
|
|
378
|
-
|
|
379
|
-
- [ ] **Step 3: Write the implementation**
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
// src/map.ts
|
|
383
|
-
import fs from "node:fs";
|
|
384
|
-
import path from "node:path";
|
|
385
|
-
|
|
386
|
-
export interface MapFile {
|
|
387
|
-
path: string;
|
|
388
|
-
relativePath: string;
|
|
389
|
-
lines: number;
|
|
390
|
-
imports: string[];
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export interface CodeMap {
|
|
394
|
-
projectRoot: string;
|
|
395
|
-
generatedAt: string;
|
|
396
|
-
files: MapFile[];
|
|
397
|
-
totalFiles: number;
|
|
398
|
-
totalLines: number;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const MAP_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"];
|
|
402
|
-
const IGNORE_DIRS = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
|
|
403
|
-
const IMPORT_PATTERNS = [
|
|
404
|
-
/^import\s+.*?from\s+['"]([^'"]+)['"]/gm,
|
|
405
|
-
/^import\s+['"]([^'"]+)['"]/gm,
|
|
406
|
-
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
|
|
407
|
-
];
|
|
408
|
-
|
|
409
|
-
function extractImports(content: string): string[] {
|
|
410
|
-
const imports: string[] = [];
|
|
411
|
-
for (const pattern of IMPORT_PATTERNS) {
|
|
412
|
-
pattern.lastIndex = 0;
|
|
413
|
-
let m: RegExpExecArray | null;
|
|
414
|
-
while ((m = pattern.exec(content)) !== null) {
|
|
415
|
-
if (m[1].startsWith(".")) imports.push(m[1]);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return [...new Set(imports)];
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function walkFiles(dir: string): string[] {
|
|
422
|
-
const results: string[] = [];
|
|
423
|
-
function walk(current: string) {
|
|
424
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
425
|
-
if (IGNORE_DIRS.includes(entry.name)) continue;
|
|
426
|
-
const full = path.join(current, entry.name);
|
|
427
|
-
if (entry.isDirectory()) walk(full);
|
|
428
|
-
else if (entry.isFile() && MAP_EXTS.includes(path.extname(entry.name)))
|
|
429
|
-
results.push(full);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
walk(dir);
|
|
433
|
-
return results;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
export function generateMap(projectRoot: string): CodeMap {
|
|
437
|
-
const filePaths = walkFiles(projectRoot);
|
|
438
|
-
const files: MapFile[] = filePaths.map((filePath) => {
|
|
439
|
-
let content = "";
|
|
440
|
-
try { content = fs.readFileSync(filePath, "utf8"); } catch { /* skip */ }
|
|
441
|
-
const lines = content.split("\n").length;
|
|
442
|
-
const imports = extractImports(content);
|
|
443
|
-
return {
|
|
444
|
-
path: filePath,
|
|
445
|
-
relativePath: path.relative(projectRoot, filePath),
|
|
446
|
-
lines,
|
|
447
|
-
imports,
|
|
448
|
-
};
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
files.sort((a, b) => b.lines - a.lines);
|
|
452
|
-
|
|
453
|
-
return {
|
|
454
|
-
projectRoot,
|
|
455
|
-
generatedAt: new Date().toISOString(),
|
|
456
|
-
files,
|
|
457
|
-
totalFiles: files.length,
|
|
458
|
-
totalLines: files.reduce((sum, f) => sum + f.lines, 0),
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
export function formatMap(map: CodeMap): string {
|
|
463
|
-
const lines: string[] = [];
|
|
464
|
-
lines.push("# Codebase Map");
|
|
465
|
-
lines.push(`Generated: ${map.generatedAt}`);
|
|
466
|
-
lines.push(`Total: ${map.totalFiles} files, ${map.totalLines} lines`);
|
|
467
|
-
lines.push("");
|
|
468
|
-
lines.push("## Files");
|
|
469
|
-
lines.push("");
|
|
470
|
-
lines.push("| File | Lines | Imports |");
|
|
471
|
-
lines.push("|---|---|---|");
|
|
472
|
-
for (const f of map.files) {
|
|
473
|
-
const imps = f.imports.length > 0 ? f.imports.slice(0, 3).join(", ") : "—";
|
|
474
|
-
lines.push(`| \`${f.relativePath}\` | ${f.lines} | ${imps} |`);
|
|
475
|
-
}
|
|
476
|
-
lines.push("");
|
|
477
|
-
lines.push("## Most Imported");
|
|
478
|
-
lines.push("");
|
|
479
|
-
const importCounts: Record<string, number> = {};
|
|
480
|
-
for (const f of map.files) {
|
|
481
|
-
for (const imp of f.imports) {
|
|
482
|
-
importCounts[imp] = (importCounts[imp] || 0) + 1;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const sorted = Object.entries(importCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
486
|
-
if (sorted.length > 0) {
|
|
487
|
-
for (const [imp, count] of sorted)
|
|
488
|
-
lines.push(`- \`${imp}\` — imported ${count}×`);
|
|
489
|
-
} else {
|
|
490
|
-
lines.push("No internal imports detected.");
|
|
491
|
-
}
|
|
492
|
-
return lines.join("\n");
|
|
493
|
-
}
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
497
|
-
|
|
498
|
-
```bash
|
|
499
|
-
node --import tsx/esm --test test/map.test.ts
|
|
500
|
-
```
|
|
501
|
-
Expected: 4 passing
|
|
502
|
-
|
|
503
|
-
- [ ] **Step 5: Wire `fh map` in cli.ts**
|
|
504
|
-
|
|
505
|
-
Add import:
|
|
506
|
-
```typescript
|
|
507
|
-
import { generateMap, formatMap } from "./map.ts";
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
Add handler:
|
|
511
|
-
```typescript
|
|
512
|
-
} else if (command === "map") {
|
|
513
|
-
const map = generateMap(projectRoot);
|
|
514
|
-
const md = formatMap(map);
|
|
515
|
-
const mapPath = path.join(forgehiveDir, "map.md");
|
|
516
|
-
fs.mkdirSync(forgehiveDir, { recursive: true });
|
|
517
|
-
fs.writeFileSync(mapPath, md, "utf8");
|
|
518
|
-
console.log(md);
|
|
519
|
-
console.log(`\n✔ Codebase-Map gespeichert: ${mapPath}`);
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
- [ ] **Step 6: Commit**
|
|
523
|
-
|
|
524
|
-
```bash
|
|
525
|
-
git add src/map.ts test/map.test.ts src/cli.ts
|
|
526
|
-
git commit -m "feat: add fh map command for codebase structure visualization"
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
---
|
|
530
|
-
|
|
531
|
-
## Task 3: Onboarding Document (`src/onboard.ts`)
|
|
532
|
-
|
|
533
|
-
**Files:**
|
|
534
|
-
- Create: `src/onboard.ts`
|
|
535
|
-
- Create: `test/onboard.test.ts`
|
|
536
|
-
|
|
537
|
-
- [ ] **Step 1: Write the failing test**
|
|
538
|
-
|
|
539
|
-
```typescript
|
|
540
|
-
// test/onboard.test.ts
|
|
541
|
-
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
542
|
-
import assert from "node:assert/strict";
|
|
543
|
-
import fs from "node:fs";
|
|
544
|
-
import path from "node:path";
|
|
545
|
-
import os from "node:os";
|
|
546
|
-
import { generateOnboardingDoc } from "../src/onboard.ts";
|
|
547
|
-
|
|
548
|
-
describe("generateOnboardingDoc()", () => {
|
|
549
|
-
let tmpDir: string;
|
|
550
|
-
beforeEach(() => {
|
|
551
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fh-onboard-"));
|
|
552
|
-
fs.mkdirSync(path.join(tmpDir, ".forgehive", "memory"), { recursive: true });
|
|
553
|
-
fs.mkdirSync(path.join(tmpDir, ".forgehive", "harness"), { recursive: true });
|
|
554
|
-
fs.writeFileSync(
|
|
555
|
-
path.join(tmpDir, ".forgehive", "capabilities.yaml"),
|
|
556
|
-
"status: confirmed\nlanguage: typescript\nframework: node\n",
|
|
557
|
-
"utf8"
|
|
558
|
-
);
|
|
559
|
-
fs.writeFileSync(
|
|
560
|
-
path.join(tmpDir, ".forgehive", "memory", "project.md"),
|
|
561
|
-
"# Projektkontext\n\nDies ist ein Test-Projekt.\n",
|
|
562
|
-
"utf8"
|
|
563
|
-
);
|
|
564
|
-
});
|
|
565
|
-
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
566
|
-
|
|
567
|
-
it("generates a markdown document", () => {
|
|
568
|
-
const doc = generateOnboardingDoc(tmpDir, path.join(tmpDir, ".forgehive"));
|
|
569
|
-
assert.ok(typeof doc === "string");
|
|
570
|
-
assert.ok(doc.length > 0);
|
|
571
|
-
assert.ok(doc.includes("#"));
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it("includes project name", () => {
|
|
575
|
-
const doc = generateOnboardingDoc(tmpDir, path.join(tmpDir, ".forgehive"));
|
|
576
|
-
assert.ok(doc.includes(path.basename(tmpDir)));
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("includes stack info from capabilities.yaml", () => {
|
|
580
|
-
const doc = generateOnboardingDoc(tmpDir, path.join(tmpDir, ".forgehive"));
|
|
581
|
-
assert.ok(doc.includes("typescript") || doc.includes("TypeScript"));
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it("includes memory content", () => {
|
|
585
|
-
const doc = generateOnboardingDoc(tmpDir, path.join(tmpDir, ".forgehive"));
|
|
586
|
-
assert.ok(doc.includes("Test-Projekt"));
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
592
|
-
|
|
593
|
-
```bash
|
|
594
|
-
node --import tsx/esm --test test/onboard.test.ts
|
|
595
|
-
```
|
|
596
|
-
Expected: FAIL — module not found
|
|
597
|
-
|
|
598
|
-
- [ ] **Step 3: Write the implementation**
|
|
599
|
-
|
|
600
|
-
```typescript
|
|
601
|
-
// src/onboard.ts
|
|
602
|
-
import fs from "node:fs";
|
|
603
|
-
import path from "node:path";
|
|
604
|
-
import { spawnSync } from "node:child_process";
|
|
605
|
-
import yaml from "js-yaml";
|
|
606
|
-
|
|
607
|
-
function getRecentCommits(projectRoot: string, n = 20): string[] {
|
|
608
|
-
const result = spawnSync("git", ["log", `--oneline`, `-${n}`], {
|
|
609
|
-
cwd: projectRoot,
|
|
610
|
-
encoding: "utf8",
|
|
611
|
-
});
|
|
612
|
-
if (result.status !== 0) return [];
|
|
613
|
-
return result.stdout.trim().split("\n").filter(Boolean);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function readCapabilities(forgehiveDir: string): Record<string, unknown> {
|
|
617
|
-
const capPath = path.join(forgehiveDir, "capabilities.yaml");
|
|
618
|
-
if (!fs.existsSync(capPath)) return {};
|
|
619
|
-
try {
|
|
620
|
-
return (yaml.load(fs.readFileSync(capPath, "utf8")) as Record<string, unknown>) ?? {};
|
|
621
|
-
} catch {
|
|
622
|
-
return {};
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function readMemoryFiles(forgehiveDir: string): Record<string, string> {
|
|
627
|
-
const memDir = path.join(forgehiveDir, "memory");
|
|
628
|
-
if (!fs.existsSync(memDir)) return {};
|
|
629
|
-
const result: Record<string, string> = {};
|
|
630
|
-
for (const f of fs.readdirSync(memDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md")) {
|
|
631
|
-
result[f] = fs.readFileSync(path.join(memDir, f), "utf8");
|
|
632
|
-
}
|
|
633
|
-
return result;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function listAdrs(forgehiveDir: string): string[] {
|
|
637
|
-
const adrsDir = path.join(forgehiveDir, "memory", "adrs");
|
|
638
|
-
if (!fs.existsSync(adrsDir)) return [];
|
|
639
|
-
return fs.readdirSync(adrsDir).filter((f) => f.endsWith(".md"));
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
export function generateOnboardingDoc(
|
|
643
|
-
projectRoot: string,
|
|
644
|
-
forgehiveDir: string
|
|
645
|
-
): string {
|
|
646
|
-
const projectName = path.basename(projectRoot);
|
|
647
|
-
const caps = readCapabilities(forgehiveDir);
|
|
648
|
-
const memFiles = readMemoryFiles(forgehiveDir);
|
|
649
|
-
const commits = getRecentCommits(projectRoot);
|
|
650
|
-
const adrs = listAdrs(forgehiveDir);
|
|
651
|
-
|
|
652
|
-
const lines: string[] = [];
|
|
653
|
-
|
|
654
|
-
lines.push(`# Onboarding: ${projectName}`);
|
|
655
|
-
lines.push("");
|
|
656
|
-
lines.push(`> Generated by forgehive on ${new Date().toISOString().slice(0, 10)}`);
|
|
657
|
-
lines.push("");
|
|
658
|
-
|
|
659
|
-
lines.push("## Tech Stack");
|
|
660
|
-
lines.push("");
|
|
661
|
-
if (caps.language) lines.push(`- **Language:** ${caps.language}`);
|
|
662
|
-
if (caps.framework) lines.push(`- **Framework:** ${caps.framework}`);
|
|
663
|
-
if (caps.packageManager) lines.push(`- **Package Manager:** ${caps.packageManager}`);
|
|
664
|
-
if (caps.testFramework) lines.push(`- **Tests:** ${caps.testFramework}`);
|
|
665
|
-
if ((caps as any).entryPoints) {
|
|
666
|
-
const eps = (caps as any).entryPoints;
|
|
667
|
-
if (Array.isArray(eps) && eps.length > 0)
|
|
668
|
-
lines.push(`- **Entry Points:** ${eps.join(", ")}`);
|
|
669
|
-
}
|
|
670
|
-
lines.push("");
|
|
671
|
-
|
|
672
|
-
lines.push("## Getting Started");
|
|
673
|
-
lines.push("");
|
|
674
|
-
lines.push("```bash");
|
|
675
|
-
lines.push("# Clone the repo");
|
|
676
|
-
lines.push(`git clone <repo-url>`);
|
|
677
|
-
lines.push(`cd ${projectName}`);
|
|
678
|
-
lines.push("");
|
|
679
|
-
|
|
680
|
-
const pm = (caps.packageManager as string) || "npm";
|
|
681
|
-
lines.push(`# Install dependencies`);
|
|
682
|
-
lines.push(pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm install" : "npm install");
|
|
683
|
-
lines.push("```");
|
|
684
|
-
lines.push("");
|
|
685
|
-
|
|
686
|
-
if (memFiles["project.md"]) {
|
|
687
|
-
lines.push("## Project Context");
|
|
688
|
-
lines.push("");
|
|
689
|
-
const projectContent = memFiles["project.md"]
|
|
690
|
-
.replace(/^---[\s\S]*?---\n/m, "")
|
|
691
|
-
.trim();
|
|
692
|
-
lines.push(projectContent);
|
|
693
|
-
lines.push("");
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (memFiles["stack.md"]) {
|
|
697
|
-
lines.push("## Stack Notes");
|
|
698
|
-
lines.push("");
|
|
699
|
-
const stackContent = memFiles["stack.md"]
|
|
700
|
-
.replace(/^---[\s\S]*?---\n/m, "")
|
|
701
|
-
.trim();
|
|
702
|
-
lines.push(stackContent);
|
|
703
|
-
lines.push("");
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (adrs.length > 0) {
|
|
707
|
-
lines.push("## Architecture Decisions");
|
|
708
|
-
lines.push("");
|
|
709
|
-
for (const adr of adrs) lines.push(`- ${adr.replace(".md", "")}`);
|
|
710
|
-
lines.push("");
|
|
711
|
-
lines.push(`See \`.forgehive/memory/adrs/\` for full decision records.`);
|
|
712
|
-
lines.push("");
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
if (commits.length > 0) {
|
|
716
|
-
lines.push("## Recent Activity");
|
|
717
|
-
lines.push("");
|
|
718
|
-
for (const c of commits.slice(0, 10)) lines.push(`- ${c}`);
|
|
719
|
-
lines.push("");
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
lines.push("## forgehive");
|
|
723
|
-
lines.push("");
|
|
724
|
-
lines.push("This project uses [forgehive](https://www.npmjs.com/package/forgehive) for AI-assisted development.");
|
|
725
|
-
lines.push("");
|
|
726
|
-
lines.push("```bash");
|
|
727
|
-
lines.push("npm install -g forgehive");
|
|
728
|
-
lines.push("fh status # check project health");
|
|
729
|
-
lines.push("```");
|
|
730
|
-
|
|
731
|
-
return lines.join("\n");
|
|
732
|
-
}
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
736
|
-
|
|
737
|
-
```bash
|
|
738
|
-
node --import tsx/esm --test test/onboard.test.ts
|
|
739
|
-
```
|
|
740
|
-
Expected: 4 passing
|
|
741
|
-
|
|
742
|
-
- [ ] **Step 5: Wire `fh onboard` in cli.ts**
|
|
743
|
-
|
|
744
|
-
Add import:
|
|
745
|
-
```typescript
|
|
746
|
-
import { generateOnboardingDoc } from "./onboard.ts";
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
Add handler:
|
|
750
|
-
```typescript
|
|
751
|
-
} else if (command === "onboard") {
|
|
752
|
-
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
753
|
-
const outputPath = outputArg ?? path.join(projectRoot, "ONBOARDING.md");
|
|
754
|
-
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
755
|
-
fs.writeFileSync(outputPath, doc, "utf8");
|
|
756
|
-
console.log(`✔ Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
- [ ] **Step 6: Commit**
|
|
760
|
-
|
|
761
|
-
```bash
|
|
762
|
-
git add src/onboard.ts test/onboard.test.ts src/cli.ts
|
|
763
|
-
git commit -m "feat: add fh onboard command for new team member documentation"
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
---
|
|
767
|
-
|
|
768
|
-
## Task 4: Semantic Changelog (`src/changelog.ts`)
|
|
769
|
-
|
|
770
|
-
**Files:**
|
|
771
|
-
- Create: `src/changelog.ts`
|
|
772
|
-
- Create: `test/changelog.test.ts`
|
|
773
|
-
|
|
774
|
-
- [ ] **Step 1: Write the failing test**
|
|
775
|
-
|
|
776
|
-
```typescript
|
|
777
|
-
// test/changelog.test.ts
|
|
778
|
-
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
779
|
-
import assert from "node:assert/strict";
|
|
780
|
-
import fs from "node:fs";
|
|
781
|
-
import path from "node:path";
|
|
782
|
-
import os from "node:os";
|
|
783
|
-
import { parseGitLog, groupByType, formatChangelog } from "../src/changelog.ts";
|
|
784
|
-
|
|
785
|
-
describe("parseGitLog()", () => {
|
|
786
|
-
it("parses conventional commit messages", () => {
|
|
787
|
-
const rawLog = [
|
|
788
|
-
"abc1234 feat: add login page",
|
|
789
|
-
"def5678 fix: correct button color",
|
|
790
|
-
"ghi9012 chore: update dependencies",
|
|
791
|
-
].join("\n");
|
|
792
|
-
const commits = parseGitLog(rawLog);
|
|
793
|
-
assert.equal(commits.length, 3);
|
|
794
|
-
assert.equal(commits[0].type, "feat");
|
|
795
|
-
assert.equal(commits[0].message, "add login page");
|
|
796
|
-
assert.equal(commits[0].hash, "abc1234");
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
it("handles non-conventional commits as 'other'", () => {
|
|
800
|
-
const rawLog = "abc1234 Updated something random";
|
|
801
|
-
const commits = parseGitLog(rawLog);
|
|
802
|
-
assert.equal(commits[0].type, "other");
|
|
803
|
-
assert.equal(commits[0].message, "Updated something random");
|
|
804
|
-
});
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
describe("groupByType()", () => {
|
|
808
|
-
it("groups commits by type", () => {
|
|
809
|
-
const commits = [
|
|
810
|
-
{ hash: "a", type: "feat", message: "feature one", scope: null },
|
|
811
|
-
{ hash: "b", type: "feat", message: "feature two", scope: null },
|
|
812
|
-
{ hash: "c", type: "fix", message: "bug fix", scope: null },
|
|
813
|
-
];
|
|
814
|
-
const groups = groupByType(commits);
|
|
815
|
-
assert.equal(groups["feat"].length, 2);
|
|
816
|
-
assert.equal(groups["fix"].length, 1);
|
|
817
|
-
});
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
describe("formatChangelog()", () => {
|
|
821
|
-
it("produces markdown with version header", () => {
|
|
822
|
-
const commits = [
|
|
823
|
-
{ hash: "a", type: "feat", message: "new feature", scope: null },
|
|
824
|
-
];
|
|
825
|
-
const md = formatChangelog(commits, "1.0.0");
|
|
826
|
-
assert.ok(md.includes("1.0.0"));
|
|
827
|
-
assert.ok(md.includes("Added") || md.includes("feat"));
|
|
828
|
-
assert.ok(md.includes("new feature"));
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
it("returns empty message when no commits", () => {
|
|
832
|
-
const md = formatChangelog([], "1.0.0");
|
|
833
|
-
assert.ok(md.includes("1.0.0"));
|
|
834
|
-
});
|
|
835
|
-
});
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
839
|
-
|
|
840
|
-
```bash
|
|
841
|
-
node --import tsx/esm --test test/changelog.test.ts
|
|
842
|
-
```
|
|
843
|
-
Expected: FAIL
|
|
844
|
-
|
|
845
|
-
- [ ] **Step 3: Write the implementation**
|
|
846
|
-
|
|
847
|
-
```typescript
|
|
848
|
-
// src/changelog.ts
|
|
849
|
-
import { spawnSync } from "node:child_process";
|
|
850
|
-
|
|
851
|
-
export interface Commit {
|
|
852
|
-
hash: string;
|
|
853
|
-
type: string;
|
|
854
|
-
scope: string | null;
|
|
855
|
-
message: string;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
const TYPE_LABELS: Record<string, string> = {
|
|
859
|
-
feat: "Added",
|
|
860
|
-
fix: "Fixed",
|
|
861
|
-
perf: "Improved",
|
|
862
|
-
refactor: "Changed",
|
|
863
|
-
chore: "Maintenance",
|
|
864
|
-
docs: "Documentation",
|
|
865
|
-
test: "Tests",
|
|
866
|
-
ci: "CI",
|
|
867
|
-
other: "Other",
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
export function parseGitLog(rawLog: string): Commit[] {
|
|
871
|
-
return rawLog
|
|
872
|
-
.split("\n")
|
|
873
|
-
.map((line) => line.trim())
|
|
874
|
-
.filter(Boolean)
|
|
875
|
-
.map((line) => {
|
|
876
|
-
const spaceIdx = line.indexOf(" ");
|
|
877
|
-
const hash = line.slice(0, spaceIdx);
|
|
878
|
-
const rest = line.slice(spaceIdx + 1);
|
|
879
|
-
const conventionalMatch = rest.match(/^(\w+)(?:\(([^)]+)\))?\s*:\s*(.+)$/);
|
|
880
|
-
if (conventionalMatch) {
|
|
881
|
-
return {
|
|
882
|
-
hash,
|
|
883
|
-
type: conventionalMatch[1],
|
|
884
|
-
scope: conventionalMatch[2] ?? null,
|
|
885
|
-
message: conventionalMatch[3],
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
return { hash, type: "other", scope: null, message: rest };
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
export function groupByType(commits: Commit[]): Record<string, Commit[]> {
|
|
893
|
-
const groups: Record<string, Commit[]> = {};
|
|
894
|
-
for (const commit of commits) {
|
|
895
|
-
if (!groups[commit.type]) groups[commit.type] = [];
|
|
896
|
-
groups[commit.type].push(commit);
|
|
897
|
-
}
|
|
898
|
-
return groups;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
export function formatChangelog(commits: Commit[], version: string): string {
|
|
902
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
903
|
-
const lines: string[] = [];
|
|
904
|
-
lines.push(`## [${version}] — ${date}`);
|
|
905
|
-
lines.push("");
|
|
906
|
-
|
|
907
|
-
if (commits.length === 0) {
|
|
908
|
-
lines.push("No changes.");
|
|
909
|
-
return lines.join("\n");
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const groups = groupByType(commits);
|
|
913
|
-
const typeOrder = ["feat", "fix", "perf", "refactor", "docs", "chore", "test", "ci", "other"];
|
|
914
|
-
|
|
915
|
-
for (const type of typeOrder) {
|
|
916
|
-
if (!groups[type] || groups[type].length === 0) continue;
|
|
917
|
-
const label = TYPE_LABELS[type] ?? type;
|
|
918
|
-
lines.push(`### ${label}`);
|
|
919
|
-
lines.push("");
|
|
920
|
-
for (const c of groups[type]) {
|
|
921
|
-
const scope = c.scope ? `**${c.scope}:** ` : "";
|
|
922
|
-
lines.push(`- ${scope}${c.message} (\`${c.hash}\`)`);
|
|
923
|
-
}
|
|
924
|
-
lines.push("");
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
return lines.join("\n");
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
export function getGitLogSince(projectRoot: string, since?: string): string {
|
|
931
|
-
const args = ["log", "--oneline", "--no-merges"];
|
|
932
|
-
if (since) args.push(`${since}..HEAD`);
|
|
933
|
-
const result = spawnSync("git", args, { cwd: projectRoot, encoding: "utf8" });
|
|
934
|
-
if (result.status !== 0) return "";
|
|
935
|
-
return result.stdout.trim();
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
export function getLatestTag(projectRoot: string): string | null {
|
|
939
|
-
const result = spawnSync("git", ["describe", "--tags", "--abbrev=0"], {
|
|
940
|
-
cwd: projectRoot,
|
|
941
|
-
encoding: "utf8",
|
|
942
|
-
});
|
|
943
|
-
if (result.status !== 0) return null;
|
|
944
|
-
return result.stdout.trim() || null;
|
|
945
|
-
}
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
949
|
-
|
|
950
|
-
```bash
|
|
951
|
-
node --import tsx/esm --test test/changelog.test.ts
|
|
952
|
-
```
|
|
953
|
-
Expected: 5 passing
|
|
954
|
-
|
|
955
|
-
- [ ] **Step 5: Wire `fh changelog` in cli.ts**
|
|
956
|
-
|
|
957
|
-
Add import:
|
|
958
|
-
```typescript
|
|
959
|
-
import { parseGitLog, formatChangelog, getGitLogSince, getLatestTag } from "./changelog.ts";
|
|
960
|
-
```
|
|
961
|
-
|
|
962
|
-
Add handler:
|
|
963
|
-
```typescript
|
|
964
|
-
} else if (command === "changelog") {
|
|
965
|
-
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
966
|
-
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
967
|
-
const since = sinceArg ?? getLatestTag(projectRoot) ?? undefined;
|
|
968
|
-
const rawLog = getGitLogSince(projectRoot, since);
|
|
969
|
-
const commits = parseGitLog(rawLog);
|
|
970
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8").replace(/^\s*\/\/.*$/gm, ""));
|
|
971
|
-
const version = pkg.version ?? "unreleased";
|
|
972
|
-
const md = formatChangelog(commits, version);
|
|
973
|
-
const outputPath = outputArg ?? path.join(projectRoot, "CHANGELOG.md");
|
|
974
|
-
let existing = "";
|
|
975
|
-
if (fs.existsSync(outputPath)) existing = fs.readFileSync(outputPath, "utf8");
|
|
976
|
-
fs.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
977
|
-
console.log(`✔ CHANGELOG.md aktualisiert: ${outputPath}`);
|
|
978
|
-
console.log(` ${commits.length} Commits verarbeitet`);
|
|
979
|
-
```
|
|
980
|
-
|
|
981
|
-
- [ ] **Step 6: Commit**
|
|
982
|
-
|
|
983
|
-
```bash
|
|
984
|
-
git add src/changelog.ts test/changelog.test.ts src/cli.ts
|
|
985
|
-
git commit -m "feat: add fh changelog command for semantic changelog generation"
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
---
|
|
989
|
-
|
|
990
|
-
## Task 5: Developer Metrics (`src/metrics.ts`)
|
|
991
|
-
|
|
992
|
-
**Files:**
|
|
993
|
-
- Create: `src/metrics.ts`
|
|
994
|
-
- Create: `test/metrics.test.ts`
|
|
995
|
-
|
|
996
|
-
- [ ] **Step 1: Write the failing test**
|
|
997
|
-
|
|
998
|
-
```typescript
|
|
999
|
-
// test/metrics.test.ts
|
|
1000
|
-
import { describe, it } from "node:test";
|
|
1001
|
-
import assert from "node:assert/strict";
|
|
1002
|
-
import { parseCommitStats, formatMetrics } from "../src/metrics.ts";
|
|
1003
|
-
|
|
1004
|
-
describe("parseCommitStats()", () => {
|
|
1005
|
-
it("counts commits by author", () => {
|
|
1006
|
-
const rawLog = [
|
|
1007
|
-
"2026-05-14 Alice feat: feature one",
|
|
1008
|
-
"2026-05-13 Bob fix: bug fix",
|
|
1009
|
-
"2026-05-12 Alice chore: cleanup",
|
|
1010
|
-
].join("\n");
|
|
1011
|
-
const stats = parseCommitStats(rawLog);
|
|
1012
|
-
assert.equal(stats.totalCommits, 3);
|
|
1013
|
-
assert.equal(stats.byAuthor["Alice"], 2);
|
|
1014
|
-
assert.equal(stats.byAuthor["Bob"], 1);
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
it("counts by commit type", () => {
|
|
1018
|
-
const rawLog = [
|
|
1019
|
-
"2026-05-14 Alice feat: one",
|
|
1020
|
-
"2026-05-13 Alice feat: two",
|
|
1021
|
-
"2026-05-12 Alice fix: three",
|
|
1022
|
-
].join("\n");
|
|
1023
|
-
const stats = parseCommitStats(rawLog);
|
|
1024
|
-
assert.equal(stats.byType["feat"], 2);
|
|
1025
|
-
assert.equal(stats.byType["fix"], 1);
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
it("returns zeros for empty log", () => {
|
|
1029
|
-
const stats = parseCommitStats("");
|
|
1030
|
-
assert.equal(stats.totalCommits, 0);
|
|
1031
|
-
});
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
describe("formatMetrics()", () => {
|
|
1035
|
-
it("produces markdown report", () => {
|
|
1036
|
-
const stats = {
|
|
1037
|
-
totalCommits: 10,
|
|
1038
|
-
byAuthor: { Alice: 7, Bob: 3 },
|
|
1039
|
-
byType: { feat: 4, fix: 3, chore: 3 },
|
|
1040
|
-
dateRange: { from: "2026-05-01", to: "2026-05-14" },
|
|
1041
|
-
};
|
|
1042
|
-
const md = formatMetrics(stats, "my-project");
|
|
1043
|
-
assert.ok(md.includes("my-project"));
|
|
1044
|
-
assert.ok(md.includes("10"));
|
|
1045
|
-
assert.ok(md.includes("Alice"));
|
|
1046
|
-
});
|
|
1047
|
-
});
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
1051
|
-
|
|
1052
|
-
```bash
|
|
1053
|
-
node --import tsx/esm --test test/metrics.test.ts
|
|
1054
|
-
```
|
|
1055
|
-
Expected: FAIL
|
|
1056
|
-
|
|
1057
|
-
- [ ] **Step 3: Write the implementation**
|
|
1058
|
-
|
|
1059
|
-
```typescript
|
|
1060
|
-
// src/metrics.ts
|
|
1061
|
-
import { spawnSync } from "node:child_process";
|
|
1062
|
-
|
|
1063
|
-
export interface CommitStats {
|
|
1064
|
-
totalCommits: number;
|
|
1065
|
-
byAuthor: Record<string, number>;
|
|
1066
|
-
byType: Record<string, number>;
|
|
1067
|
-
dateRange: { from: string; to: string };
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
export function parseCommitStats(rawLog: string): CommitStats {
|
|
1071
|
-
const lines = rawLog.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1072
|
-
const byAuthor: Record<string, number> = {};
|
|
1073
|
-
const byType: Record<string, number> = {};
|
|
1074
|
-
const dates: string[] = [];
|
|
1075
|
-
|
|
1076
|
-
for (const line of lines) {
|
|
1077
|
-
const parts = line.split(" ");
|
|
1078
|
-
if (parts.length < 3) continue;
|
|
1079
|
-
const date = parts[0];
|
|
1080
|
-
const author = parts[1];
|
|
1081
|
-
const message = parts.slice(2).join(" ");
|
|
1082
|
-
|
|
1083
|
-
dates.push(date);
|
|
1084
|
-
byAuthor[author] = (byAuthor[author] || 0) + 1;
|
|
1085
|
-
|
|
1086
|
-
const typeMatch = message.match(/^(\w+)(?:\([^)]+\))?:/);
|
|
1087
|
-
const type = typeMatch ? typeMatch[1] : "other";
|
|
1088
|
-
byType[type] = (byType[type] || 0) + 1;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
const sortedDates = dates.sort();
|
|
1092
|
-
return {
|
|
1093
|
-
totalCommits: lines.length,
|
|
1094
|
-
byAuthor,
|
|
1095
|
-
byType,
|
|
1096
|
-
dateRange: {
|
|
1097
|
-
from: sortedDates[0] ?? "",
|
|
1098
|
-
to: sortedDates[sortedDates.length - 1] ?? "",
|
|
1099
|
-
},
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
export function formatMetrics(stats: CommitStats, projectName: string): string {
|
|
1104
|
-
const lines: string[] = [];
|
|
1105
|
-
lines.push(`# Developer Metrics: ${projectName}`);
|
|
1106
|
-
lines.push(`Generated: ${new Date().toISOString().slice(0, 10)}`);
|
|
1107
|
-
lines.push("");
|
|
1108
|
-
lines.push(`**Total commits:** ${stats.totalCommits}`);
|
|
1109
|
-
if (stats.dateRange.from)
|
|
1110
|
-
lines.push(`**Date range:** ${stats.dateRange.from} → ${stats.dateRange.to}`);
|
|
1111
|
-
lines.push("");
|
|
1112
|
-
|
|
1113
|
-
if (Object.keys(stats.byAuthor).length > 0) {
|
|
1114
|
-
lines.push("## Commits by Author");
|
|
1115
|
-
lines.push("");
|
|
1116
|
-
for (const [author, count] of Object.entries(stats.byAuthor).sort((a, b) => b[1] - a[1]))
|
|
1117
|
-
lines.push(`- **${author}:** ${count}`);
|
|
1118
|
-
lines.push("");
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
if (Object.keys(stats.byType).length > 0) {
|
|
1122
|
-
lines.push("## Commits by Type");
|
|
1123
|
-
lines.push("");
|
|
1124
|
-
for (const [type, count] of Object.entries(stats.byType).sort((a, b) => b[1] - a[1]))
|
|
1125
|
-
lines.push(`- **${type}:** ${count}`);
|
|
1126
|
-
lines.push("");
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
return lines.join("\n");
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
export function getMetricsGitLog(projectRoot: string, since?: string): string {
|
|
1133
|
-
const args = ["log", "--no-merges", "--format=%as %an %s"];
|
|
1134
|
-
if (since) args.push(`--since=${since}`);
|
|
1135
|
-
const result = spawnSync("git", args, { cwd: projectRoot, encoding: "utf8" });
|
|
1136
|
-
if (result.status !== 0) return "";
|
|
1137
|
-
return result.stdout.trim();
|
|
1138
|
-
}
|
|
1139
|
-
```
|
|
1140
|
-
|
|
1141
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
1142
|
-
|
|
1143
|
-
```bash
|
|
1144
|
-
node --import tsx/esm --test test/metrics.test.ts
|
|
1145
|
-
```
|
|
1146
|
-
Expected: 4 passing
|
|
1147
|
-
|
|
1148
|
-
- [ ] **Step 5: Wire `fh metrics` in cli.ts**
|
|
1149
|
-
|
|
1150
|
-
Add import:
|
|
1151
|
-
```typescript
|
|
1152
|
-
import { parseCommitStats, formatMetrics, getMetricsGitLog } from "./metrics.ts";
|
|
1153
|
-
```
|
|
1154
|
-
|
|
1155
|
-
Add handler:
|
|
1156
|
-
```typescript
|
|
1157
|
-
} else if (command === "metrics") {
|
|
1158
|
-
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : undefined;
|
|
1159
|
-
const rawLog = getMetricsGitLog(projectRoot, sinceArg);
|
|
1160
|
-
const stats = parseCommitStats(rawLog);
|
|
1161
|
-
const projectName = require("path").basename(projectRoot);
|
|
1162
|
-
const md = formatMetrics(stats, path.basename(projectRoot));
|
|
1163
|
-
const metricsPath = path.join(forgehiveDir, "metrics.md");
|
|
1164
|
-
fs.mkdirSync(forgehiveDir, { recursive: true });
|
|
1165
|
-
fs.writeFileSync(metricsPath, md, "utf8");
|
|
1166
|
-
console.log(md);
|
|
1167
|
-
console.log(`\n✔ Metrics gespeichert: ${metricsPath}`);
|
|
1168
|
-
```
|
|
1169
|
-
|
|
1170
|
-
- [ ] **Step 6: Commit**
|
|
1171
|
-
|
|
1172
|
-
```bash
|
|
1173
|
-
git add src/metrics.ts test/metrics.test.ts src/cli.ts
|
|
1174
|
-
git commit -m "feat: add fh metrics command for developer productivity metrics"
|
|
1175
|
-
```
|
|
1176
|
-
|
|
1177
|
-
---
|
|
1178
|
-
|
|
1179
|
-
## Task 6: Team Sync (`src/sync.ts`)
|
|
1180
|
-
|
|
1181
|
-
**Files:**
|
|
1182
|
-
- Create: `src/sync.ts`
|
|
1183
|
-
- Create: `test/sync.test.ts`
|
|
1184
|
-
|
|
1185
|
-
- [ ] **Step 1: Write the failing test**
|
|
1186
|
-
|
|
1187
|
-
```typescript
|
|
1188
|
-
// test/sync.test.ts
|
|
1189
|
-
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
1190
|
-
import assert from "node:assert/strict";
|
|
1191
|
-
import fs from "node:fs";
|
|
1192
|
-
import path from "node:path";
|
|
1193
|
-
import os from "node:os";
|
|
1194
|
-
import { spawnSync } from "node:child_process";
|
|
1195
|
-
import { getSyncStatus } from "../src/sync.ts";
|
|
1196
|
-
|
|
1197
|
-
describe("getSyncStatus()", () => {
|
|
1198
|
-
let tmpDir: string;
|
|
1199
|
-
beforeEach(() => {
|
|
1200
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fh-sync-"));
|
|
1201
|
-
fs.mkdirSync(path.join(tmpDir, ".forgehive", "memory"), { recursive: true });
|
|
1202
|
-
fs.writeFileSync(
|
|
1203
|
-
path.join(tmpDir, ".forgehive", "memory", "project.md"),
|
|
1204
|
-
"# Project\n\nTest content.\n",
|
|
1205
|
-
"utf8"
|
|
1206
|
-
);
|
|
1207
|
-
spawnSync("git", ["init"], { cwd: tmpDir });
|
|
1208
|
-
spawnSync("git", ["config", "user.email", "test@test.com"], { cwd: tmpDir });
|
|
1209
|
-
spawnSync("git", ["config", "user.name", "Test"], { cwd: tmpDir });
|
|
1210
|
-
});
|
|
1211
|
-
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
1212
|
-
|
|
1213
|
-
it("returns memory file count", () => {
|
|
1214
|
-
const status = getSyncStatus(path.join(tmpDir, ".forgehive"));
|
|
1215
|
-
assert.equal(status.files, 1);
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
|
-
it("returns false for hasRemote when no remote configured", () => {
|
|
1219
|
-
const status = getSyncStatus(path.join(tmpDir, ".forgehive"));
|
|
1220
|
-
assert.equal(status.hasRemote, false);
|
|
1221
|
-
});
|
|
1222
|
-
});
|
|
1223
|
-
```
|
|
1224
|
-
|
|
1225
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
1226
|
-
|
|
1227
|
-
```bash
|
|
1228
|
-
node --import tsx/esm --test test/sync.test.ts
|
|
1229
|
-
```
|
|
1230
|
-
Expected: FAIL
|
|
1231
|
-
|
|
1232
|
-
- [ ] **Step 3: Write the implementation**
|
|
1233
|
-
|
|
1234
|
-
```typescript
|
|
1235
|
-
// src/sync.ts
|
|
1236
|
-
import fs from "node:fs";
|
|
1237
|
-
import path from "node:path";
|
|
1238
|
-
import { spawnSync } from "node:child_process";
|
|
1239
|
-
|
|
1240
|
-
export interface SyncStatus {
|
|
1241
|
-
files: number;
|
|
1242
|
-
hasRemote: boolean;
|
|
1243
|
-
remote: string | null;
|
|
1244
|
-
branch: string | null;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
export function getSyncStatus(forgehiveDir: string): SyncStatus {
|
|
1248
|
-
const memDir = path.join(forgehiveDir, "memory");
|
|
1249
|
-
const files = fs.existsSync(memDir)
|
|
1250
|
-
? fs.readdirSync(memDir).filter((f) => f.endsWith(".md")).length
|
|
1251
|
-
: 0;
|
|
1252
|
-
|
|
1253
|
-
const projectRoot = path.dirname(forgehiveDir);
|
|
1254
|
-
const configResult = spawnSync(
|
|
1255
|
-
"git",
|
|
1256
|
-
["config", "--get", "forgehive.sync-remote"],
|
|
1257
|
-
{ cwd: projectRoot, encoding: "utf8" }
|
|
1258
|
-
);
|
|
1259
|
-
const remote = configResult.status === 0 ? configResult.stdout.trim() : null;
|
|
1260
|
-
const branchResult = spawnSync(
|
|
1261
|
-
"git",
|
|
1262
|
-
["config", "--get", "forgehive.sync-branch"],
|
|
1263
|
-
{ cwd: projectRoot, encoding: "utf8" }
|
|
1264
|
-
);
|
|
1265
|
-
const branch = branchResult.status === 0 ? branchResult.stdout.trim() : null;
|
|
1266
|
-
|
|
1267
|
-
return { files, hasRemote: remote !== null, remote, branch };
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
export interface SyncPushResult {
|
|
1271
|
-
success: boolean;
|
|
1272
|
-
message: string;
|
|
1273
|
-
filesCommitted: number;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
export function pushSync(
|
|
1277
|
-
forgehiveDir: string,
|
|
1278
|
-
remote = "origin",
|
|
1279
|
-
branch = "forgehive-memory"
|
|
1280
|
-
): SyncPushResult {
|
|
1281
|
-
const projectRoot = path.dirname(forgehiveDir);
|
|
1282
|
-
const memDir = path.join(forgehiveDir, "memory");
|
|
1283
|
-
|
|
1284
|
-
if (!fs.existsSync(memDir)) {
|
|
1285
|
-
return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const files = fs.readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
1289
|
-
if (files.length === 0) {
|
|
1290
|
-
return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
// Stage memory files
|
|
1294
|
-
const addResult = spawnSync(
|
|
1295
|
-
"git",
|
|
1296
|
-
["add", path.join(".forgehive", "memory")],
|
|
1297
|
-
{ cwd: projectRoot, encoding: "utf8" }
|
|
1298
|
-
);
|
|
1299
|
-
if (addResult.status !== 0) {
|
|
1300
|
-
return { success: false, message: `git add failed: ${addResult.stderr}`, filesCommitted: 0 };
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const commitResult = spawnSync(
|
|
1304
|
-
"git",
|
|
1305
|
-
["commit", "-m", `chore: sync forgehive memory [${new Date().toISOString()}]`],
|
|
1306
|
-
{ cwd: projectRoot, encoding: "utf8" }
|
|
1307
|
-
);
|
|
1308
|
-
// Exit 1 with "nothing to commit" is OK
|
|
1309
|
-
const commitOk =
|
|
1310
|
-
commitResult.status === 0 ||
|
|
1311
|
-
(commitResult.stdout + commitResult.stderr).includes("nothing to commit");
|
|
1312
|
-
|
|
1313
|
-
if (!commitOk) {
|
|
1314
|
-
return { success: false, message: `git commit failed: ${commitResult.stderr}`, filesCommitted: 0 };
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const pushResult = spawnSync(
|
|
1318
|
-
"git",
|
|
1319
|
-
["push", remote, `HEAD:${branch}`],
|
|
1320
|
-
{ cwd: projectRoot, encoding: "utf8" }
|
|
1321
|
-
);
|
|
1322
|
-
if (pushResult.status !== 0) {
|
|
1323
|
-
return { success: false, message: `git push failed: ${pushResult.stderr}`, filesCommitted: 0 };
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
// Save config for future syncs
|
|
1327
|
-
spawnSync("git", ["config", "forgehive.sync-remote", remote], { cwd: projectRoot });
|
|
1328
|
-
spawnSync("git", ["config", "forgehive.sync-branch", branch], { cwd: projectRoot });
|
|
1329
|
-
|
|
1330
|
-
return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
export interface SyncPullResult {
|
|
1334
|
-
success: boolean;
|
|
1335
|
-
message: string;
|
|
1336
|
-
filesImported: string[];
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
export function pullSync(
|
|
1340
|
-
forgehiveDir: string,
|
|
1341
|
-
remote = "origin",
|
|
1342
|
-
branch = "forgehive-memory"
|
|
1343
|
-
): SyncPullResult {
|
|
1344
|
-
const projectRoot = path.dirname(forgehiveDir);
|
|
1345
|
-
const memDir = path.join(forgehiveDir, "memory");
|
|
1346
|
-
fs.mkdirSync(memDir, { recursive: true });
|
|
1347
|
-
|
|
1348
|
-
const fetchResult = spawnSync(
|
|
1349
|
-
"git",
|
|
1350
|
-
["fetch", remote, branch],
|
|
1351
|
-
{ cwd: projectRoot, encoding: "utf8" }
|
|
1352
|
-
);
|
|
1353
|
-
if (fetchResult.status !== 0) {
|
|
1354
|
-
return { success: false, message: `git fetch failed: ${fetchResult.stderr}`, filesImported: [] };
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// Get list of memory files from remote branch
|
|
1358
|
-
const listResult = spawnSync(
|
|
1359
|
-
"git",
|
|
1360
|
-
["ls-tree", "--name-only", `${remote}/${branch}`, ".forgehive/memory/"],
|
|
1361
|
-
{ cwd: projectRoot, encoding: "utf8" }
|
|
1362
|
-
);
|
|
1363
|
-
if (listResult.status !== 0) {
|
|
1364
|
-
return { success: false, message: "Remote branch hat keine Memory-Dateien.", filesImported: [] };
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
|
|
1368
|
-
const imported: string[] = [];
|
|
1369
|
-
|
|
1370
|
-
for (const remotePath of remoteFiles) {
|
|
1371
|
-
const filename = path.basename(remotePath);
|
|
1372
|
-
const localPath = path.join(memDir, filename);
|
|
1373
|
-
if (!fs.existsSync(localPath)) {
|
|
1374
|
-
const contentResult = spawnSync(
|
|
1375
|
-
"git",
|
|
1376
|
-
["show", `${remote}/${branch}:${remotePath}`],
|
|
1377
|
-
{ cwd: projectRoot, encoding: "utf8" }
|
|
1378
|
-
);
|
|
1379
|
-
if (contentResult.status === 0) {
|
|
1380
|
-
fs.writeFileSync(localPath, contentResult.stdout, "utf8");
|
|
1381
|
-
imported.push(filename);
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
return {
|
|
1387
|
-
success: true,
|
|
1388
|
-
message: imported.length > 0
|
|
1389
|
-
? `${imported.length} neue Dateien importiert.`
|
|
1390
|
-
: "Keine neuen Dateien (bereits aktuell).",
|
|
1391
|
-
filesImported: imported,
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
```
|
|
1395
|
-
|
|
1396
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
1397
|
-
|
|
1398
|
-
```bash
|
|
1399
|
-
node --import tsx/esm --test test/sync.test.ts
|
|
1400
|
-
```
|
|
1401
|
-
Expected: 2 passing
|
|
1402
|
-
|
|
1403
|
-
- [ ] **Step 5: Wire `fh sync` in cli.ts**
|
|
1404
|
-
|
|
1405
|
-
Add import:
|
|
1406
|
-
```typescript
|
|
1407
|
-
import { pushSync, pullSync, getSyncStatus } from "./sync.ts";
|
|
1408
|
-
```
|
|
1409
|
-
|
|
1410
|
-
Add handler:
|
|
1411
|
-
```typescript
|
|
1412
|
-
} else if (command === "sync") {
|
|
1413
|
-
const remoteArg = rest.includes("--remote") ? rest[rest.indexOf("--remote") + 1] : "origin";
|
|
1414
|
-
const branchArg = rest.includes("--branch") ? rest[rest.indexOf("--branch") + 1] : "forgehive-memory";
|
|
1415
|
-
|
|
1416
|
-
if (subcommand === "push") {
|
|
1417
|
-
const result = pushSync(forgehiveDir, remoteArg, branchArg);
|
|
1418
|
-
console.log(result.success ? `✔ ${result.message}` : `✗ ${result.message}`);
|
|
1419
|
-
if (!result.success) process.exit(1);
|
|
1420
|
-
} else if (subcommand === "pull") {
|
|
1421
|
-
const result = pullSync(forgehiveDir, remoteArg, branchArg);
|
|
1422
|
-
console.log(result.success ? `✔ ${result.message}` : `✗ ${result.message}`);
|
|
1423
|
-
if (result.filesImported.length > 0)
|
|
1424
|
-
console.log(" Importiert:", result.filesImported.join(", "));
|
|
1425
|
-
if (!result.success) process.exit(1);
|
|
1426
|
-
} else {
|
|
1427
|
-
const status = getSyncStatus(forgehiveDir);
|
|
1428
|
-
console.log(`Memory: ${status.files} Dateien`);
|
|
1429
|
-
console.log(status.hasRemote
|
|
1430
|
-
? `Remote: ${status.remote}/${status.branch}`
|
|
1431
|
-
: "Kein Remote konfiguriert. Nutze: fh sync push [--remote origin --branch forgehive-memory]");
|
|
1432
|
-
}
|
|
1433
|
-
```
|
|
1434
|
-
|
|
1435
|
-
- [ ] **Step 6: Commit**
|
|
1436
|
-
|
|
1437
|
-
```bash
|
|
1438
|
-
git add src/sync.ts test/sync.test.ts src/cli.ts
|
|
1439
|
-
git commit -m "feat: add fh sync push/pull for team memory sharing via git"
|
|
1440
|
-
```
|
|
1441
|
-
|
|
1442
|
-
---
|
|
1443
|
-
|
|
1444
|
-
## Task 7: Background Agent Execution (`src/background.ts`)
|
|
1445
|
-
|
|
1446
|
-
**Files:**
|
|
1447
|
-
- Create: `src/background.ts`
|
|
1448
|
-
- Create: `test/background.test.ts`
|
|
1449
|
-
|
|
1450
|
-
- [ ] **Step 1: Write the failing test**
|
|
1451
|
-
|
|
1452
|
-
```typescript
|
|
1453
|
-
// test/background.test.ts
|
|
1454
|
-
import { describe, it } from "node:test";
|
|
1455
|
-
import assert from "node:assert/strict";
|
|
1456
|
-
import { buildAgentPrompt, resolveAgent } from "../src/background.ts";
|
|
1457
|
-
|
|
1458
|
-
describe("buildAgentPrompt()", () => {
|
|
1459
|
-
it("includes the issue URL in the prompt", () => {
|
|
1460
|
-
const prompt = buildAgentPrompt("https://github.com/user/repo/issues/42", "kai");
|
|
1461
|
-
assert.ok(prompt.includes("https://github.com/user/repo/issues/42"));
|
|
1462
|
-
});
|
|
1463
|
-
|
|
1464
|
-
it("includes the agent name in the prompt", () => {
|
|
1465
|
-
const prompt = buildAgentPrompt("https://linear.app/team/issue/ENG-42", "vera");
|
|
1466
|
-
assert.ok(prompt.includes("vera") || prompt.includes("Vera") || prompt.includes("Security"));
|
|
1467
|
-
});
|
|
1468
|
-
|
|
1469
|
-
it("returns a non-empty string", () => {
|
|
1470
|
-
const prompt = buildAgentPrompt("https://example.com/issue/1", "kai");
|
|
1471
|
-
assert.ok(prompt.length > 50);
|
|
1472
|
-
});
|
|
1473
|
-
});
|
|
1474
|
-
|
|
1475
|
-
describe("resolveAgent()", () => {
|
|
1476
|
-
it("resolves kai for code tasks by default", () => {
|
|
1477
|
-
const agent = resolveAgent(undefined);
|
|
1478
|
-
assert.equal(agent, "kai");
|
|
1479
|
-
});
|
|
1480
|
-
|
|
1481
|
-
it("resolves vera for security label", () => {
|
|
1482
|
-
const agent = resolveAgent("security");
|
|
1483
|
-
assert.equal(agent, "vera");
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
it("resolves nora for research label", () => {
|
|
1487
|
-
const agent = resolveAgent("research");
|
|
1488
|
-
assert.equal(agent, "nora");
|
|
1489
|
-
});
|
|
1490
|
-
});
|
|
1491
|
-
```
|
|
1492
|
-
|
|
1493
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
1494
|
-
|
|
1495
|
-
```bash
|
|
1496
|
-
node --import tsx/esm --test test/background.test.ts
|
|
1497
|
-
```
|
|
1498
|
-
Expected: FAIL
|
|
1499
|
-
|
|
1500
|
-
- [ ] **Step 3: Write the implementation**
|
|
1501
|
-
|
|
1502
|
-
```typescript
|
|
1503
|
-
// src/background.ts
|
|
1504
|
-
import fs from "node:fs";
|
|
1505
|
-
import path from "node:path";
|
|
1506
|
-
import { spawn } from "node:child_process";
|
|
1507
|
-
|
|
1508
|
-
const AGENT_ROLES: Record<string, string> = {
|
|
1509
|
-
kai: "Senior Engineer — implements features and fixes bugs",
|
|
1510
|
-
sam: "QA & Test Architect — writes tests and checks quality",
|
|
1511
|
-
viktor: "System Architect — designs architecture and reviews structure",
|
|
1512
|
-
vera: "Security Analyst — reviews for OWASP, GDPR, auth issues",
|
|
1513
|
-
nora: "Senior Research Analyst — researches options and summarizes findings",
|
|
1514
|
-
eli: "Technical Writer — writes documentation and changelogs",
|
|
1515
|
-
suki: "UX Designer — reviews UI/UX and user flows",
|
|
1516
|
-
remy: "Creative Strategist — explores creative approaches",
|
|
1517
|
-
};
|
|
1518
|
-
|
|
1519
|
-
const LABEL_TO_AGENT: Record<string, string> = {
|
|
1520
|
-
security: "vera",
|
|
1521
|
-
research: "nora",
|
|
1522
|
-
docs: "eli",
|
|
1523
|
-
documentation: "eli",
|
|
1524
|
-
design: "suki",
|
|
1525
|
-
ux: "suki",
|
|
1526
|
-
architecture: "viktor",
|
|
1527
|
-
arch: "viktor",
|
|
1528
|
-
test: "sam",
|
|
1529
|
-
qa: "sam",
|
|
1530
|
-
};
|
|
1531
|
-
|
|
1532
|
-
export function resolveAgent(label: string | undefined): string {
|
|
1533
|
-
if (!label) return "kai";
|
|
1534
|
-
return LABEL_TO_AGENT[label.toLowerCase()] ?? "kai";
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
export function buildAgentPrompt(issueUrl: string, agentId: string): string {
|
|
1538
|
-
const role = AGENT_ROLES[agentId] ?? "Senior Engineer";
|
|
1539
|
-
return `You are ${agentId} — ${role}.
|
|
1540
|
-
|
|
1541
|
-
Your task: Resolve the following issue autonomously.
|
|
1542
|
-
|
|
1543
|
-
Issue URL: ${issueUrl}
|
|
1544
|
-
|
|
1545
|
-
Instructions:
|
|
1546
|
-
1. Read the issue at the URL above (use web fetch or gh CLI if available)
|
|
1547
|
-
2. Understand what needs to be done
|
|
1548
|
-
3. Implement the solution following the project's conventions (read .forgehive/ for context)
|
|
1549
|
-
4. Write tests for any new code
|
|
1550
|
-
5. Commit your changes with a descriptive message
|
|
1551
|
-
6. Report what you did
|
|
1552
|
-
|
|
1553
|
-
Work autonomously. Do not ask for clarification — use your best judgment based on the issue description and codebase context.`;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
export interface BackgroundRunResult {
|
|
1557
|
-
pid: number | undefined;
|
|
1558
|
-
logFile: string;
|
|
1559
|
-
message: string;
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
export function runBackgroundAgent(
|
|
1563
|
-
forgehiveDir: string,
|
|
1564
|
-
issueUrl: string,
|
|
1565
|
-
agentId: string
|
|
1566
|
-
): BackgroundRunResult {
|
|
1567
|
-
const logsDir = path.join(forgehiveDir, "background-runs");
|
|
1568
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
1569
|
-
|
|
1570
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1571
|
-
const logFile = path.join(logsDir, `${agentId}-${timestamp}.log`);
|
|
1572
|
-
const prompt = buildAgentPrompt(issueUrl, agentId);
|
|
1573
|
-
|
|
1574
|
-
const logStream = fs.openSync(logFile, "w");
|
|
1575
|
-
const child = spawn(
|
|
1576
|
-
"claude",
|
|
1577
|
-
["-p", prompt, "--output-format", "text"],
|
|
1578
|
-
{
|
|
1579
|
-
cwd: path.dirname(forgehiveDir),
|
|
1580
|
-
detached: true,
|
|
1581
|
-
stdio: ["ignore", logStream, logStream],
|
|
1582
|
-
}
|
|
1583
|
-
);
|
|
1584
|
-
|
|
1585
|
-
child.unref();
|
|
1586
|
-
fs.closeSync(logStream);
|
|
1587
|
-
|
|
1588
|
-
return {
|
|
1589
|
-
pid: child.pid,
|
|
1590
|
-
logFile,
|
|
1591
|
-
message: `Agent ${agentId} gestartet (PID ${child.pid}). Log: ${logFile}`,
|
|
1592
|
-
};
|
|
1593
|
-
}
|
|
1594
|
-
```
|
|
1595
|
-
|
|
1596
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
1597
|
-
|
|
1598
|
-
```bash
|
|
1599
|
-
node --import tsx/esm --test test/background.test.ts
|
|
1600
|
-
```
|
|
1601
|
-
Expected: 5 passing
|
|
1602
|
-
|
|
1603
|
-
- [ ] **Step 5: Wire `fh run` in cli.ts**
|
|
1604
|
-
|
|
1605
|
-
Add import:
|
|
1606
|
-
```typescript
|
|
1607
|
-
import { runBackgroundAgent, resolveAgent } from "./background.ts";
|
|
1608
|
-
```
|
|
1609
|
-
|
|
1610
|
-
Add handler:
|
|
1611
|
-
```typescript
|
|
1612
|
-
} else if (command === "run") {
|
|
1613
|
-
const issueUrl = subcommand;
|
|
1614
|
-
if (!issueUrl) {
|
|
1615
|
-
console.error("Usage: fh run <issue-url> [--agent <name>]");
|
|
1616
|
-
process.exit(1);
|
|
1617
|
-
}
|
|
1618
|
-
const agentArg = rest.includes("--agent") ? rest[rest.indexOf("--agent") + 1] : undefined;
|
|
1619
|
-
const labelArg = rest.includes("--label") ? rest[rest.indexOf("--label") + 1] : undefined;
|
|
1620
|
-
const agentId = agentArg ?? resolveAgent(labelArg);
|
|
1621
|
-
const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
|
|
1622
|
-
console.log(`✔ ${result.message}`);
|
|
1623
|
-
console.log(` fh run status — aktive Sessions anzeigen`);
|
|
1624
|
-
```
|
|
1625
|
-
|
|
1626
|
-
- [ ] **Step 6: Commit**
|
|
1627
|
-
|
|
1628
|
-
```bash
|
|
1629
|
-
git add src/background.ts test/background.test.ts src/cli.ts
|
|
1630
|
-
git commit -m "feat: add fh run for background agent execution via claude -p"
|
|
1631
|
-
```
|
|
1632
|
-
|
|
1633
|
-
---
|
|
1634
|
-
|
|
1635
|
-
## Task 8: New Slash Commands (Markdown Files)
|
|
1636
|
-
|
|
1637
|
-
**Files:**
|
|
1638
|
-
- Create: `forgehive/commands/fh-deploy.md`
|
|
1639
|
-
- Create: `forgehive/commands/fh-test-this.md`
|
|
1640
|
-
- Create: `forgehive/commands/fh-docs.md`
|
|
1641
|
-
- Modify: `forgehive/commands/fh-sprint.md`
|
|
1642
|
-
|
|
1643
|
-
- [ ] **Step 1: Create `/fh-deploy`**
|
|
1644
|
-
|
|
1645
|
-
```markdown
|
|
1646
|
-
<!-- forgehive/commands/fh-deploy.md -->
|
|
1647
|
-
You are running a deployment workflow using ForgeHive.
|
|
1648
|
-
|
|
1649
|
-
## Deployment Protocol
|
|
1650
|
-
|
|
1651
|
-
### Step 1: Pre-deploy checks
|
|
1652
|
-
|
|
1653
|
-
1. Run the full test suite — stop if any test fails:
|
|
1654
|
-
- Node.js: `npm test`
|
|
1655
|
-
- Python: `pytest`
|
|
1656
|
-
- Go: `go test ./...`
|
|
1657
|
-
2. Run `fh security scan` — stop if CRITICAL or HIGH findings
|
|
1658
|
-
3. Show `git diff main...HEAD --stat` — confirm scope is as expected
|
|
1659
|
-
4. Check for uncommitted changes (`git status`) — commit or stash first
|
|
1660
|
-
|
|
1661
|
-
### Step 2: Build
|
|
1662
|
-
|
|
1663
|
-
Run the build command from `capabilities.yaml` or ask the user:
|
|
1664
|
-
- Node.js: `npm run build`
|
|
1665
|
-
- Python: detect from `pyproject.toml` or `setup.py`
|
|
1666
|
-
|
|
1667
|
-
Report bundle size and any build warnings.
|
|
1668
|
-
|
|
1669
|
-
### Step 3: Deploy to staging
|
|
1670
|
-
|
|
1671
|
-
Check `.forgehive/capabilities.yaml` for deploy configuration. If MCP is connected for the deploy service, use it. Otherwise ask: **"Wie deploye ich in die Staging-Umgebung?"**
|
|
1672
|
-
|
|
1673
|
-
After deploying, run smoke tests:
|
|
1674
|
-
- Check the health endpoint if one exists
|
|
1675
|
-
- Verify the app responds with 200
|
|
1676
|
-
|
|
1677
|
-
### Step 4: Confirm and promote to production
|
|
1678
|
-
|
|
1679
|
-
Ask: **"Staging sieht gut aus — soll ich in Production deployen?"**
|
|
1680
|
-
|
|
1681
|
-
Wait for explicit confirmation before proceeding.
|
|
1682
|
-
|
|
1683
|
-
### Step 5: Post-deploy
|
|
1684
|
-
|
|
1685
|
-
After successful production deploy:
|
|
1686
|
-
1. Create a git tag: `git tag v<version> && git push origin v<version>`
|
|
1687
|
-
2. Run `fh changelog` to update CHANGELOG.md
|
|
1688
|
-
3. Report: deployment complete, version, timestamp
|
|
1689
|
-
|
|
1690
|
-
### Rollback
|
|
1691
|
-
|
|
1692
|
-
If anything fails after production deploy:
|
|
1693
|
-
1. Identify the last known good version
|
|
1694
|
-
2. Trigger rollback via the deploy service
|
|
1695
|
-
3. Report what failed and why
|
|
1696
|
-
```
|
|
1697
|
-
|
|
1698
|
-
- [ ] **Step 2: Create `/fh-test-this`**
|
|
1699
|
-
|
|
1700
|
-
```markdown
|
|
1701
|
-
<!-- forgehive/commands/fh-test-this.md -->
|
|
1702
|
-
You are Sam — QA & Test Architect. Your job is to write tests for the current changes.
|
|
1703
|
-
|
|
1704
|
-
## Test-This Protocol
|
|
1705
|
-
|
|
1706
|
-
### Step 1: Identify what changed
|
|
1707
|
-
|
|
1708
|
-
1. Run `git diff HEAD --stat` to see which files changed
|
|
1709
|
-
2. Run `git diff HEAD` to see the actual changes
|
|
1710
|
-
3. Note: which functions/classes were added or modified?
|
|
1711
|
-
|
|
1712
|
-
### Step 2: Load testing skill
|
|
1713
|
-
|
|
1714
|
-
Read `.forgehive/skills/expert/testing-strategies.md` for the project's testing conventions.
|
|
1715
|
-
|
|
1716
|
-
Check which test framework is in use (from `capabilities.yaml`):
|
|
1717
|
-
- `node:test` → use `describe/it` with `node:assert/strict`
|
|
1718
|
-
- `jest` → use `describe/it` with `expect`
|
|
1719
|
-
- `pytest` → use `def test_` functions
|
|
1720
|
-
- `go test` → use `func TestX(t *testing.T)`
|
|
1721
|
-
|
|
1722
|
-
### Step 3: Write tests
|
|
1723
|
-
|
|
1724
|
-
For each changed function or class, write:
|
|
1725
|
-
1. A **happy path test** — normal input, expected output
|
|
1726
|
-
2. An **edge case test** — empty input, boundary values, null/undefined
|
|
1727
|
-
3. An **error case test** — invalid input, should throw or return error
|
|
1728
|
-
|
|
1729
|
-
Place tests in the appropriate test file (same name as source file, `test/` directory or `*.test.ts` suffix).
|
|
1730
|
-
|
|
1731
|
-
### Step 4: Run tests
|
|
1732
|
-
|
|
1733
|
-
Run the test suite and confirm all new tests pass:
|
|
1734
|
-
```bash
|
|
1735
|
-
npm test # or pytest, go test, etc.
|
|
1736
|
-
```
|
|
1737
|
-
|
|
1738
|
-
If tests fail, fix the implementation or the test until all pass.
|
|
1739
|
-
|
|
1740
|
-
### Step 5: Coverage check
|
|
1741
|
-
|
|
1742
|
-
If coverage tooling is available, run it and report the coverage percentage for the changed files.
|
|
1743
|
-
|
|
1744
|
-
### Step 6: Commit
|
|
1745
|
-
|
|
1746
|
-
```bash
|
|
1747
|
-
git add <test-files>
|
|
1748
|
-
git commit -m "test: add tests for <what you tested>"
|
|
1749
|
-
```
|
|
1750
|
-
```
|
|
1751
|
-
|
|
1752
|
-
- [ ] **Step 3: Create `/fh-docs`**
|
|
1753
|
-
|
|
1754
|
-
```markdown
|
|
1755
|
-
<!-- forgehive/commands/fh-docs.md -->
|
|
1756
|
-
You are Eli — Technical Writer. Your job is to write or update documentation.
|
|
1757
|
-
|
|
1758
|
-
## Documentation Protocol
|
|
1759
|
-
|
|
1760
|
-
Ask the user: **"Was soll ich dokumentieren?"**
|
|
1761
|
-
|
|
1762
|
-
Options:
|
|
1763
|
-
1. **README update** — reflect recent features/changes
|
|
1764
|
-
2. **API reference** — document endpoints or exported functions
|
|
1765
|
-
3. **CHANGELOG** — run `fh changelog` and review the output
|
|
1766
|
-
4. **ADR** — document an architecture decision (`fh memory adr "<title>"`)
|
|
1767
|
-
5. **Inline docs** — add JSDoc/docstrings to changed functions
|
|
1768
|
-
6. **ONBOARDING** — run `fh onboard` and review
|
|
1769
|
-
|
|
1770
|
-
### For README updates
|
|
1771
|
-
|
|
1772
|
-
1. Read the current `README.md`
|
|
1773
|
-
2. Read `.forgehive/memory/project.md` for context
|
|
1774
|
-
3. Read `git log --oneline -20` for recent changes
|
|
1775
|
-
4. Update sections that are outdated — do NOT rewrite sections that are current
|
|
1776
|
-
5. Add a section for any major features added since last README update
|
|
1777
|
-
|
|
1778
|
-
### For API reference
|
|
1779
|
-
|
|
1780
|
-
1. Find all exported functions in `src/` (TypeScript) or `__init__.py` (Python)
|
|
1781
|
-
2. For each exported function: name, parameters with types, return type, one-sentence description, example
|
|
1782
|
-
3. Write to `docs/API.md` or update existing file
|
|
1783
|
-
|
|
1784
|
-
### For inline documentation
|
|
1785
|
-
|
|
1786
|
-
1. Read the changed files from `git diff HEAD --name-only`
|
|
1787
|
-
2. For each public function missing a JSDoc/docstring: add a single-line description
|
|
1788
|
-
3. Only document the WHY when it's non-obvious — never document WHAT the code does
|
|
1789
|
-
|
|
1790
|
-
### Quality check
|
|
1791
|
-
|
|
1792
|
-
Before committing:
|
|
1793
|
-
- All links in Markdown files resolve
|
|
1794
|
-
- Code examples in docs are syntactically valid
|
|
1795
|
-
- No placeholder text ("TODO", "TBD", "...")
|
|
1796
|
-
```
|
|
1797
|
-
|
|
1798
|
-
- [ ] **Step 4: Update `/fh-sprint` with MCP awareness**
|
|
1799
|
-
|
|
1800
|
-
Replace the entire content of `forgehive/commands/fh-sprint.md` Step 2 ("Collect backlog items") section with this enhanced version that adds MCP integration:
|
|
1801
|
-
|
|
1802
|
-
In `forgehive/commands/fh-sprint.md`, replace the **Step 2** section:
|
|
1803
|
-
|
|
1804
|
-
```markdown
|
|
1805
|
-
### Step 2: Collect backlog items
|
|
1806
|
-
|
|
1807
|
-
Ask the user: **"Welche Items kommen in den Sprint? Liste sie auf — ein Item pro Zeile. Oder soll ich Linear/Jira laden?"**
|
|
1808
|
-
|
|
1809
|
-
**If Linear MCP is available** (check if `.mcp.json` contains `linear`):
|
|
1810
|
-
Use the Linear MCP tool to fetch open issues:
|
|
1811
|
-
```
|
|
1812
|
-
mcp__linear__list_issues({ state: "backlog", limit: 30 })
|
|
1813
|
-
```
|
|
1814
|
-
Show the fetched issues and ask: **"Welche davon kommen in den Sprint?"**
|
|
1815
|
-
|
|
1816
|
-
**If GitHub MCP is available** (check if `.mcp.json` contains `github`):
|
|
1817
|
-
```bash
|
|
1818
|
-
gh issue list --state open --label "sprint-candidate" --limit 20
|
|
1819
|
-
```
|
|
1820
|
-
|
|
1821
|
-
**If no MCP connected:**
|
|
1822
|
-
Accept free-text input — one item per line.
|
|
1823
|
-
```
|
|
1824
|
-
|
|
1825
|
-
- [ ] **Step 5: Commit**
|
|
1826
|
-
|
|
1827
|
-
```bash
|
|
1828
|
-
git add forgehive/commands/fh-deploy.md forgehive/commands/fh-test-this.md forgehive/commands/fh-docs.md forgehive/commands/fh-sprint.md
|
|
1829
|
-
git commit -m "feat: add /fh-deploy, /fh-test-this, /fh-docs slash commands; enhance /fh-sprint with MCP"
|
|
1830
|
-
```
|
|
1831
|
-
|
|
1832
|
-
---
|
|
1833
|
-
|
|
1834
|
-
## Task 9: Multi-Model Routing
|
|
1835
|
-
|
|
1836
|
-
**Files:**
|
|
1837
|
-
- Modify: `forgehive/party/defaults.yaml`
|
|
1838
|
-
- Modify: `src/cli.ts` (fix version string)
|
|
1839
|
-
|
|
1840
|
-
- [ ] **Step 1: Update defaults.yaml with model fields**
|
|
1841
|
-
|
|
1842
|
-
Replace `forgehive/party/defaults.yaml` with:
|
|
1843
|
-
|
|
1844
|
-
```yaml
|
|
1845
|
-
sets:
|
|
1846
|
-
design:
|
|
1847
|
-
agents: [suki, viktor]
|
|
1848
|
-
trigger: "/design-party"
|
|
1849
|
-
description: "UX + Architektur parallel"
|
|
1850
|
-
models:
|
|
1851
|
-
suki: claude-sonnet-4-6
|
|
1852
|
-
viktor: claude-opus-4-7
|
|
1853
|
-
build:
|
|
1854
|
-
agents: [viktor, kai, sam]
|
|
1855
|
-
trigger: "/party"
|
|
1856
|
-
description: "Architektur + Engineering + QA"
|
|
1857
|
-
models:
|
|
1858
|
-
viktor: claude-opus-4-7
|
|
1859
|
-
kai: claude-sonnet-4-6
|
|
1860
|
-
sam: claude-haiku-4-5
|
|
1861
|
-
review:
|
|
1862
|
-
agents: [kai, sam, eli]
|
|
1863
|
-
trigger: "/review-party"
|
|
1864
|
-
description: "Code Review + QA + Doku"
|
|
1865
|
-
models:
|
|
1866
|
-
kai: claude-opus-4-7
|
|
1867
|
-
sam: claude-sonnet-4-6
|
|
1868
|
-
eli: claude-sonnet-4-6
|
|
1869
|
-
full:
|
|
1870
|
-
agents: [nora, eli, remy, suki, viktor, kai, sam]
|
|
1871
|
-
trigger: "/full-party"
|
|
1872
|
-
description: "Alle Agenten"
|
|
1873
|
-
models:
|
|
1874
|
-
viktor: claude-opus-4-7
|
|
1875
|
-
kai: claude-opus-4-7
|
|
1876
|
-
nora: claude-sonnet-4-6
|
|
1877
|
-
eli: claude-sonnet-4-6
|
|
1878
|
-
sam: claude-sonnet-4-6
|
|
1879
|
-
suki: claude-sonnet-4-6
|
|
1880
|
-
remy: claude-haiku-4-5
|
|
1881
|
-
security:
|
|
1882
|
-
agents: [vera, sam]
|
|
1883
|
-
trigger: "/security-party"
|
|
1884
|
-
description: "Security Review + QA"
|
|
1885
|
-
models:
|
|
1886
|
-
vera: claude-opus-4-7
|
|
1887
|
-
sam: claude-sonnet-4-6
|
|
1888
|
-
```
|
|
1889
|
-
|
|
1890
|
-
- [ ] **Step 2: Fix version string in cli.ts**
|
|
1891
|
-
|
|
1892
|
-
In `src/cli.ts`, change line 35:
|
|
1893
|
-
```typescript
|
|
1894
|
-
// From:
|
|
1895
|
-
console.log("0.6.1");
|
|
1896
|
-
// To:
|
|
1897
|
-
console.log("0.7.0");
|
|
1898
|
-
```
|
|
1899
|
-
|
|
1900
|
-
- [ ] **Step 3: Update package.json version**
|
|
1901
|
-
|
|
1902
|
-
In `package.json`, change `"version": "0.6.3"` to `"version": "0.7.0"`.
|
|
1903
|
-
|
|
1904
|
-
- [ ] **Step 4: Commit**
|
|
1905
|
-
|
|
1906
|
-
```bash
|
|
1907
|
-
git add forgehive/party/defaults.yaml src/cli.ts package.json
|
|
1908
|
-
git commit -m "feat: add multi-model routing to party sets, bump to v0.7.0"
|
|
1909
|
-
```
|
|
1910
|
-
|
|
1911
|
-
---
|
|
1912
|
-
|
|
1913
|
-
## Task 10: Full Test Run, Build, and Publish
|
|
1914
|
-
|
|
1915
|
-
- [ ] **Step 1: Run all tests**
|
|
1916
|
-
|
|
1917
|
-
```bash
|
|
1918
|
-
cd /home/stefan/projekte/forgehive
|
|
1919
|
-
npm test
|
|
1920
|
-
```
|
|
1921
|
-
|
|
1922
|
-
Expected: All tests pass. Target: ~260+ tests (previous 217 + ~7 new test files × ~5 tests each).
|
|
1923
|
-
|
|
1924
|
-
If any test fails, fix it before proceeding.
|
|
1925
|
-
|
|
1926
|
-
- [ ] **Step 2: Type check**
|
|
1927
|
-
|
|
1928
|
-
```bash
|
|
1929
|
-
npm run typecheck
|
|
1930
|
-
```
|
|
1931
|
-
|
|
1932
|
-
Expected: 0 errors.
|
|
1933
|
-
|
|
1934
|
-
- [ ] **Step 3: Build**
|
|
1935
|
-
|
|
1936
|
-
```bash
|
|
1937
|
-
npm run build
|
|
1938
|
-
```
|
|
1939
|
-
|
|
1940
|
-
Expected: `dist/cli.js` built successfully, shebang on line 1 (`head -1 dist/cli.js` shows `#!/usr/bin/env node`).
|
|
1941
|
-
|
|
1942
|
-
- [ ] **Step 4: Update help text in cli.ts**
|
|
1943
|
-
|
|
1944
|
-
In `src/cli.ts`, update the unknown command handler's help text to include all new commands:
|
|
1945
|
-
|
|
1946
|
-
```typescript
|
|
1947
|
-
console.error("Verfügbar: init | confirm | rollback | scan --update | scan --check | status | " +
|
|
1948
|
-
"ci [--format json|markdown] [--fail-on critical|high|any] [--init] | " +
|
|
1949
|
-
"map | onboard [--output path] | changelog [--since tag] | metrics [--since date] | " +
|
|
1950
|
-
"sync [push|pull] [--remote origin --branch forgehive-memory] | " +
|
|
1951
|
-
"run <issue-url> [--agent <name>] | " +
|
|
1952
|
-
"cost [today|week|all] | cost --limit N --alert N | " +
|
|
1953
|
-
"memory [show|clean|export|prune|snapshot] | memory adr [list|<titel>] | " +
|
|
1954
|
-
"skills [list|regen|pull <url>] | party [--set <name>|run|status|cleanup] | " +
|
|
1955
|
-
"wire <service> | mcp [auth|search|add] | " +
|
|
1956
|
-
"security [scan|deps|report|audit|permissions]");
|
|
1957
|
-
```
|
|
1958
|
-
|
|
1959
|
-
- [ ] **Step 5: Final commit**
|
|
1960
|
-
|
|
1961
|
-
```bash
|
|
1962
|
-
git add src/cli.ts
|
|
1963
|
-
git commit -m "chore: update help text for v0.7.0 commands"
|
|
1964
|
-
```
|
|
1965
|
-
|
|
1966
|
-
- [ ] **Step 6: Publish**
|
|
1967
|
-
|
|
1968
|
-
```bash
|
|
1969
|
-
npm publish
|
|
1970
|
-
```
|
|
1971
|
-
|
|
1972
|
-
Expected: `forgehive@0.7.0` published successfully.
|
|
1973
|
-
|
|
1974
|
-
- [ ] **Step 7: Update memory**
|
|
1975
|
-
|
|
1976
|
-
Update `/home/stefan/.claude/projects/-home-stefan-projekte-framework-os/memory/project.md`:
|
|
1977
|
-
- Version: v0.7.0
|
|
1978
|
-
- New commands: ci, map, onboard, changelog, metrics, sync, run
|
|
1979
|
-
- New slash commands: /fh-deploy, /fh-test-this, /fh-docs
|
|
1980
|
-
- Updated: /fh-sprint (MCP), party defaults.yaml (model routing)
|