forgehive 0.8.0 → 0.9.0

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
@@ -2753,7 +2753,7 @@ var init_harness = __esm({
2753
2753
  init_js_yaml();
2754
2754
  import fs35 from "node:fs";
2755
2755
  import path36 from "node:path";
2756
- import { spawnSync as spawnSync12 } from "node:child_process";
2756
+ import { spawnSync as spawnSync13 } from "node:child_process";
2757
2757
  import { createInterface } from "node:readline";
2758
2758
 
2759
2759
  // src/scanner.ts
@@ -4860,6 +4860,109 @@ function countMdFiles(dir) {
4860
4860
  if (!fs15.existsSync(dir)) return 0;
4861
4861
  return fs15.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
4862
4862
  }
4863
+ function readYamlFrontmatterFiles(dir) {
4864
+ if (!fs15.existsSync(dir)) return [];
4865
+ const results = [];
4866
+ for (const filename of fs15.readdirSync(dir)) {
4867
+ if (!filename.endsWith(".md")) continue;
4868
+ try {
4869
+ const content = fs15.readFileSync(path15.join(dir, filename), "utf8");
4870
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
4871
+ if (!match) continue;
4872
+ const parsed = jsYaml.load(match[1]);
4873
+ if (parsed && typeof parsed === "object") results.push(parsed);
4874
+ } catch {
4875
+ }
4876
+ }
4877
+ return results;
4878
+ }
4879
+ function formatSprintSection(forgehiveDir2) {
4880
+ const storiesDir = path15.join(forgehiveDir2, "memory", "stories");
4881
+ if (!fs15.existsSync(storiesDir)) return "";
4882
+ const stories = readYamlFrontmatterFiles(storiesDir);
4883
+ const open = stories.filter((s) => s.status !== "done");
4884
+ const inSprint = stories.filter((s) => s.status === "in-sprint");
4885
+ const next = inSprint[0] ?? open[0] ?? null;
4886
+ const epicsDir = path15.join(forgehiveDir2, "memory", "epics");
4887
+ const activeEpics = readYamlFrontmatterFiles(epicsDir).filter((e) => e.status === "active" || e.status === "in-progress");
4888
+ const lines = ["SPRINT"];
4889
+ lines.push(` \u25CF ${open.length} offen \u25CF ${inSprint.length} in-sprint`);
4890
+ if (activeEpics.length > 0) {
4891
+ lines.push(` Epic: ${activeEpics[0].id} ${activeEpics[0].title}`);
4892
+ }
4893
+ if (next) {
4894
+ const pts = next.points ? ` [${next.points} pts]` : "";
4895
+ lines.push(` \u2192 n\xE4chstes: ${next.id} "${next.title}"${pts}`);
4896
+ }
4897
+ return lines.join("\n");
4898
+ }
4899
+ function formatCodeHealthSection(projectRoot2, forgehiveDir2) {
4900
+ const lines = ["CODE HEALTH"];
4901
+ const gitResult = spawnSync4("git", ["status", "--porcelain"], {
4902
+ cwd: projectRoot2,
4903
+ encoding: "utf8"
4904
+ });
4905
+ if (gitResult.status === 0) {
4906
+ const changed = gitResult.stdout.trim().split("\n").filter(Boolean).length;
4907
+ lines.push(
4908
+ changed === 0 ? " \u2713 Arbeitsbaum sauber" : ` \u26A0 ${changed} ge\xE4nderte Datei(en) \u2014 nicht committed`
4909
+ );
4910
+ }
4911
+ const secPath = path15.join(forgehiveDir2, "security-last-scan.txt");
4912
+ if (fs15.existsSync(secPath)) {
4913
+ try {
4914
+ const ts = fs15.readFileSync(secPath, "utf8").trim();
4915
+ const days = Math.floor((Date.now() - new Date(ts).getTime()) / 864e5);
4916
+ lines.push(
4917
+ days > 7 ? ` \u26A0 Security-Scan: ${days} Tage alt \u2014 fh security scan` : ` \u2713 Security-Scan: ${days} Tage alt`
4918
+ );
4919
+ } catch {
4920
+ }
4921
+ }
4922
+ const prCachePath = path15.join(forgehiveDir2, "github-pr-cache.json");
4923
+ if (fs15.existsSync(prCachePath)) {
4924
+ try {
4925
+ const cache = JSON.parse(fs15.readFileSync(prCachePath, "utf8"));
4926
+ if (cache.open.length > 0) {
4927
+ lines.push(` \u25CF ${cache.open.length} offene PR(s): #${cache.open.join(", #")}`);
4928
+ }
4929
+ } catch {
4930
+ }
4931
+ }
4932
+ return lines.join("\n");
4933
+ }
4934
+ function formatWatchSection(forgehiveDir2) {
4935
+ const logPath = path15.join(forgehiveDir2, "watch-events.jsonl");
4936
+ if (!fs15.existsSync(logPath)) return "";
4937
+ const events = fs15.readFileSync(logPath, "utf8").trim().split("\n").filter(Boolean).map((line) => {
4938
+ try {
4939
+ return JSON.parse(line);
4940
+ } catch {
4941
+ return null;
4942
+ }
4943
+ }).filter((e) => e !== null).slice(-5).reverse();
4944
+ if (events.length === 0) return "";
4945
+ const lines = ["WATCH (letzte Ereignisse)"];
4946
+ for (const e of events) {
4947
+ const time = new Date(e.ts).toLocaleTimeString("de", { hour: "2-digit", minute: "2-digit" });
4948
+ if (e.type === "stack-update") {
4949
+ lines.push(` [${time}] \u2713 Stack-Update (${e.file ?? "?"}) \u2014 capabilities.yaml aktualisiert`);
4950
+ } else {
4951
+ const verdict = e.accepted === true ? "\u2192 gestartet" : e.accepted === false ? "\u2192 abgelehnt" : "\u2192 vorgeschlagen";
4952
+ lines.push(` [${time}] \u26A0 ${e.file ?? e.type} ${verdict}: ${e.agent ?? "?"}`);
4953
+ }
4954
+ }
4955
+ return lines.join("\n");
4956
+ }
4957
+ function formatDashboard(projectRoot2, forgehiveDir2) {
4958
+ const sections = [
4959
+ formatSprintSection(forgehiveDir2),
4960
+ formatCodeHealthSection(projectRoot2, forgehiveDir2),
4961
+ formatWatchSection(forgehiveDir2)
4962
+ ].filter(Boolean);
4963
+ if (sections.length === 0) return "";
4964
+ return "\n" + sections.join("\n\n");
4965
+ }
4863
4966
  function checkDrift(projectRoot2, forgehiveDir2) {
4864
4967
  const scanPath = path15.join(forgehiveDir2, "scan-result.yaml");
4865
4968
  if (!fs15.existsSync(scanPath)) {
@@ -4974,6 +5077,7 @@ function projectStatus(projectRoot2, forgehiveDir2) {
4974
5077
  // src/watch.ts
4975
5078
  import fs16 from "node:fs";
4976
5079
  import path16 from "node:path";
5080
+ import { spawnSync as spawnSync5 } from "node:child_process";
4977
5081
  function checkProjectHash(projectRoot2, forgehiveDir2, claudeMdBlock) {
4978
5082
  const hashPath = path16.join(forgehiveDir2, ".scan-hash");
4979
5083
  const savedHash = fs16.existsSync(hashPath) ? fs16.readFileSync(hashPath, "utf8").trim() : null;
@@ -5031,6 +5135,91 @@ function defaultOnUpdate(changed) {
5031
5135
  console.log("[forgehive] \u2713 Codebase changed \u2014 capabilities.yaml aktualisiert");
5032
5136
  }
5033
5137
  }
5138
+ function appendWatchEvent(forgehiveDir2, event) {
5139
+ const logPath = path16.join(forgehiveDir2, "watch-events.jsonl");
5140
+ fs16.appendFileSync(logPath, JSON.stringify(event) + "\n", "utf8");
5141
+ try {
5142
+ const stat = fs16.statSync(logPath);
5143
+ if (stat.size > 25e3) {
5144
+ const lines = fs16.readFileSync(logPath, "utf8").trim().split("\n").filter(Boolean);
5145
+ if (lines.length >= 500) {
5146
+ fs16.writeFileSync(logPath, lines.slice(-500).join("\n") + "\n", "utf8");
5147
+ }
5148
+ }
5149
+ } catch {
5150
+ }
5151
+ }
5152
+ function detectErrorType(output) {
5153
+ const lower = output.toLowerCase();
5154
+ if (lower.includes("failing") || lower.includes("assertionerror") || lower.includes("not ok")) {
5155
+ return { agent: "sam", reason: "Test-Fehler erkannt" };
5156
+ }
5157
+ if (lower.includes("error ts") || lower.includes("type error")) {
5158
+ return { agent: "kai", reason: "TypeScript-Fehler erkannt" };
5159
+ }
5160
+ if (lower.includes("secret") || lower.includes("vulnerability") || lower.includes("cve")) {
5161
+ return { agent: "vera", reason: "Sicherheitsproblem erkannt" };
5162
+ }
5163
+ if (lower.includes("error") || lower.includes("failed to compile")) {
5164
+ return { agent: "kai", reason: "Build-Fehler erkannt" };
5165
+ }
5166
+ return null;
5167
+ }
5168
+ function smartWatch(projectRoot2, forgehiveDir2, testCmd, onSuggest) {
5169
+ const srcDir = path16.join(projectRoot2, "src");
5170
+ const testDir = path16.join(projectRoot2, "test");
5171
+ const watchDirs = [srcDir, testDir].filter((d) => fs16.existsSync(d));
5172
+ let debounce = null;
5173
+ const watchers = [];
5174
+ function runAndAnalyze(changedFile) {
5175
+ const result = spawnSync5(testCmd, {
5176
+ cwd: projectRoot2,
5177
+ encoding: "utf8",
5178
+ shell: true
5179
+ });
5180
+ if (result.status === 0) return;
5181
+ const output = (result.stdout ?? "") + (result.stderr ?? "");
5182
+ const detection = detectErrorType(output);
5183
+ if (!detection) return;
5184
+ const relFile = path16.relative(projectRoot2, changedFile);
5185
+ console.log(`
5186
+ \u26A0 ${relFile} \u2014 ${detection.reason}`);
5187
+ console.log(` \u2192 ${detection.agent} ist der richtige Agent.`);
5188
+ void onSuggest(detection.agent, detection.reason, relFile).then((accepted) => {
5189
+ appendWatchEvent(forgehiveDir2, {
5190
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5191
+ type: "test-failure",
5192
+ file: relFile,
5193
+ agent: detection.agent,
5194
+ accepted
5195
+ });
5196
+ });
5197
+ }
5198
+ function schedule(changedFile) {
5199
+ if (debounce) clearTimeout(debounce);
5200
+ debounce = setTimeout(() => runAndAnalyze(changedFile), 1500);
5201
+ }
5202
+ for (const dir of watchDirs) {
5203
+ try {
5204
+ const watcher = fs16.watch(dir, { recursive: true }, (_evt, filename) => {
5205
+ if (filename && (filename.endsWith(".ts") || filename.endsWith(".js"))) {
5206
+ schedule(path16.join(dir, filename));
5207
+ }
5208
+ });
5209
+ watchers.push(watcher);
5210
+ } catch {
5211
+ }
5212
+ }
5213
+ return () => {
5214
+ if (debounce) clearTimeout(debounce);
5215
+ for (const w of watchers) {
5216
+ try {
5217
+ w.close();
5218
+ } catch {
5219
+ }
5220
+ }
5221
+ };
5222
+ }
5034
5223
 
5035
5224
  // src/cost.ts
5036
5225
  import fs17 from "node:fs";
@@ -5200,7 +5389,7 @@ function checkMcpTrust(packageName) {
5200
5389
  // src/mcp-registry.ts
5201
5390
  import fs19 from "node:fs";
5202
5391
  import path19 from "node:path";
5203
- import { spawnSync as spawnSync5 } from "node:child_process";
5392
+ import { spawnSync as spawnSync6 } from "node:child_process";
5204
5393
  function parseRegistryResponse(raw) {
5205
5394
  if (!raw || typeof raw !== "object") return [];
5206
5395
  const data = raw;
@@ -5214,7 +5403,7 @@ function parseRegistryResponse(raw) {
5214
5403
  }
5215
5404
  function searchRegistry(query) {
5216
5405
  const url = `https://registry.smithery.ai/servers?q=${encodeURIComponent(query)}&pageSize=10`;
5217
- const result = spawnSync5("curl", [
5406
+ const result = spawnSync6("curl", [
5218
5407
  "-s",
5219
5408
  "--max-time",
5220
5409
  "10",
@@ -5277,7 +5466,7 @@ function addMcpFromRegistry(projectRoot2, forgehiveDir2, packageName, envKeys) {
5277
5466
  // src/security-scan.ts
5278
5467
  import fs20 from "node:fs";
5279
5468
  import path20 from "node:path";
5280
- import { spawnSync as spawnSync6 } from "node:child_process";
5469
+ import { spawnSync as spawnSync7 } from "node:child_process";
5281
5470
  var SECRET_PATTERNS = [
5282
5471
  { name: "Anthropic Key", pattern: /sk-ant-[a-zA-Z0-9-]{20,}/g },
5283
5472
  { name: "OpenAI Key", pattern: /sk-(?!ant-)[a-zA-Z0-9]{20,}/g },
@@ -5412,7 +5601,7 @@ function scanSast(projectRoot2) {
5412
5601
  function scanDeps(projectRoot2) {
5413
5602
  const pkgJsonPath = path20.join(projectRoot2, "package.json");
5414
5603
  if (!fs20.existsSync(pkgJsonPath)) return [];
5415
- const result = spawnSync6("npm", ["audit", "--json"], {
5604
+ const result = spawnSync7("npm", ["audit", "--json"], {
5416
5605
  cwd: projectRoot2,
5417
5606
  encoding: "utf8",
5418
5607
  timeout: 3e4
@@ -5996,9 +6185,9 @@ function buildSemanticMap(projectRoot2) {
5996
6185
  init_js_yaml();
5997
6186
  import fs25 from "node:fs";
5998
6187
  import path26 from "node:path";
5999
- import { spawnSync as spawnSync7 } from "node:child_process";
6188
+ import { spawnSync as spawnSync8 } from "node:child_process";
6000
6189
  function getRecentCommits(projectRoot2, n = 20) {
6001
- const result = spawnSync7("git", ["log", `--oneline`, `-${n}`], {
6190
+ const result = spawnSync8("git", ["log", `--oneline`, `-${n}`], {
6002
6191
  cwd: projectRoot2,
6003
6192
  encoding: "utf8"
6004
6193
  });
@@ -6103,7 +6292,7 @@ function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
6103
6292
  }
6104
6293
 
6105
6294
  // src/changelog.ts
6106
- import { spawnSync as spawnSync8 } from "node:child_process";
6295
+ import { spawnSync as spawnSync9 } from "node:child_process";
6107
6296
  var TYPE_LABELS = {
6108
6297
  feat: "Added",
6109
6298
  fix: "Fixed",
@@ -6167,12 +6356,12 @@ function formatChangelog(commits, version2) {
6167
6356
  function getGitLogSince(projectRoot2, since) {
6168
6357
  const args = ["log", "--oneline", "--no-merges"];
6169
6358
  if (since) args.push(`${since}..HEAD`);
6170
- const result = spawnSync8("git", args, { cwd: projectRoot2, encoding: "utf8" });
6359
+ const result = spawnSync9("git", args, { cwd: projectRoot2, encoding: "utf8" });
6171
6360
  if (result.status !== 0) return "";
6172
6361
  return result.stdout.trim();
6173
6362
  }
6174
6363
  function getLatestTag(projectRoot2) {
6175
- const result = spawnSync8("git", ["describe", "--tags", "--abbrev=0"], {
6364
+ const result = spawnSync9("git", ["describe", "--tags", "--abbrev=0"], {
6176
6365
  cwd: projectRoot2,
6177
6366
  encoding: "utf8"
6178
6367
  });
@@ -6181,7 +6370,7 @@ function getLatestTag(projectRoot2) {
6181
6370
  }
6182
6371
 
6183
6372
  // src/metrics.ts
6184
- import { spawnSync as spawnSync9 } from "node:child_process";
6373
+ import { spawnSync as spawnSync10 } from "node:child_process";
6185
6374
  function parseCommitStats(rawLog) {
6186
6375
  const lines = rawLog.split("\n").map((l) => l.trim()).filter(Boolean);
6187
6376
  const byAuthor = {};
@@ -6238,7 +6427,7 @@ function formatMetrics(stats, projectName) {
6238
6427
  function getMetricsGitLog(projectRoot2, since) {
6239
6428
  const args = ["log", "--no-merges", "--format=%as %an %s"];
6240
6429
  if (since) args.push(`--since=${since}`);
6241
- const result = spawnSync9("git", args, { cwd: projectRoot2, encoding: "utf8" });
6430
+ const result = spawnSync10("git", args, { cwd: projectRoot2, encoding: "utf8" });
6242
6431
  if (result.status !== 0) return "";
6243
6432
  return result.stdout.trim();
6244
6433
  }
@@ -6246,18 +6435,18 @@ function getMetricsGitLog(projectRoot2, since) {
6246
6435
  // src/sync.ts
6247
6436
  import fs26 from "node:fs";
6248
6437
  import path27 from "node:path";
6249
- import { spawnSync as spawnSync10 } from "node:child_process";
6438
+ import { spawnSync as spawnSync11 } from "node:child_process";
6250
6439
  function getSyncStatus(forgehiveDir2) {
6251
6440
  const memDir = path27.join(forgehiveDir2, "memory");
6252
6441
  const files = fs26.existsSync(memDir) ? fs26.readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
6253
6442
  const projectRoot2 = path27.dirname(forgehiveDir2);
6254
- const configResult = spawnSync10(
6443
+ const configResult = spawnSync11(
6255
6444
  "git",
6256
6445
  ["config", "--get", "forgehive.sync-remote"],
6257
6446
  { cwd: projectRoot2, encoding: "utf8" }
6258
6447
  );
6259
6448
  const remote = configResult.status === 0 ? configResult.stdout.trim() : null;
6260
- const branchResult = spawnSync10(
6449
+ const branchResult = spawnSync11(
6261
6450
  "git",
6262
6451
  ["config", "--get", "forgehive.sync-branch"],
6263
6452
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6275,7 +6464,7 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6275
6464
  if (files.length === 0) {
6276
6465
  return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
6277
6466
  }
6278
- const addResult = spawnSync10(
6467
+ const addResult = spawnSync11(
6279
6468
  "git",
6280
6469
  ["add", path27.join(".forgehive", "memory")],
6281
6470
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6283,7 +6472,7 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6283
6472
  if (addResult.status !== 0) {
6284
6473
  return { success: false, message: `git add failed: ${addResult.stderr}`, filesCommitted: 0 };
6285
6474
  }
6286
- const commitResult = spawnSync10(
6475
+ const commitResult = spawnSync11(
6287
6476
  "git",
6288
6477
  ["commit", "-m", `chore: sync forgehive memory [${(/* @__PURE__ */ new Date()).toISOString()}]`],
6289
6478
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6292,7 +6481,7 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6292
6481
  if (!commitOk) {
6293
6482
  return { success: false, message: `git commit failed: ${commitResult.stderr}`, filesCommitted: 0 };
6294
6483
  }
6295
- const pushResult = spawnSync10(
6484
+ const pushResult = spawnSync11(
6296
6485
  "git",
6297
6486
  ["push", remote, `HEAD:${branch}`],
6298
6487
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6300,15 +6489,15 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6300
6489
  if (pushResult.status !== 0) {
6301
6490
  return { success: false, message: `git push failed: ${pushResult.stderr}`, filesCommitted: 0 };
6302
6491
  }
6303
- spawnSync10("git", ["config", "forgehive.sync-remote", remote], { cwd: projectRoot2 });
6304
- spawnSync10("git", ["config", "forgehive.sync-branch", branch], { cwd: projectRoot2 });
6492
+ spawnSync11("git", ["config", "forgehive.sync-remote", remote], { cwd: projectRoot2 });
6493
+ spawnSync11("git", ["config", "forgehive.sync-branch", branch], { cwd: projectRoot2 });
6305
6494
  return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
6306
6495
  }
6307
6496
  function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
6308
6497
  const projectRoot2 = path27.dirname(forgehiveDir2);
6309
6498
  const memDir = path27.join(forgehiveDir2, "memory");
6310
6499
  fs26.mkdirSync(memDir, { recursive: true });
6311
- const fetchResult = spawnSync10(
6500
+ const fetchResult = spawnSync11(
6312
6501
  "git",
6313
6502
  ["fetch", remote, branch],
6314
6503
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6316,7 +6505,7 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6316
6505
  if (fetchResult.status !== 0) {
6317
6506
  return { success: false, message: `git fetch failed: ${fetchResult.stderr}`, filesImported: [] };
6318
6507
  }
6319
- const listResult = spawnSync10(
6508
+ const listResult = spawnSync11(
6320
6509
  "git",
6321
6510
  ["ls-tree", "--name-only", `${remote}/${branch}`, ".forgehive/memory/"],
6322
6511
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6330,7 +6519,7 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6330
6519
  const filename = path27.basename(remotePath);
6331
6520
  const localPath = path27.join(memDir, filename);
6332
6521
  if (!fs26.existsSync(localPath)) {
6333
- const contentResult = spawnSync10(
6522
+ const contentResult = spawnSync11(
6334
6523
  "git",
6335
6524
  ["show", `${remote}/${branch}:${remotePath}`],
6336
6525
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6848,7 +7037,7 @@ function formatVelocityReport(history) {
6848
7037
  init_js_yaml();
6849
7038
  import fs32 from "node:fs";
6850
7039
  import path33 from "node:path";
6851
- import { spawnSync as spawnSync11 } from "node:child_process";
7040
+ import { spawnSync as spawnSync12 } from "node:child_process";
6852
7041
  var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
6853
7042
  var IGNORE_DIRS4 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
6854
7043
  var EXPORT_PATTERNS2 = [
@@ -6897,7 +7086,7 @@ function readMemoryFiles2(forgehiveDir2) {
6897
7086
  return result;
6898
7087
  }
6899
7088
  function getRecentCommits2(projectRoot2, n = 10) {
6900
- const result = spawnSync11("git", ["log", "--oneline", `-${n}`], { cwd: projectRoot2, encoding: "utf8" });
7089
+ const result = spawnSync12("git", ["log", "--oneline", `-${n}`], { cwd: projectRoot2, encoding: "utf8" });
6901
7090
  if (result.status !== 0) return [];
6902
7091
  return result.stdout.trim().split("\n").filter(Boolean);
6903
7092
  }
@@ -7339,6 +7528,21 @@ function formatPRContext(pr, files, repoFullName) {
7339
7528
  fileLines || "(keine Dateien)"
7340
7529
  ].join("\n");
7341
7530
  }
7531
+ async function fetchOpenPRs(owner, repo, token) {
7532
+ const prs = [];
7533
+ let page = 1;
7534
+ while (true) {
7535
+ const batch = await githubGet(
7536
+ `/repos/${owner}/${repo}/pulls?state=open&per_page=100&page=${page}`,
7537
+ token
7538
+ );
7539
+ if (batch.length === 0) break;
7540
+ prs.push(...batch);
7541
+ if (batch.length < 100) break;
7542
+ page++;
7543
+ }
7544
+ return prs.map((pr) => pr.number);
7545
+ }
7342
7546
 
7343
7547
  // src/cli.ts
7344
7548
  import { createRequire } from "node:module";
@@ -7363,6 +7567,7 @@ SETUP
7363
7567
  fh confirm Activate capabilities (draft \u2192 confirmed)
7364
7568
  fh rollback Remove forgehive from the project
7365
7569
  fh status Show current project state
7570
+ fh watch [--smart] Observe project; --smart runs tests on change and suggests agents
7366
7571
  fh scan --update Re-scan project after changes
7367
7572
  fh scan --check Check if scan is still current
7368
7573
 
@@ -7450,7 +7655,7 @@ async function promptConfirm(question) {
7450
7655
  rl.question(question, (answer) => {
7451
7656
  rl.close();
7452
7657
  const a = answer.trim().toLowerCase();
7453
- resolve(a === "y" || a === "yes" || a === "");
7658
+ resolve(a === "y" || a === "yes" || a === "j" || a === "ja" || a === "");
7454
7659
  });
7455
7660
  });
7456
7661
  }
@@ -7464,14 +7669,14 @@ function buildCapabilitySummary(ids) {
7464
7669
  }
7465
7670
  if (command === "init") {
7466
7671
  (async () => {
7467
- const gitCheck = spawnSync12("git", ["--version"], { stdio: "ignore" });
7672
+ const gitCheck = spawnSync13("git", ["--version"], { stdio: "ignore" });
7468
7673
  if (gitCheck.error || gitCheck.status !== 0) {
7469
7674
  console.error("Fehler: git nicht gefunden.");
7470
7675
  console.error(" Installation: https://git-scm.com");
7471
7676
  process.exit(1);
7472
7677
  }
7473
7678
  const forgehiveDirExists = fs35.existsSync(forgehiveDir);
7474
- if (forgehiveDirExists && !rest.includes("--force")) {
7679
+ if (forgehiveDirExists && subcommand !== "--force" && !rest.includes("--force")) {
7475
7680
  console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
7476
7681
  console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
7477
7682
  console.log(` Nutze 'fh scan --update' um nur den Scan zu aktualisieren.`);
@@ -7870,6 +8075,8 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7870
8075
  }
7871
8076
  } else if (command === "status") {
7872
8077
  console.log(projectStatus(projectRoot, forgehiveDir));
8078
+ const dashboard = formatDashboard(projectRoot, forgehiveDir);
8079
+ if (dashboard) console.log(dashboard);
7873
8080
  } else if (command === "cost") {
7874
8081
  const allArgs = [subcommand, ...rest].filter((a) => Boolean(a));
7875
8082
  const limitIdx = allArgs.indexOf("--limit");
@@ -7912,17 +8119,44 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7912
8119
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7913
8120
  process.exit(1);
7914
8121
  }
7915
- const block = loadClaudeMdBlock();
7916
- console.log("\u{1F441} ForgeHive watch gestartet \u2014 beobachte Projekt-Dateien (Ctrl+C zum Beenden)\n");
7917
- const stop = watchProject(projectRoot, forgehiveDir, block);
7918
- process.on("SIGINT", () => {
7919
- stop();
7920
- process.exit(0);
7921
- });
7922
- process.on("SIGTERM", () => {
7923
- stop();
7924
- process.exit(0);
7925
- });
8122
+ const isSmartMode = subcommand === "--smart" || rest.includes("--smart");
8123
+ if (isSmartMode) {
8124
+ let testCmd = "npm test";
8125
+ const capsPath = path36.join(forgehiveDir, "capabilities.yaml");
8126
+ if (fs35.existsSync(capsPath)) {
8127
+ try {
8128
+ const caps = jsYaml.load(fs35.readFileSync(capsPath, "utf8"));
8129
+ if (caps?.test_command) testCmd = caps.test_command;
8130
+ } catch {
8131
+ }
8132
+ }
8133
+ console.log(`\u{1F441} ForgeHive Smart Watch \u2014 Testbefehl: ${testCmd}`);
8134
+ console.log(" \xDCberwache src/ und test/ auf \xC4nderungen (Ctrl+C zum Beenden)\n");
8135
+ const stop = smartWatch(projectRoot, forgehiveDir, testCmd, async (agent, _reason, file) => {
8136
+ console.log(` Soll ich ${agent} starten f\xFCr ${file}?`);
8137
+ return promptConfirm(" [j/n] ");
8138
+ });
8139
+ process.on("SIGINT", () => {
8140
+ stop();
8141
+ process.exit(0);
8142
+ });
8143
+ process.on("SIGTERM", () => {
8144
+ stop();
8145
+ process.exit(0);
8146
+ });
8147
+ } else {
8148
+ const block = loadClaudeMdBlock();
8149
+ console.log("\u{1F441} ForgeHive watch gestartet \u2014 beobachte Projekt-Dateien (Ctrl+C zum Beenden)\n");
8150
+ const stop = watchProject(projectRoot, forgehiveDir, block);
8151
+ process.on("SIGINT", () => {
8152
+ stop();
8153
+ process.exit(0);
8154
+ });
8155
+ process.on("SIGTERM", () => {
8156
+ stop();
8157
+ process.exit(0);
8158
+ });
8159
+ }
7926
8160
  } else if (command === "mcp") {
7927
8161
  if (!fs35.existsSync(forgehiveDir)) {
7928
8162
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
@@ -8088,6 +8322,11 @@ Setze diese Umgebungsvariablen:`);
8088
8322
  high: high.length
8089
8323
  }
8090
8324
  });
8325
+ fs35.writeFileSync(
8326
+ path36.join(forgehiveDir, "security-last-scan.txt"),
8327
+ (/* @__PURE__ */ new Date()).toISOString(),
8328
+ "utf8"
8329
+ );
8091
8330
  if (secrets.length > 0 || critical.length > 0 || high.length > 0) {
8092
8331
  process.exit(1);
8093
8332
  }
@@ -8654,6 +8893,16 @@ Task: "${taskDescription}"
8654
8893
  }
8655
8894
  console.log(`
8656
8895
  ${created} ${created === 1 ? "Story" : "Stories"} erstellt, ${skipped} \xFCbersprungen.`);
8896
+ try {
8897
+ const [syncOwner, syncRepo] = config.repo.split("/");
8898
+ const openPrNumbers = await fetchOpenPRs(syncOwner, syncRepo, creds.GITHUB_TOKEN);
8899
+ fs35.writeFileSync(
8900
+ path36.join(forgehiveDir, "github-pr-cache.json"),
8901
+ JSON.stringify({ open: openPrNumbers, ts: (/* @__PURE__ */ new Date()).toISOString() }),
8902
+ "utf8"
8903
+ );
8904
+ } catch {
8905
+ }
8657
8906
  } else if (subcommand === "pr") {
8658
8907
  const prNumberRaw = rest[0];
8659
8908
  if (!prNumberRaw || isNaN(parseInt(prNumberRaw, 10))) {
@@ -1,6 +1,6 @@
1
1
  # forgehive — User Guide
2
2
 
3
- > Version 0.7.7 · [npm](https://www.npmjs.com/package/forgehive) · `npm install -g forgehive`
3
+ > Version 0.8.0 · [npm](https://www.npmjs.com/package/forgehive) · `npm install -g forgehive`
4
4
 
5
5
  forgehive (`fh`) gives Claude Code persistent memory about your project. Without it, Claude forgets everything between sessions — your stack, your conventions, your decisions. With it, Claude opens every session already knowing your codebase.
6
6
 
@@ -17,7 +17,7 @@ Requires **Node.js ≥ 18** and **Claude Code** (`claude` CLI). Installs two ali
17
17
  Verify:
18
18
 
19
19
  ```bash
20
- fh --version # prints 0.7.7
20
+ fh --version # prints 0.8.0
21
21
  fh --help # full command reference
22
22
  ```
23
23
 
@@ -63,6 +63,7 @@ my-project/
63
63
  adrs/ ← Architecture Decision Records
64
64
  stories/ ← story cards (US-N.md)
65
65
  epics/ ← epic cards (EPC-N.md)
66
+ prds/ ← Product Requirements Documents (PRD-N.md)
66
67
  velocity.md ← sprint velocity history
67
68
  skills/expert/ ← 16 preinstalled expert skills
68
69
  AGENTS.md
@@ -196,6 +197,7 @@ Lightweight agile tracking inside `.forgehive/memory/`:
196
197
  ```bash
197
198
  # Epics
198
199
  fh epic create "User Authentication" --goal "Users can log in securely"
200
+ fh epic create "User Authentication" --prd PRD-1 # link to a PRD
199
201
  fh epic list
200
202
  fh epic show EPC-1
201
203
 
@@ -216,6 +218,83 @@ In a Claude Code session, run `/fh-sprint` to let Claude read the backlog and ve
216
218
 
217
219
  ---
218
220
 
221
+ ### GitHub Integration
222
+
223
+ Connect forgehive to GitHub to sync Issues as Story cards and pull PR context.
224
+
225
+ ```bash
226
+ fh github setup # interactive: GitHub token + owner/repo → stored in ~/.forgehive/credentials.json
227
+ fh github sync # fetch open Issues → create Story cards (idempotent, skips already-synced)
228
+ fh github pr 42 # fetch PR #42 metadata + changed files → .forgehive/github-pr-42.md
229
+ ```
230
+
231
+ **Setup is a one-time step.** `fh github setup` prompts for a personal access token (stored at `~/.forgehive/credentials.json`) and the repository in `owner/repo` format (stored at `.forgehive/github.yaml`).
232
+
233
+ **Issue sync** creates one Story card per open GitHub Issue. It's idempotent — run it multiple times without duplicating cards. Already-synced issues (detected by `GitHub: #N` in the story file) are skipped.
234
+
235
+ **PR context** writes a `.forgehive/github-pr-N.md` file with the PR title, body, author, and full list of changed files — useful to share with a Review Party:
236
+
237
+ ```bash
238
+ fh github pr 42
239
+ fh party run review # Kai + Sam + Eli each read the PR context
240
+ ```
241
+
242
+ ---
243
+
244
+ ### Intelligent Routing — fh ask
245
+
246
+ Not sure which agent to use? Describe the task:
247
+
248
+ ```bash
249
+ fh ask "fix the login timeout bug"
250
+ fh ask "write integration tests for the payment service"
251
+ fh ask "design the new onboarding flow"
252
+ fh ask "review the auth module for security issues"
253
+ ```
254
+
255
+ `fh ask` analyzes the task description using keyword matching and returns:
256
+ - **Which agent or party** to use
257
+ - **Confidence level** (high / medium / low)
258
+ - **Why** — which keywords triggered the recommendation
259
+ - **Exact command** to run — either `cd <worktree> && claude` for a single agent or `fh party run <set>` for a party
260
+
261
+ `fh run "<freetext>"` does the same when the argument isn't a URL:
262
+
263
+ ```bash
264
+ fh run "refactor the database layer" # routes to best agent automatically
265
+ ```
266
+
267
+ ---
268
+
269
+ ### Product Workflow
270
+
271
+ `fh product` manages the full PRD → Epic → Story pipeline.
272
+
273
+ ```bash
274
+ # PRDs
275
+ fh product prd "User Authentication Redesign" # create PRD-N (auto-incremented)
276
+ fh product list # list all PRDs with status and date
277
+ fh product show PRD-1 # print full PRD content
278
+
279
+ # Pipeline view
280
+ fh product status # tree: PRDs → Epics → Stories with status
281
+ fh product roadmap # write ROADMAP.md to project root
282
+ ```
283
+
284
+ **Typical flow:**
285
+
286
+ ```bash
287
+ fh product prd "Onboarding Redesign"
288
+ fh epic create "Welcome Flow" --prd PRD-1 --goal "Users complete setup in < 5 min"
289
+ fh story create "As a new user I want to see a setup checklist" --epic EPC-1 --points 3
290
+ fh product status # see the full pipeline as a tree
291
+ fh product roadmap # generate ROADMAP.md
292
+ ```
293
+
294
+ `fh epic create --prd PRD-N` links the epic to a PRD at creation time. Epics without a PRD link appear as orphans in `fh product status`.
295
+
296
+ ---
297
+
219
298
  ## Party Mode — Parallel Agent Sessions
220
299
 
221
300
  Party Mode runs specialized agent sets in parallel, each in an isolated git worktree. Use this for reviews, design sessions, or full-release audits.
@@ -340,3 +419,9 @@ Run `fh party cleanup` to remove all finished worktrees.
340
419
 
341
420
  **`fh cost` shows no data**
342
421
  Cost tracking reads `~/.claude/` — requires Claude Code to be installed and to have run at least one session.
422
+
423
+ **`fh github sync` creates duplicate stories**
424
+ This shouldn't happen — `fh github sync` checks for `GitHub: #N` in existing story files and skips them. If you see duplicates, check that the story file contains the `GitHub: #N` reference line.
425
+
426
+ **`fh github setup` says "Nicht konfiguriert"**
427
+ Run `fh github setup` to configure the token and repository.
@@ -9,6 +9,7 @@ This project uses **forgehive** for structured AI-assisted development.
9
9
  - If `status: confirmed` → load silently and apply throughout the session
10
10
  2. Read `.forgehive/memory/MEMORY.md` — follow the index links to load project context
11
11
  3. Run `fh scan --check` to verify the stack snapshot is current
12
+ 4. Run `fh status` for the daily overview (sprint, code health, watch events)
12
13
 
13
14
  ### During the Session
14
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgehive",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Context-aware AI development environment — one binary, your stack.",
5
5
  "type": "module",
6
6
  "bin": {