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.
@@ -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)