forgehive 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2751,8 +2751,8 @@ var init_harness = __esm({
2751
2751
 
2752
2752
  // src/cli.ts
2753
2753
  init_js_yaml();
2754
- import fs27 from "node:fs";
2755
- import path28 from "node:path";
2754
+ import fs30 from "node:fs";
2755
+ import path31 from "node:path";
2756
2756
 
2757
2757
  // src/scanner.ts
2758
2758
  import fs from "node:fs";
@@ -6196,24 +6196,282 @@ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
6196
6196
  };
6197
6197
  }
6198
6198
 
6199
+ // src/stories.ts
6200
+ init_js_yaml();
6201
+ import fs27 from "node:fs";
6202
+ import path28 from "node:path";
6203
+ function nextStoryId(storiesDir) {
6204
+ if (!fs27.existsSync(storiesDir)) return "US-1";
6205
+ const existing = fs27.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseInt(f.replace("US-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
6206
+ const max = existing.length > 0 ? Math.max(...existing) : 0;
6207
+ return `US-${max + 1}`;
6208
+ }
6209
+ function storyToMarkdown(story) {
6210
+ const frontmatter = jsYaml.dump({
6211
+ id: story.id,
6212
+ title: story.title,
6213
+ asA: story.asA,
6214
+ iWant: story.iWant,
6215
+ soThat: story.soThat,
6216
+ points: story.points,
6217
+ epicId: story.epicId,
6218
+ status: story.status
6219
+ });
6220
+ const acLines = story.acceptanceCriteria.map((c) => `- [ ] ${c}`).join("\n");
6221
+ return `---
6222
+ ${frontmatter}---
6223
+
6224
+ # ${story.id}: ${story.title}
6225
+
6226
+ ## User Story
6227
+
6228
+ Als **${story.asA}** m\xF6chte ich **${story.iWant}**, damit **${story.soThat}**.
6229
+
6230
+ ## Acceptance Criteria
6231
+
6232
+ ${acLines || "- [ ] (noch nicht definiert)"}
6233
+ `;
6234
+ }
6235
+ function parseStoryFile(filePath) {
6236
+ try {
6237
+ const content = fs27.readFileSync(filePath, "utf8");
6238
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
6239
+ if (!match) return null;
6240
+ const data = jsYaml.load(match[1]);
6241
+ return {
6242
+ id: data.id ?? "",
6243
+ title: data.title ?? "",
6244
+ asA: data.asA ?? "",
6245
+ iWant: data.iWant ?? "",
6246
+ soThat: data.soThat ?? "",
6247
+ acceptanceCriteria: data.acceptanceCriteria ?? [],
6248
+ points: data.points ?? null,
6249
+ epicId: data.epicId ?? null,
6250
+ status: data.status ?? "backlog"
6251
+ };
6252
+ } catch {
6253
+ return null;
6254
+ }
6255
+ }
6256
+ function createStory(storiesDir, title, epicId) {
6257
+ fs27.mkdirSync(storiesDir, { recursive: true });
6258
+ const id = nextStoryId(storiesDir);
6259
+ const story = {
6260
+ id,
6261
+ title,
6262
+ asA: "",
6263
+ iWant: title,
6264
+ soThat: "",
6265
+ acceptanceCriteria: [],
6266
+ points: null,
6267
+ epicId: epicId ?? null,
6268
+ status: "backlog"
6269
+ };
6270
+ fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6271
+ return story;
6272
+ }
6273
+ function listStories(storiesDir) {
6274
+ if (!fs27.existsSync(storiesDir)) return [];
6275
+ return fs27.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseStoryFile(path28.join(storiesDir, f))).filter((s) => s !== null).sort((a, b) => {
6276
+ const na = parseInt(a.id.replace("US-", ""), 10);
6277
+ const nb = parseInt(b.id.replace("US-", ""), 10);
6278
+ return na - nb;
6279
+ });
6280
+ }
6281
+ function getStory(storiesDir, id) {
6282
+ const filePath = path28.join(storiesDir, `${id}.md`);
6283
+ if (!fs27.existsSync(filePath)) return null;
6284
+ return parseStoryFile(filePath);
6285
+ }
6286
+ function updateStoryPoints(storiesDir, id, points) {
6287
+ const story = getStory(storiesDir, id);
6288
+ if (!story) throw new Error(`Story ${id} nicht gefunden`);
6289
+ story.points = points;
6290
+ fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6291
+ }
6292
+ function updateStoryStatus(storiesDir, id, status) {
6293
+ const story = getStory(storiesDir, id);
6294
+ if (!story) throw new Error(`Story ${id} nicht gefunden`);
6295
+ story.status = status;
6296
+ fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6297
+ }
6298
+ function formatStoryCard(story) {
6299
+ const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
6300
+ const epic = story.epicId ? ` \xB7 ${story.epicId}` : "";
6301
+ const lines = [];
6302
+ lines.push(`## ${story.id}: ${story.title}${points}${epic}`);
6303
+ lines.push(`**Status:** ${story.status}`);
6304
+ if (story.asA || story.iWant)
6305
+ lines.push(`**Story:** Als ${story.asA || "Nutzer"} m\xF6chte ich ${story.iWant}${story.soThat ? `, damit ${story.soThat}` : ""}.`);
6306
+ if (story.acceptanceCriteria.length > 0) {
6307
+ lines.push("**Acceptance Criteria:**");
6308
+ for (const ac of story.acceptanceCriteria) lines.push(`- ${ac}`);
6309
+ }
6310
+ return lines.join("\n");
6311
+ }
6312
+
6313
+ // src/epics.ts
6314
+ init_js_yaml();
6315
+ import fs28 from "node:fs";
6316
+ import path29 from "node:path";
6317
+ function nextEpicId(epicsDir) {
6318
+ if (!fs28.existsSync(epicsDir)) return "EPC-1";
6319
+ const existing = fs28.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseInt(f.replace("EPC-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
6320
+ const max = existing.length > 0 ? Math.max(...existing) : 0;
6321
+ return `EPC-${max + 1}`;
6322
+ }
6323
+ function epicToMarkdown(epic) {
6324
+ const frontmatter = jsYaml.dump({
6325
+ id: epic.id,
6326
+ title: epic.title,
6327
+ goal: epic.goal,
6328
+ stories: epic.stories,
6329
+ status: epic.status
6330
+ });
6331
+ const storyLines = epic.stories.map((s) => `- ${s}`).join("\n");
6332
+ return `---
6333
+ ${frontmatter}---
6334
+
6335
+ # ${epic.id}: ${epic.title}
6336
+
6337
+ **Ziel:** ${epic.goal || "(noch nicht definiert)"}
6338
+
6339
+ ## Stories
6340
+
6341
+ ${storyLines || "(noch keine Stories)"}
6342
+ `;
6343
+ }
6344
+ function parseEpicFile(filePath) {
6345
+ try {
6346
+ const content = fs28.readFileSync(filePath, "utf8");
6347
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
6348
+ if (!match) return null;
6349
+ const data = jsYaml.load(match[1]);
6350
+ return {
6351
+ id: data.id ?? "",
6352
+ title: data.title ?? "",
6353
+ goal: data.goal ?? "",
6354
+ stories: data.stories ?? [],
6355
+ status: data.status ?? "active"
6356
+ };
6357
+ } catch {
6358
+ return null;
6359
+ }
6360
+ }
6361
+ function createEpic(epicsDir, title, goal) {
6362
+ fs28.mkdirSync(epicsDir, { recursive: true });
6363
+ const id = nextEpicId(epicsDir);
6364
+ const epic = { id, title, goal: goal ?? "", stories: [], status: "active" };
6365
+ fs28.writeFileSync(path29.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
6366
+ return epic;
6367
+ }
6368
+ function listEpics(epicsDir) {
6369
+ if (!fs28.existsSync(epicsDir)) return [];
6370
+ return fs28.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseEpicFile(path29.join(epicsDir, f))).filter((e) => e !== null).sort((a, b) => {
6371
+ const na = parseInt(a.id.replace("EPC-", ""), 10);
6372
+ const nb = parseInt(b.id.replace("EPC-", ""), 10);
6373
+ return na - nb;
6374
+ });
6375
+ }
6376
+ function getEpic(epicsDir, id) {
6377
+ const filePath = path29.join(epicsDir, `${id}.md`);
6378
+ if (!fs28.existsSync(filePath)) return null;
6379
+ return parseEpicFile(filePath);
6380
+ }
6381
+ function formatEpicCard(epic, stories) {
6382
+ const lines = [];
6383
+ lines.push(`## ${epic.id}: ${epic.title} [${epic.status}]`);
6384
+ if (epic.goal) lines.push(`**Ziel:** ${epic.goal}`);
6385
+ lines.push(`**Stories:** ${epic.stories.length}`);
6386
+ if (stories && stories.length > 0) {
6387
+ const total = stories.reduce((sum, s) => sum + (s.points ?? 0), 0);
6388
+ lines.push(`**Punkte:** ${total}`);
6389
+ lines.push("");
6390
+ for (const s of stories) {
6391
+ const pts = s.points !== null ? ` [${s.points}pt]` : " [?pt]";
6392
+ lines.push(` - ${s.id}${pts} \u2014 ${s.title} [${s.status}]`);
6393
+ }
6394
+ } else if (epic.stories.length > 0) {
6395
+ for (const id of epic.stories) lines.push(` - ${id}`);
6396
+ }
6397
+ return lines.join("\n");
6398
+ }
6399
+
6400
+ // src/velocity.ts
6401
+ import fs29 from "node:fs";
6402
+ import path30 from "node:path";
6403
+ var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
6404
+ function recordVelocity(velocityFile, sprint, committed, delivered) {
6405
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6406
+ const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
6407
+ const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
6408
+ `;
6409
+ if (!fs29.existsSync(velocityFile)) {
6410
+ fs29.mkdirSync(path30.dirname(velocityFile), { recursive: true });
6411
+ fs29.writeFileSync(velocityFile, HEADER + row, "utf8");
6412
+ } else {
6413
+ fs29.appendFileSync(velocityFile, row, "utf8");
6414
+ }
6415
+ }
6416
+ function getVelocityHistory(velocityFile) {
6417
+ if (!fs29.existsSync(velocityFile)) return [];
6418
+ const content = fs29.readFileSync(velocityFile, "utf8");
6419
+ const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
6420
+ return rows.map((row) => {
6421
+ const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
6422
+ const sprintMatch = cells[0]?.match(/Sprint (\d+)/);
6423
+ return {
6424
+ sprint: sprintMatch ? parseInt(sprintMatch[1], 10) : 0,
6425
+ date: cells[1] ?? "",
6426
+ committed: parseInt(cells[2] ?? "0", 10),
6427
+ delivered: parseInt(cells[3] ?? "0", 10)
6428
+ };
6429
+ }).filter((r) => r.sprint > 0);
6430
+ }
6431
+ function getRollingAverage(history, window = 3) {
6432
+ if (history.length === 0) return 0;
6433
+ const recent = history.slice(-window);
6434
+ const sum = recent.reduce((acc, s) => acc + s.delivered, 0);
6435
+ return Math.round(sum / recent.length);
6436
+ }
6437
+ function formatVelocityReport(history) {
6438
+ if (history.length === 0)
6439
+ return "Noch keine Velocity-Daten vorhanden.\n\nStarte mit: `fh velocity record <sprint> --committed N --delivered N`";
6440
+ const avg3 = getRollingAverage(history, 3);
6441
+ const lines = [];
6442
+ lines.push("# Sprint Velocity");
6443
+ lines.push("");
6444
+ lines.push(`**Rolling Average (letzte 3 Sprints):** ${avg3} Punkte`);
6445
+ lines.push("");
6446
+ lines.push("| Sprint | Datum | Committed | Delivered | Rate |");
6447
+ lines.push("|---|---|---|---|---|");
6448
+ for (const s of history) {
6449
+ const rate = s.committed > 0 ? Math.round(s.delivered / s.committed * 100) : 0;
6450
+ lines.push(`| Sprint ${s.sprint} | ${s.date} | ${s.committed} | ${s.delivered} | ${rate}% |`);
6451
+ }
6452
+ lines.push("");
6453
+ lines.push(`**Empfehlung f\xFCr n\xE4chsten Sprint:** ~${avg3} Punkte einplanen`);
6454
+ return lines.join("\n");
6455
+ }
6456
+
6199
6457
  // src/cli.ts
6200
6458
  var [, , command, subcommand, ...rest] = process.argv;
6201
6459
  var projectRoot = process.cwd();
6202
- var forgehiveDir = path28.join(projectRoot, ".forgehive");
6460
+ var forgehiveDir = path31.join(projectRoot, ".forgehive");
6203
6461
  if (command === "--version" || command === "-v") {
6204
- console.log("0.7.0");
6462
+ console.log("0.7.1");
6205
6463
  process.exit(0);
6206
6464
  }
6207
6465
  function loadClaudeMdBlock() {
6208
- const templatePath = path28.join(
6209
- path28.dirname(new URL(import.meta.url).pathname),
6466
+ const templatePath = path31.join(
6467
+ path31.dirname(new URL(import.meta.url).pathname),
6210
6468
  "..",
6211
6469
  "forgehive",
6212
6470
  "templates",
6213
6471
  "claude-md.block.md"
6214
6472
  );
6215
- if (!fs27.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6216
- return fs27.readFileSync(templatePath, "utf8");
6473
+ if (!fs30.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6474
+ return fs30.readFileSync(templatePath, "utf8");
6217
6475
  }
6218
6476
  if (command === "init") {
6219
6477
  console.log("\u{1F50D} Analysiere Projekt...\n");
@@ -6227,9 +6485,9 @@ if (command === "init") {
6227
6485
  const block = loadClaudeMdBlock();
6228
6486
  writeForgehiveDir(projectRoot, scanResult, capMap, block);
6229
6487
  const hash = computeHash(projectRoot);
6230
- fs27.writeFileSync(path28.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6231
- const runtimeDir = path28.join(
6232
- path28.dirname(new URL(import.meta.url).pathname),
6488
+ fs30.writeFileSync(path31.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6489
+ const runtimeDir = path31.join(
6490
+ path31.dirname(new URL(import.meta.url).pathname),
6233
6491
  "..",
6234
6492
  "forgehive"
6235
6493
  );
@@ -6261,7 +6519,7 @@ if (command === "init") {
6261
6519
  process.exit(1);
6262
6520
  }
6263
6521
  } else if (command === "memory") {
6264
- if (!fs27.existsSync(forgehiveDir)) {
6522
+ if (!fs30.existsSync(forgehiveDir)) {
6265
6523
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6266
6524
  process.exit(1);
6267
6525
  }
@@ -6270,7 +6528,7 @@ if (command === "init") {
6270
6528
  } else if (subcommand === "clean") {
6271
6529
  cleanMemory(forgehiveDir);
6272
6530
  } else if (subcommand === "export") {
6273
- const outputPath = rest[0] ?? path28.join(projectRoot, "forgehive-memory-export.md");
6531
+ const outputPath = rest[0] ?? path31.join(projectRoot, "forgehive-memory-export.md");
6274
6532
  try {
6275
6533
  exportMemory(forgehiveDir, outputPath);
6276
6534
  } catch (err) {
@@ -6318,7 +6576,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6318
6576
  } else if (subcommand === "snapshot") {
6319
6577
  const snapAction = rest[0];
6320
6578
  if (snapAction === "export") {
6321
- const outPath = rest[1] ?? path28.join(
6579
+ const outPath = rest[1] ?? path31.join(
6322
6580
  projectRoot,
6323
6581
  `forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
6324
6582
  );
@@ -6358,11 +6616,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6358
6616
  process.exit(1);
6359
6617
  }
6360
6618
  } else if (command === "scan" && subcommand === "--update") {
6361
- if (!fs27.existsSync(forgehiveDir)) {
6619
+ if (!fs30.existsSync(forgehiveDir)) {
6362
6620
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6363
6621
  process.exit(1);
6364
6622
  }
6365
- const savedHash = fs27.existsSync(path28.join(forgehiveDir, ".scan-hash")) ? fs27.readFileSync(path28.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
6623
+ const savedHash = fs30.existsSync(path31.join(forgehiveDir, ".scan-hash")) ? fs30.readFileSync(path31.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
6366
6624
  const currentHash = computeHash(projectRoot);
6367
6625
  if (savedHash === currentHash) {
6368
6626
  console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
@@ -6370,7 +6628,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6370
6628
  }
6371
6629
  console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
6372
6630
  const oldDoc = jsYaml.load(
6373
- fs27.readFileSync(path28.join(forgehiveDir, "capabilities.yaml"), "utf8")
6631
+ fs30.readFileSync(path31.join(forgehiveDir, "capabilities.yaml"), "utf8")
6374
6632
  );
6375
6633
  const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
6376
6634
  const scanResult = scan(projectRoot);
@@ -6390,16 +6648,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6390
6648
  console.log();
6391
6649
  const block = loadClaudeMdBlock();
6392
6650
  writeForgehiveDir(projectRoot, scanResult, newMap, block);
6393
- fs27.writeFileSync(path28.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6651
+ fs30.writeFileSync(path31.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6394
6652
  console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
6395
6653
  console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
6396
6654
  }
6397
6655
  } else if (command === "scan" && subcommand === "--check") {
6398
- if (!fs27.existsSync(path28.join(forgehiveDir, ".scan-hash"))) {
6656
+ if (!fs30.existsSync(path31.join(forgehiveDir, ".scan-hash"))) {
6399
6657
  console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
6400
6658
  process.exit(1);
6401
6659
  }
6402
- const saved = fs27.readFileSync(path28.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6660
+ const saved = fs30.readFileSync(path31.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6403
6661
  const current = computeHash(projectRoot);
6404
6662
  if (saved !== current) {
6405
6663
  console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
@@ -6408,7 +6666,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6408
6666
  }
6409
6667
  console.log("\u2713 capabilities.yaml ist aktuell");
6410
6668
  } else if (command === "skills") {
6411
- if (!fs27.existsSync(forgehiveDir)) {
6669
+ if (!fs30.existsSync(forgehiveDir)) {
6412
6670
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6413
6671
  process.exit(1);
6414
6672
  }
@@ -6444,7 +6702,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6444
6702
  process.exit(1);
6445
6703
  }
6446
6704
  } else if (command === "party") {
6447
- if (!fs27.existsSync(forgehiveDir)) {
6705
+ if (!fs30.existsSync(forgehiveDir)) {
6448
6706
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6449
6707
  process.exit(1);
6450
6708
  }
@@ -6565,7 +6823,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6565
6823
  }
6566
6824
  }
6567
6825
  } else if (command === "wire") {
6568
- if (!fs27.existsSync(forgehiveDir)) {
6826
+ if (!fs30.existsSync(forgehiveDir)) {
6569
6827
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6570
6828
  process.exit(1);
6571
6829
  }
@@ -6604,7 +6862,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6604
6862
  const limitIdx = allArgs.indexOf("--limit");
6605
6863
  const alertIdx = allArgs.indexOf("--alert");
6606
6864
  if (limitIdx !== -1) {
6607
- if (!fs27.existsSync(forgehiveDir)) {
6865
+ if (!fs30.existsSync(forgehiveDir)) {
6608
6866
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6609
6867
  process.exit(1);
6610
6868
  }
@@ -6630,14 +6888,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6630
6888
  }
6631
6889
  const sessions = parseCostSessions(projectRoot);
6632
6890
  console.log(formatCostReport(sessions, range));
6633
- if (fs27.existsSync(forgehiveDir)) {
6891
+ if (fs30.existsSync(forgehiveDir)) {
6634
6892
  const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
6635
6893
  const status = checkSpendStatus(forgehiveDir, total);
6636
6894
  if (status.message) console.log("\n" + status.message);
6637
6895
  }
6638
6896
  }
6639
6897
  } else if (command === "watch") {
6640
- if (!fs27.existsSync(forgehiveDir)) {
6898
+ if (!fs30.existsSync(forgehiveDir)) {
6641
6899
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6642
6900
  process.exit(1);
6643
6901
  }
@@ -6653,7 +6911,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6653
6911
  process.exit(0);
6654
6912
  });
6655
6913
  } else if (command === "mcp") {
6656
- if (!fs27.existsSync(forgehiveDir)) {
6914
+ if (!fs30.existsSync(forgehiveDir)) {
6657
6915
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6658
6916
  process.exit(1);
6659
6917
  }
@@ -6764,7 +7022,7 @@ Setze diese Umgebungsvariablen:`);
6764
7022
  process.exit(1);
6765
7023
  }
6766
7024
  } else if (command === "security") {
6767
- if (!fs27.existsSync(forgehiveDir)) {
7025
+ if (!fs30.existsSync(forgehiveDir)) {
6768
7026
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6769
7027
  process.exit(1);
6770
7028
  }
@@ -6850,8 +7108,8 @@ Setze diese Umgebungsvariablen:`);
6850
7108
  `);
6851
7109
  const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
6852
7110
  const text = formatSecurityReport(report);
6853
- const reportPath = path28.join(forgehiveDir, "security-report.md");
6854
- fs27.writeFileSync(reportPath, text, "utf8");
7111
+ const reportPath = path31.join(forgehiveDir, "security-report.md");
7112
+ fs30.writeFileSync(reportPath, text, "utf8");
6855
7113
  console.log(text);
6856
7114
  console.log(`
6857
7115
  \u2713 Report gespeichert: ${reportPath}`);
@@ -6863,8 +7121,8 @@ Setze diese Umgebungsvariablen:`);
6863
7121
  } else if (subcommand === "permissions") {
6864
7122
  const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
6865
7123
  writePermissions2(forgehiveDir);
6866
- const permPath = path28.join(forgehiveDir, "harness", "permissions.yaml");
6867
- console.log(fs27.readFileSync(permPath, "utf8"));
7124
+ const permPath = path31.join(forgehiveDir, "harness", "permissions.yaml");
7125
+ console.log(fs30.readFileSync(permPath, "utf8"));
6868
7126
  } else {
6869
7127
  console.error(`Unbekannter security-Subcommand: ${subcommand}`);
6870
7128
  console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
@@ -6876,35 +7134,35 @@ Setze diese Umgebungsvariablen:`);
6876
7134
  const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
6877
7135
  const initFlag = allCiArgs.includes("--init");
6878
7136
  if (initFlag) {
6879
- const ghDir = path28.join(projectRoot, ".github", "workflows");
6880
- fs27.mkdirSync(ghDir, { recursive: true });
6881
- const outPath = path28.join(ghDir, "forgehive.yml");
6882
- fs27.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
7137
+ const ghDir = path31.join(projectRoot, ".github", "workflows");
7138
+ fs30.mkdirSync(ghDir, { recursive: true });
7139
+ const outPath = path31.join(ghDir, "forgehive.yml");
7140
+ fs30.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
6883
7141
  console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
6884
7142
  } else {
6885
7143
  const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
6886
7144
  const output = formatCiReport(report, format);
6887
7145
  console.log(output);
6888
7146
  if (format === "json") {
6889
- fs27.mkdirSync(forgehiveDir, { recursive: true });
6890
- fs27.writeFileSync(path28.join(forgehiveDir, "ci-report.json"), output, "utf8");
7147
+ fs30.mkdirSync(forgehiveDir, { recursive: true });
7148
+ fs30.writeFileSync(path31.join(forgehiveDir, "ci-report.json"), output, "utf8");
6891
7149
  }
6892
7150
  if (report.status === "fail") process.exit(1);
6893
7151
  }
6894
7152
  } else if (command === "map") {
6895
7153
  const map2 = generateMap(projectRoot);
6896
7154
  const md = formatMap(map2);
6897
- const mapPath = path28.join(forgehiveDir, "map.md");
6898
- fs27.mkdirSync(forgehiveDir, { recursive: true });
6899
- fs27.writeFileSync(mapPath, md, "utf8");
7155
+ const mapPath = path31.join(forgehiveDir, "map.md");
7156
+ fs30.mkdirSync(forgehiveDir, { recursive: true });
7157
+ fs30.writeFileSync(mapPath, md, "utf8");
6900
7158
  console.log(md);
6901
7159
  console.log(`
6902
7160
  \u2714 Codebase-Map gespeichert: ${mapPath}`);
6903
7161
  } else if (command === "onboard") {
6904
7162
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
6905
- const outputPath = outputArg ?? path28.join(projectRoot, "ONBOARDING.md");
7163
+ const outputPath = outputArg ?? path31.join(projectRoot, "ONBOARDING.md");
6906
7164
  const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
6907
- fs27.writeFileSync(outputPath, doc, "utf8");
7165
+ fs30.writeFileSync(outputPath, doc, "utf8");
6908
7166
  console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
6909
7167
  } else if (command === "changelog") {
6910
7168
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
@@ -6914,28 +7172,28 @@ Setze diese Umgebungsvariablen:`);
6914
7172
  const commits = parseGitLog(rawLog);
6915
7173
  let version = "unreleased";
6916
7174
  try {
6917
- const pkgPath = path28.join(projectRoot, "package.json");
6918
- if (fs27.existsSync(pkgPath)) {
6919
- const pkg = JSON.parse(fs27.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
7175
+ const pkgPath = path31.join(projectRoot, "package.json");
7176
+ if (fs30.existsSync(pkgPath)) {
7177
+ const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
6920
7178
  version = pkg.version ?? "unreleased";
6921
7179
  }
6922
7180
  } catch {
6923
7181
  }
6924
7182
  const md = formatChangelog(commits, version);
6925
- const outputPath = outputArg ?? path28.join(projectRoot, "CHANGELOG.md");
7183
+ const outputPath = outputArg ?? path31.join(projectRoot, "CHANGELOG.md");
6926
7184
  let existing = "";
6927
- if (fs27.existsSync(outputPath)) existing = fs27.readFileSync(outputPath, "utf8");
6928
- fs27.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
7185
+ if (fs30.existsSync(outputPath)) existing = fs30.readFileSync(outputPath, "utf8");
7186
+ fs30.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
6929
7187
  console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
6930
7188
  console.log(` ${commits.length} Commits verarbeitet`);
6931
7189
  } else if (command === "metrics") {
6932
7190
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
6933
7191
  const rawLog = getMetricsGitLog(projectRoot, sinceArg);
6934
7192
  const stats = parseCommitStats(rawLog);
6935
- const md = formatMetrics(stats, path28.basename(projectRoot));
6936
- const metricsPath = path28.join(forgehiveDir, "metrics.md");
6937
- fs27.mkdirSync(forgehiveDir, { recursive: true });
6938
- fs27.writeFileSync(metricsPath, md, "utf8");
7193
+ const md = formatMetrics(stats, path31.basename(projectRoot));
7194
+ const metricsPath = path31.join(forgehiveDir, "metrics.md");
7195
+ fs30.mkdirSync(forgehiveDir, { recursive: true });
7196
+ fs30.writeFileSync(metricsPath, md, "utf8");
6939
7197
  console.log(md);
6940
7198
  console.log(`
6941
7199
  \u2714 Metrics gespeichert: ${metricsPath}`);
@@ -6969,6 +7227,111 @@ Setze diese Umgebungsvariablen:`);
6969
7227
  const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
6970
7228
  console.log(`\u2714 ${result.message}`);
6971
7229
  console.log(` fh run status \u2014 aktive Sessions anzeigen`);
7230
+ } else if (command === "story") {
7231
+ const storiesDir = path31.join(forgehiveDir, "memory", "stories");
7232
+ if (subcommand === "create") {
7233
+ const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7234
+ const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
7235
+ const pointsArg = rest.includes("--points") ? parseInt(rest[rest.indexOf("--points") + 1], 10) : void 0;
7236
+ if (!title) {
7237
+ console.error("Usage: fh story create <titel> [--epic EPC-N] [--points N]");
7238
+ process.exit(1);
7239
+ }
7240
+ const story = createStory(storiesDir, title, epicArg);
7241
+ if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
7242
+ console.log(`\u2714 ${story.id} erstellt: ${path31.join(storiesDir, story.id + ".md")}`);
7243
+ console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
7244
+ } else if (subcommand === "list") {
7245
+ const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
7246
+ let stories = listStories(storiesDir);
7247
+ if (epicFilter) stories = stories.filter((s) => s.epicId === epicFilter);
7248
+ if (stories.length === 0) {
7249
+ console.log("Keine Stories gefunden.");
7250
+ } else {
7251
+ for (const s of stories) {
7252
+ const pts = s.points !== null ? ` [${s.points}pt]` : " [?pt]";
7253
+ const epic = s.epicId ? ` (${s.epicId})` : "";
7254
+ console.log(` ${s.id}${pts}${epic} \u2014 ${s.title} [${s.status}]`);
7255
+ }
7256
+ }
7257
+ } else if (subcommand === "done") {
7258
+ const id = rest[0];
7259
+ const pointsArg = rest.includes("--points") ? parseInt(rest[rest.indexOf("--points") + 1], 10) : void 0;
7260
+ if (!id) {
7261
+ console.error("Usage: fh story done <US-N> [--points N]");
7262
+ process.exit(1);
7263
+ }
7264
+ if (pointsArg) updateStoryPoints(storiesDir, id, pointsArg);
7265
+ updateStoryStatus(storiesDir, id, "done");
7266
+ console.log(`\u2714 ${id} als done markiert`);
7267
+ } else if (subcommand === "show") {
7268
+ const id = rest[0];
7269
+ if (!id) {
7270
+ console.error("Usage: fh story show <US-N>");
7271
+ process.exit(1);
7272
+ }
7273
+ const story = getStory(storiesDir, id);
7274
+ if (!story) {
7275
+ console.error(`Story ${id} nicht gefunden`);
7276
+ process.exit(1);
7277
+ }
7278
+ console.log(formatStoryCard(story));
7279
+ } else {
7280
+ console.error("Verf\xFCgbar: fh story create | list | show | done");
7281
+ }
7282
+ } else if (command === "epic") {
7283
+ const epicsDir = path31.join(forgehiveDir, "memory", "epics");
7284
+ const storiesDir = path31.join(forgehiveDir, "memory", "stories");
7285
+ if (subcommand === "create") {
7286
+ const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7287
+ const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
7288
+ if (!title) {
7289
+ console.error("Usage: fh epic create <titel> [--goal <ziel>]");
7290
+ process.exit(1);
7291
+ }
7292
+ const epic = createEpic(epicsDir, title, goalArg);
7293
+ console.log(`\u2714 ${epic.id} erstellt: ${path31.join(epicsDir, epic.id + ".md")}`);
7294
+ } else if (subcommand === "list") {
7295
+ const epics = listEpics(epicsDir);
7296
+ if (epics.length === 0) {
7297
+ console.log("Keine Epics gefunden.");
7298
+ } else {
7299
+ for (const e of epics)
7300
+ console.log(` ${e.id} \u2014 ${e.title} [${e.status}] (${e.stories.length} Stories)`);
7301
+ }
7302
+ } else if (subcommand === "show") {
7303
+ const id = rest[0];
7304
+ if (!id) {
7305
+ console.error("Usage: fh epic show <EPC-N>");
7306
+ process.exit(1);
7307
+ }
7308
+ const epic = getEpic(epicsDir, id);
7309
+ if (!epic) {
7310
+ console.error(`Epic ${id} nicht gefunden`);
7311
+ process.exit(1);
7312
+ }
7313
+ const stories = listStories(storiesDir).filter((s) => s.epicId === id);
7314
+ console.log(formatEpicCard(epic, stories));
7315
+ } else {
7316
+ console.error("Verf\xFCgbar: fh epic create | list | show");
7317
+ }
7318
+ } else if (command === "velocity") {
7319
+ const velocityFile = path31.join(forgehiveDir, "memory", "velocity.md");
7320
+ if (subcommand === "record") {
7321
+ const sprintNum = parseInt(rest[0] ?? "0", 10);
7322
+ const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
7323
+ const delivered = rest.includes("--delivered") ? parseInt(rest[rest.indexOf("--delivered") + 1], 10) : NaN;
7324
+ if (!sprintNum || isNaN(committed) || isNaN(delivered)) {
7325
+ console.error("Usage: fh velocity record <sprint-num> --committed N --delivered N");
7326
+ process.exit(1);
7327
+ }
7328
+ recordVelocity(velocityFile, sprintNum, committed, delivered);
7329
+ const avg = getRollingAverage(getVelocityHistory(velocityFile));
7330
+ console.log(`\u2714 Sprint ${sprintNum} gespeichert. Rolling Average: ${avg} Punkte`);
7331
+ } else {
7332
+ const history = getVelocityHistory(velocityFile);
7333
+ console.log(formatVelocityReport(history));
7334
+ }
6972
7335
  } else {
6973
7336
  const cmd = [command, subcommand].filter(Boolean).join(" ") || "(kein)";
6974
7337
  console.error(`Unbekannter Befehl: ${cmd}`);
@@ -7,11 +7,15 @@ You are running a Sprint Planning session using the ForgeHive workflow.
7
7
  1. Read `.forgehive/capabilities.yaml` — understand the tech stack and constraints
8
8
  2. Read `.forgehive/memory/MEMORY.md` and all linked memory files — load project context
9
9
  3. Check if `.forgehive/memory/sprint.md` exists — if so, show the last sprint summary first
10
- 4. Run `fh scan --check` to verify the codebase snapshot is current
10
+ 4. Check if `.forgehive/memory/velocity.md` exists if so, show rolling average as capacity hint
11
+ 5. Run `fh scan --check` to verify the codebase snapshot is current
11
12
 
12
13
  ### Step 2: Collect backlog items
13
14
 
14
- Ask the user: **"Welche Items kommen in den Sprint? Liste sie auf — ein Item pro Zeile. Oder soll ich Linear/Jira laden?"**
15
+ Ask the user: **"Welche Items kommen in den Sprint? Liste sie auf — oder soll ich Backlog-Stories laden?"**
16
+
17
+ **If stories exist** in `.forgehive/memory/stories/` with status `backlog`:
18
+ Run `fh story list` to show available stories. Ask: **"Welche dieser Stories kommen in den Sprint?"**
15
19
 
16
20
  **If Linear MCP is available** (check if `.mcp.json` contains `linear`):
17
21
  Use the Linear MCP tool to fetch open issues:
@@ -25,7 +29,7 @@ Show the fetched issues and ask: **"Welche davon kommen in den Sprint?"**
25
29
  gh issue list --state open --label "sprint-candidate" --limit 20
26
30
  ```
27
31
 
28
- **If no MCP connected:**
32
+ **If no stories/MCP:**
29
33
  Accept free-text input — one item per line.
30
34
 
31
35
  ### Step 3: Clarify and refine
@@ -37,44 +41,59 @@ For each item, ask one clarifying question if needed:
37
41
 
38
42
  Do NOT ask more than one question per item. If the item is clear, skip this step.
39
43
 
40
- ### Step 4: Estimate effort
44
+ ### Step 4: Estimate with Fibonacci Points
41
45
 
42
- Estimate each item using T-shirt sizes:
46
+ Estimate each item using Fibonacci story points:
43
47
 
44
- | Size | Meaning | Typical scope |
48
+ | Points | Meaning | Typical scope |
45
49
  |---|---|---|
46
- | XS | < 2h | Config change, copy fix, small bug |
47
- | S | half day | Simple feature, isolated fix |
48
- | M | 1–2 days | Feature with tests, moderate complexity |
49
- | L | 3–5 days | Complex feature, multiple files, integration work |
50
- | XL | > 5 days | Should be broken down further |
50
+ | 1 | Trivial | Config change, copy fix, 1-line fix |
51
+ | 2 | Small | Simple isolated change |
52
+ | 3 | Medium-small | Small feature, isolated fix |
53
+ | 5 | Medium | Feature with tests, moderate complexity |
54
+ | 8 | Large | Complex feature, multiple files |
55
+ | 13 | Extra-large | Should be broken down |
56
+
57
+ **T-Shirt aliases:** XS=1, S=2, M=5, L=8, XL=13
51
58
 
52
- Flag any XL items: **"Dieses Item ist zu groß für einen Sprint — soll ich es aufteilen?"**
59
+ Flag any 13-point items: **"Dieses Item ist zu groß für einen Sprint — soll ich es aufteilen?"**
60
+
61
+ If items were loaded from `.forgehive/memory/stories/`, update their points:
62
+ ```bash
63
+ fh story <US-N> --points <N>
64
+ ```
53
65
 
54
66
  ### Step 5: Prioritize
55
67
 
56
68
  Sort items into three buckets:
57
69
 
58
- **Must (Sprint-Ziel)** — delivers the sprint goal, blocks other work, or is overdue
59
- **Should (Best Effort)** — important but not blocking
60
- **Could (Nice to Have)** — do if capacity allows
70
+ **Must (Sprint-Ziel)** — delivers the sprint goal, blocks other work, or is overdue
71
+ **Should (Best Effort)** — important but not blocking
72
+ **Could (Nice to Have)** — do if capacity allows
61
73
 
62
74
  Suggest a sprint goal in one sentence based on the Must items.
63
75
 
64
76
  ### Step 6: Check capacity
65
77
 
66
- Ask: **"Wie viele Entwickler-Tage habt ihr im Sprint?"** (default: 10 days for a 2-week sprint with one developer)
78
+ Show velocity hint if available:
79
+ ```bash
80
+ fh velocity show
81
+ ```
82
+
83
+ Ask: **"Wie viele Punkte habt ihr im Sprint?"**
67
84
 
68
- Calculate if the Must + Should items fit. If they don't, move the lowest-priority Should items to Could.
85
+ Default: rolling average from velocity history, or 20 points for a 2-week sprint with one developer.
86
+
87
+ Calculate if Must + Should items fit. If they don't, move the lowest-priority Should items to Could.
69
88
 
70
89
  Show a capacity summary:
71
90
  ```
72
- Sprint-Kapazität: 10 Tage
73
- Must: [sum] Tage
74
- Should: [sum] Tage
75
- Could: [sum] Tage
76
- ─────────────────
77
- Geplant: [must+should] / 10 Tage
91
+ Sprint-Kapazität: 20 Punkte
92
+ Must: [sum] Punkte
93
+ Should: [sum] Punkte
94
+ Could: [sum] Punkte
95
+ ─────────────────────
96
+ Geplant: [must+should] / 20 Punkte
78
97
  ```
79
98
 
80
99
  ### Step 7: Output the sprint plan
@@ -86,16 +105,16 @@ Write the sprint plan to `.forgehive/memory/sprint.md` in this format:
86
105
 
87
106
  **Ziel:** [one sentence sprint goal]
88
107
 
89
- **Kapazität:** [X] Entwickler-Tage
108
+ **Kapazität:** [X] Punkte
90
109
 
91
110
  ## Must
92
- - [ ] [Item] ([size]) — [one-line description]
111
+ - [ ] [ID] [Item] ([points]pt) — [one-line description]
93
112
 
94
- ## Should
95
- - [ ] [Item] ([size]) — [one-line description]
113
+ ## Should
114
+ - [ ] [ID] [Item] ([points]pt) — [one-line description]
96
115
 
97
116
  ## Could
98
- - [ ] [Item] ([size]) — [one-line description]
117
+ - [ ] [ID] [Item] ([points]pt) — [one-line description]
99
118
 
100
119
  ## Offen / Blocked
101
120
  - [any blocked items with reason]
@@ -104,10 +123,25 @@ Write the sprint plan to `.forgehive/memory/sprint.md` in this format:
104
123
  *Erstellt mit fh sprint — [timestamp]*
105
124
  ```
106
125
 
107
- Confirm with the user: **"Sprint Plan gespeichert in `.forgehive/memory/sprint.md`. Soll ich für jedes Must-Item direkt einen Branch anlegen?"**
126
+ Confirm with the user: **"Sprint Plan gespeichert. Soll ich für jedes Must-Item direkt einen Branch anlegen?"**
108
127
 
109
- If yes: create branches for each Must item following the `git-conventions` skill (`feat/<slug>`, `fix/<slug>`, `chore/<slug>`).
128
+ If yes: create branches following `feat/<slug>`, `fix/<slug>`, `chore/<slug>`.
110
129
 
111
130
  ### Step 8: Update project memory
112
131
 
113
- Append the sprint goal to `.forgehive/memory/project.md` under a `## Aktueller Sprint` section so Claude knows the current focus in every future session.
132
+ Append the sprint goal to `.forgehive/memory/project.md` under a `## Aktueller Sprint` section.
133
+
134
+ ### Step 9: Record velocity (at sprint end)
135
+
136
+ When the user runs `/fh-sprint` and mentions "Sprint ist fertig" or "Sprint abgeschlossen":
137
+
138
+ 1. Ask: **"Wie viele Punkte habt ihr tatsächlich geliefert?"**
139
+ 2. Read the committed points from `sprint.md`
140
+ 3. Record velocity:
141
+ ```bash
142
+ fh velocity record <N> --committed <committed> --delivered <delivered>
143
+ ```
144
+ 4. Show updated velocity report:
145
+ ```bash
146
+ fh velocity show
147
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgehive",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Context-aware AI development environment — one binary, your stack.",
5
5
  "type": "module",
6
6
  "bin": {