forgehive 0.7.9 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +140 -11
  2. package/dist/cli.js +1126 -194
  3. package/package.json +1 -1
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 fs31 from "node:fs";
2755
- import path32 from "node:path";
2754
+ import fs35 from "node:fs";
2755
+ import path36 from "node:path";
2756
2756
  import { spawnSync as spawnSync12 } from "node:child_process";
2757
2757
  import { createInterface } from "node:readline";
2758
2758
 
@@ -5736,7 +5736,44 @@ function generateMap(projectRoot2) {
5736
5736
  totalLines: files.reduce((sum, f) => sum + f.lines, 0)
5737
5737
  };
5738
5738
  }
5739
- function formatMap(map2) {
5739
+ function formatSemanticBlock(semantic, projectRoot2) {
5740
+ const lines = [];
5741
+ lines.push("## Semantic Graph");
5742
+ lines.push("");
5743
+ lines.push("### Hotspots (meistimportiert)");
5744
+ if (semantic.hotspots.length === 0) {
5745
+ lines.push("Keine Hotspot-Dateien gefunden (Schwellenwert: 3+ Imports).");
5746
+ } else {
5747
+ for (const h of semantic.hotspots) {
5748
+ const rel = h.file.startsWith(projectRoot2) ? h.file.slice(projectRoot2.length + 1) : h.file;
5749
+ lines.push(`- \`${rel}\` \u2014 importiert von ${h.importedBy} Dateien`);
5750
+ }
5751
+ }
5752
+ lines.push("");
5753
+ lines.push("### Zirkul\xE4re Abh\xE4ngigkeiten");
5754
+ if (semantic.cycles.length === 0) {
5755
+ lines.push("Keine gefunden.");
5756
+ } else {
5757
+ for (const cycle of semantic.cycles) {
5758
+ const relCycle = cycle.map(
5759
+ (f) => f.startsWith(projectRoot2) ? f.slice(projectRoot2.length + 1) : f
5760
+ );
5761
+ lines.push(`- ${relCycle.map((f) => `\`${f}\``).join(" \u2192 ")}`);
5762
+ }
5763
+ }
5764
+ lines.push("");
5765
+ const fileCount = Object.keys(semantic.imports).length;
5766
+ const relationCount = Object.values(semantic.imports).reduce(
5767
+ (sum, deps) => sum + deps.length,
5768
+ 0
5769
+ );
5770
+ lines.push("### Abh\xE4ngigkeits\xFCbersicht");
5771
+ lines.push(`- ${fileCount} Dateien analysiert`);
5772
+ lines.push(`- ${relationCount} Import-Beziehungen`);
5773
+ lines.push(`- ${semantic.hotspots.length} Hotspot-Dateien (importiert von 3+ anderen)`);
5774
+ return lines.join("\n");
5775
+ }
5776
+ function formatMap(map2, semantic) {
5740
5777
  const lines = [];
5741
5778
  lines.push("# Codebase Map");
5742
5779
  lines.push(`Generated: ${map2.generatedAt}`);
@@ -5766,13 +5803,199 @@ function formatMap(map2) {
5766
5803
  } else {
5767
5804
  lines.push("No internal imports detected.");
5768
5805
  }
5806
+ if (semantic) {
5807
+ lines.push("");
5808
+ lines.push(formatSemanticBlock(semantic, map2.projectRoot));
5809
+ }
5769
5810
  return lines.join("\n");
5770
5811
  }
5771
5812
 
5772
- // src/onboard.ts
5773
- init_js_yaml();
5813
+ // src/semantic-map.ts
5774
5814
  import fs24 from "node:fs";
5775
5815
  import path25 from "node:path";
5816
+ var IGNORE_DIRS3 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
5817
+ var MAP_EXTS2 = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
5818
+ var IMPORT_PATTERNS2 = {
5819
+ ts: [
5820
+ /^import\s+[\s\S]*?from\s+['"]([^'"]+)['"]/gm,
5821
+ /^import\s+['"]([^'"]+)['"]/gm,
5822
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm
5823
+ ],
5824
+ js: [
5825
+ /^import\s+[\s\S]*?from\s+['"]([^'"]+)['"]/gm,
5826
+ /^import\s+['"]([^'"]+)['"]/gm,
5827
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm
5828
+ ],
5829
+ py: [
5830
+ /^import\s+(\S+)/gm,
5831
+ /^from\s+(\S+)\s+import/gm
5832
+ ],
5833
+ go: [
5834
+ /^\s+"([^"]+)"/gm
5835
+ ]
5836
+ };
5837
+ var EXPORT_PATTERNS = {
5838
+ ts: [
5839
+ /export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|type|interface|enum)\s+(\w+)/gm,
5840
+ /export\s*\{\s*([^}]+)\s*\}/gm
5841
+ ],
5842
+ js: [
5843
+ /export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var)\s+(\w+)/gm,
5844
+ /export\s*\{\s*([^}]+)\s*\}/gm
5845
+ ],
5846
+ py: [
5847
+ /^def\s+(\w+)/gm,
5848
+ /^class\s+(\w+)/gm
5849
+ ],
5850
+ go: [
5851
+ /^func\s+([A-Z]\w*)/gm
5852
+ ]
5853
+ };
5854
+ function langOf(ext) {
5855
+ if (ext === ".ts" || ext === ".tsx") return "ts";
5856
+ if (ext === ".js" || ext === ".jsx") return "js";
5857
+ if (ext === ".py") return "py";
5858
+ if (ext === ".go") return "go";
5859
+ return "";
5860
+ }
5861
+ function walkFiles2(dir) {
5862
+ const results = [];
5863
+ function walk(current) {
5864
+ let entries;
5865
+ try {
5866
+ entries = fs24.readdirSync(current, { withFileTypes: true });
5867
+ } catch {
5868
+ return;
5869
+ }
5870
+ for (const entry of entries) {
5871
+ if (IGNORE_DIRS3.includes(entry.name)) continue;
5872
+ const full = path25.join(current, entry.name);
5873
+ if (entry.isDirectory()) walk(full);
5874
+ else if (entry.isFile() && MAP_EXTS2.includes(path25.extname(entry.name))) {
5875
+ results.push(full);
5876
+ }
5877
+ }
5878
+ }
5879
+ walk(dir);
5880
+ return results;
5881
+ }
5882
+ function _extractImports(content, lang) {
5883
+ const patterns = IMPORT_PATTERNS2[lang] ?? [];
5884
+ const found = [];
5885
+ for (const pattern of patterns) {
5886
+ const re = new RegExp(pattern.source, pattern.flags);
5887
+ let m;
5888
+ while ((m = re.exec(content)) !== null) {
5889
+ const imp = m[1].trim();
5890
+ if (lang === "py" && !imp.startsWith(".")) continue;
5891
+ if ((lang === "ts" || lang === "js") && !imp.startsWith(".")) continue;
5892
+ found.push(imp);
5893
+ }
5894
+ }
5895
+ return [...new Set(found)];
5896
+ }
5897
+ function _extractExports(content, lang) {
5898
+ const patterns = EXPORT_PATTERNS[lang] ?? [];
5899
+ const found = [];
5900
+ for (const pattern of patterns) {
5901
+ const re = new RegExp(pattern.source, pattern.flags);
5902
+ let m;
5903
+ while ((m = re.exec(content)) !== null) {
5904
+ const raw = m[1];
5905
+ if (raw.includes(",") || pattern.source.includes("{")) {
5906
+ for (const name of raw.split(",")) {
5907
+ const trimmed = name.trim().split(/\s+as\s+/)[0].trim();
5908
+ if (trimmed) found.push(trimmed);
5909
+ }
5910
+ } else {
5911
+ const name = raw.trim();
5912
+ if (name) found.push(name);
5913
+ }
5914
+ }
5915
+ }
5916
+ return [...new Set(found)];
5917
+ }
5918
+ function _resolveImport(fromFile, importPath, allFiles) {
5919
+ if (!importPath.startsWith(".")) return null;
5920
+ const fromDir = path25.dirname(fromFile);
5921
+ const base = path25.resolve(fromDir, importPath);
5922
+ if (allFiles.includes(base)) return base;
5923
+ for (const ext of [".ts", ".tsx", ".js", ".jsx", ".py", ".go"]) {
5924
+ const candidate = base + ext;
5925
+ if (allFiles.includes(candidate)) return candidate;
5926
+ }
5927
+ for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
5928
+ const candidate = path25.join(base, "index" + ext);
5929
+ if (allFiles.includes(candidate)) return candidate;
5930
+ }
5931
+ return null;
5932
+ }
5933
+ function _detectCycles(imports) {
5934
+ const cycles = [];
5935
+ const visited = /* @__PURE__ */ new Set();
5936
+ const stack = [];
5937
+ const onStack = /* @__PURE__ */ new Set();
5938
+ function dfs(node) {
5939
+ if (onStack.has(node)) {
5940
+ const cycleStart = stack.indexOf(node);
5941
+ cycles.push([...stack.slice(cycleStart), node]);
5942
+ return;
5943
+ }
5944
+ if (visited.has(node)) return;
5945
+ visited.add(node);
5946
+ onStack.add(node);
5947
+ stack.push(node);
5948
+ for (const neighbour of imports[node] ?? []) {
5949
+ dfs(neighbour);
5950
+ }
5951
+ stack.pop();
5952
+ onStack.delete(node);
5953
+ }
5954
+ for (const node of Object.keys(imports)) {
5955
+ dfs(node);
5956
+ }
5957
+ return cycles;
5958
+ }
5959
+ function _findHotspots(imports) {
5960
+ const THRESHOLD = 3;
5961
+ const counts = {};
5962
+ for (const deps of Object.values(imports)) {
5963
+ for (const dep of deps) {
5964
+ counts[dep] = (counts[dep] ?? 0) + 1;
5965
+ }
5966
+ }
5967
+ return Object.entries(counts).filter(([, count]) => count >= THRESHOLD).map(([file, importedBy]) => ({ file, importedBy })).sort((a, b) => b.importedBy - a.importedBy);
5968
+ }
5969
+ function buildSemanticMap(projectRoot2) {
5970
+ const allFiles = walkFiles2(projectRoot2);
5971
+ const imports = {};
5972
+ const exports = {};
5973
+ for (const filePath of allFiles) {
5974
+ let content = "";
5975
+ try {
5976
+ content = fs24.readFileSync(filePath, "utf8");
5977
+ } catch {
5978
+ }
5979
+ const ext = path25.extname(filePath);
5980
+ const lang = langOf(ext);
5981
+ const rawImps = _extractImports(content, lang);
5982
+ const resolvedImps = [];
5983
+ for (const imp of rawImps) {
5984
+ const resolved = _resolveImport(filePath, imp, allFiles);
5985
+ if (resolved) resolvedImps.push(resolved);
5986
+ }
5987
+ imports[filePath] = resolvedImps;
5988
+ exports[filePath] = lang ? _extractExports(content, lang) : [];
5989
+ }
5990
+ const cycles = _detectCycles(imports);
5991
+ const hotspots = _findHotspots(imports);
5992
+ return { imports, exports, hotspots, cycles };
5993
+ }
5994
+
5995
+ // src/onboard.ts
5996
+ init_js_yaml();
5997
+ import fs25 from "node:fs";
5998
+ import path26 from "node:path";
5776
5999
  import { spawnSync as spawnSync7 } from "node:child_process";
5777
6000
  function getRecentCommits(projectRoot2, n = 20) {
5778
6001
  const result = spawnSync7("git", ["log", `--oneline`, `-${n}`], {
@@ -5783,30 +6006,30 @@ function getRecentCommits(projectRoot2, n = 20) {
5783
6006
  return result.stdout.trim().split("\n").filter(Boolean);
5784
6007
  }
5785
6008
  function readCapabilities(forgehiveDir2) {
5786
- const capPath = path25.join(forgehiveDir2, "capabilities.yaml");
5787
- if (!fs24.existsSync(capPath)) return {};
6009
+ const capPath = path26.join(forgehiveDir2, "capabilities.yaml");
6010
+ if (!fs25.existsSync(capPath)) return {};
5788
6011
  try {
5789
- return jsYaml.load(fs24.readFileSync(capPath, "utf8")) ?? {};
6012
+ return jsYaml.load(fs25.readFileSync(capPath, "utf8")) ?? {};
5790
6013
  } catch {
5791
6014
  return {};
5792
6015
  }
5793
6016
  }
5794
6017
  function readMemoryFiles(forgehiveDir2) {
5795
- const memDir = path25.join(forgehiveDir2, "memory");
5796
- if (!fs24.existsSync(memDir)) return {};
6018
+ const memDir = path26.join(forgehiveDir2, "memory");
6019
+ if (!fs25.existsSync(memDir)) return {};
5797
6020
  const result = {};
5798
- for (const f of fs24.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
5799
- result[f] = fs24.readFileSync(path25.join(memDir, f), "utf8");
6021
+ for (const f of fs25.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
6022
+ result[f] = fs25.readFileSync(path26.join(memDir, f), "utf8");
5800
6023
  }
5801
6024
  return result;
5802
6025
  }
5803
6026
  function listAdrs2(forgehiveDir2) {
5804
- const adrsDir = path25.join(forgehiveDir2, "memory", "adrs");
5805
- if (!fs24.existsSync(adrsDir)) return [];
5806
- return fs24.readdirSync(adrsDir).filter((f) => f.endsWith(".md"));
6027
+ const adrsDir = path26.join(forgehiveDir2, "memory", "adrs");
6028
+ if (!fs25.existsSync(adrsDir)) return [];
6029
+ return fs25.readdirSync(adrsDir).filter((f) => f.endsWith(".md"));
5807
6030
  }
5808
6031
  function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
5809
- const projectName = path25.basename(projectRoot2);
6032
+ const projectName = path26.basename(projectRoot2);
5810
6033
  const caps = readCapabilities(forgehiveDir2);
5811
6034
  const memFiles = readMemoryFiles(forgehiveDir2);
5812
6035
  const commits = getRecentCommits(projectRoot2);
@@ -6021,13 +6244,13 @@ function getMetricsGitLog(projectRoot2, since) {
6021
6244
  }
6022
6245
 
6023
6246
  // src/sync.ts
6024
- import fs25 from "node:fs";
6025
- import path26 from "node:path";
6247
+ import fs26 from "node:fs";
6248
+ import path27 from "node:path";
6026
6249
  import { spawnSync as spawnSync10 } from "node:child_process";
6027
6250
  function getSyncStatus(forgehiveDir2) {
6028
- const memDir = path26.join(forgehiveDir2, "memory");
6029
- const files = fs25.existsSync(memDir) ? fs25.readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
6030
- const projectRoot2 = path26.dirname(forgehiveDir2);
6251
+ const memDir = path27.join(forgehiveDir2, "memory");
6252
+ const files = fs26.existsSync(memDir) ? fs26.readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
6253
+ const projectRoot2 = path27.dirname(forgehiveDir2);
6031
6254
  const configResult = spawnSync10(
6032
6255
  "git",
6033
6256
  ["config", "--get", "forgehive.sync-remote"],
@@ -6043,18 +6266,18 @@ function getSyncStatus(forgehiveDir2) {
6043
6266
  return { files, hasRemote: remote !== null, remote, branch };
6044
6267
  }
6045
6268
  function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
6046
- const projectRoot2 = path26.dirname(forgehiveDir2);
6047
- const memDir = path26.join(forgehiveDir2, "memory");
6048
- if (!fs25.existsSync(memDir)) {
6269
+ const projectRoot2 = path27.dirname(forgehiveDir2);
6270
+ const memDir = path27.join(forgehiveDir2, "memory");
6271
+ if (!fs26.existsSync(memDir)) {
6049
6272
  return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
6050
6273
  }
6051
- const files = fs25.readdirSync(memDir).filter((f) => f.endsWith(".md"));
6274
+ const files = fs26.readdirSync(memDir).filter((f) => f.endsWith(".md"));
6052
6275
  if (files.length === 0) {
6053
6276
  return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
6054
6277
  }
6055
6278
  const addResult = spawnSync10(
6056
6279
  "git",
6057
- ["add", path26.join(".forgehive", "memory")],
6280
+ ["add", path27.join(".forgehive", "memory")],
6058
6281
  { cwd: projectRoot2, encoding: "utf8" }
6059
6282
  );
6060
6283
  if (addResult.status !== 0) {
@@ -6082,9 +6305,9 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6082
6305
  return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
6083
6306
  }
6084
6307
  function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
6085
- const projectRoot2 = path26.dirname(forgehiveDir2);
6086
- const memDir = path26.join(forgehiveDir2, "memory");
6087
- fs25.mkdirSync(memDir, { recursive: true });
6308
+ const projectRoot2 = path27.dirname(forgehiveDir2);
6309
+ const memDir = path27.join(forgehiveDir2, "memory");
6310
+ fs26.mkdirSync(memDir, { recursive: true });
6088
6311
  const fetchResult = spawnSync10(
6089
6312
  "git",
6090
6313
  ["fetch", remote, branch],
@@ -6104,16 +6327,16 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6104
6327
  const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
6105
6328
  const imported = [];
6106
6329
  for (const remotePath of remoteFiles) {
6107
- const filename = path26.basename(remotePath);
6108
- const localPath = path26.join(memDir, filename);
6109
- if (!fs25.existsSync(localPath)) {
6330
+ const filename = path27.basename(remotePath);
6331
+ const localPath = path27.join(memDir, filename);
6332
+ if (!fs26.existsSync(localPath)) {
6110
6333
  const contentResult = spawnSync10(
6111
6334
  "git",
6112
6335
  ["show", `${remote}/${branch}:${remotePath}`],
6113
6336
  { cwd: projectRoot2, encoding: "utf8" }
6114
6337
  );
6115
6338
  if (contentResult.status === 0) {
6116
- fs25.writeFileSync(localPath, contentResult.stdout, "utf8");
6339
+ fs26.writeFileSync(localPath, contentResult.stdout, "utf8");
6117
6340
  imported.push(filename);
6118
6341
  }
6119
6342
  }
@@ -6126,8 +6349,8 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6126
6349
  }
6127
6350
 
6128
6351
  // src/background.ts
6129
- import fs26 from "node:fs";
6130
- import path27 from "node:path";
6352
+ import fs27 from "node:fs";
6353
+ import path28 from "node:path";
6131
6354
  import { spawn } from "node:child_process";
6132
6355
  var AGENT_ROLES = {
6133
6356
  kai: "Senior Engineer \u2014 implements features and fixes bugs",
@@ -6174,23 +6397,23 @@ Instructions:
6174
6397
  Work autonomously. Do not ask for clarification \u2014 use your best judgment based on the issue description and codebase context.`;
6175
6398
  }
6176
6399
  function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
6177
- const logsDir = path27.join(forgehiveDir2, "background-runs");
6178
- fs26.mkdirSync(logsDir, { recursive: true });
6400
+ const logsDir = path28.join(forgehiveDir2, "background-runs");
6401
+ fs27.mkdirSync(logsDir, { recursive: true });
6179
6402
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6180
- const logFile = path27.join(logsDir, `${agentId}-${timestamp2}.log`);
6403
+ const logFile = path28.join(logsDir, `${agentId}-${timestamp2}.log`);
6181
6404
  const prompt = buildAgentPrompt(issueUrl, agentId);
6182
- const logStream = fs26.openSync(logFile, "w");
6405
+ const logStream = fs27.openSync(logFile, "w");
6183
6406
  const child = spawn(
6184
6407
  "claude",
6185
6408
  ["-p", prompt, "--output-format", "text"],
6186
6409
  {
6187
- cwd: path27.dirname(forgehiveDir2),
6410
+ cwd: path28.dirname(forgehiveDir2),
6188
6411
  detached: true,
6189
6412
  stdio: ["ignore", logStream, logStream]
6190
6413
  }
6191
6414
  );
6192
6415
  child.unref();
6193
- fs26.closeSync(logStream);
6416
+ fs27.closeSync(logStream);
6194
6417
  return {
6195
6418
  pid: child.pid,
6196
6419
  logFile,
@@ -6200,11 +6423,11 @@ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
6200
6423
 
6201
6424
  // src/stories.ts
6202
6425
  init_js_yaml();
6203
- import fs27 from "node:fs";
6204
- import path28 from "node:path";
6426
+ import fs28 from "node:fs";
6427
+ import path29 from "node:path";
6205
6428
  function nextStoryId(storiesDir) {
6206
- if (!fs27.existsSync(storiesDir)) return "US-1";
6207
- 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));
6429
+ if (!fs28.existsSync(storiesDir)) return "US-1";
6430
+ const existing = fs28.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseInt(f.replace("US-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
6208
6431
  const max = existing.length > 0 ? Math.max(...existing) : 0;
6209
6432
  return `US-${max + 1}`;
6210
6433
  }
@@ -6236,7 +6459,7 @@ ${acLines || "- [ ] (noch nicht definiert)"}
6236
6459
  }
6237
6460
  function parseStoryFile(filePath) {
6238
6461
  try {
6239
- const content = fs27.readFileSync(filePath, "utf8");
6462
+ const content = fs28.readFileSync(filePath, "utf8");
6240
6463
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6241
6464
  if (!match) return null;
6242
6465
  const data = jsYaml.load(match[1]);
@@ -6256,7 +6479,7 @@ function parseStoryFile(filePath) {
6256
6479
  }
6257
6480
  }
6258
6481
  function createStory(storiesDir, title, epicId) {
6259
- fs27.mkdirSync(storiesDir, { recursive: true });
6482
+ fs28.mkdirSync(storiesDir, { recursive: true });
6260
6483
  const id = nextStoryId(storiesDir);
6261
6484
  const story = {
6262
6485
  id,
@@ -6269,33 +6492,33 @@ function createStory(storiesDir, title, epicId) {
6269
6492
  epicId: epicId ?? null,
6270
6493
  status: "backlog"
6271
6494
  };
6272
- fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6495
+ fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6273
6496
  return story;
6274
6497
  }
6275
6498
  function listStories(storiesDir) {
6276
- if (!fs27.existsSync(storiesDir)) return [];
6277
- 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) => {
6499
+ if (!fs28.existsSync(storiesDir)) return [];
6500
+ return fs28.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseStoryFile(path29.join(storiesDir, f))).filter((s) => s !== null).sort((a, b) => {
6278
6501
  const na = parseInt(a.id.replace("US-", ""), 10);
6279
6502
  const nb = parseInt(b.id.replace("US-", ""), 10);
6280
6503
  return na - nb;
6281
6504
  });
6282
6505
  }
6283
6506
  function getStory(storiesDir, id) {
6284
- const filePath = path28.join(storiesDir, `${id}.md`);
6285
- if (!fs27.existsSync(filePath)) return null;
6507
+ const filePath = path29.join(storiesDir, `${id}.md`);
6508
+ if (!fs28.existsSync(filePath)) return null;
6286
6509
  return parseStoryFile(filePath);
6287
6510
  }
6288
6511
  function updateStoryPoints(storiesDir, id, points) {
6289
6512
  const story = getStory(storiesDir, id);
6290
6513
  if (!story) throw new Error(`Story ${id} nicht gefunden`);
6291
6514
  story.points = points;
6292
- fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6515
+ fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6293
6516
  }
6294
6517
  function updateStoryStatus(storiesDir, id, status) {
6295
6518
  const story = getStory(storiesDir, id);
6296
6519
  if (!story) throw new Error(`Story ${id} nicht gefunden`);
6297
6520
  story.status = status;
6298
- fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6521
+ fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6299
6522
  }
6300
6523
  function formatStoryCard(story) {
6301
6524
  const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
@@ -6314,11 +6537,11 @@ function formatStoryCard(story) {
6314
6537
 
6315
6538
  // src/epics.ts
6316
6539
  init_js_yaml();
6317
- import fs28 from "node:fs";
6318
- import path29 from "node:path";
6540
+ import fs29 from "node:fs";
6541
+ import path30 from "node:path";
6319
6542
  function nextEpicId(epicsDir) {
6320
- if (!fs28.existsSync(epicsDir)) return "EPC-1";
6321
- 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));
6543
+ if (!fs29.existsSync(epicsDir)) return "EPC-1";
6544
+ const existing = fs29.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseInt(f.replace("EPC-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
6322
6545
  const max = existing.length > 0 ? Math.max(...existing) : 0;
6323
6546
  return `EPC-${max + 1}`;
6324
6547
  }
@@ -6328,7 +6551,8 @@ function epicToMarkdown(epic) {
6328
6551
  title: epic.title,
6329
6552
  goal: epic.goal,
6330
6553
  stories: epic.stories,
6331
- status: epic.status
6554
+ status: epic.status,
6555
+ prdId: epic.prdId
6332
6556
  });
6333
6557
  const storyLines = epic.stories.map((s) => `- ${s}`).join("\n");
6334
6558
  return `---
@@ -6345,7 +6569,7 @@ ${storyLines || "(noch keine Stories)"}
6345
6569
  }
6346
6570
  function parseEpicFile(filePath) {
6347
6571
  try {
6348
- const content = fs28.readFileSync(filePath, "utf8");
6572
+ const content = fs29.readFileSync(filePath, "utf8");
6349
6573
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6350
6574
  if (!match) return null;
6351
6575
  const data = jsYaml.load(match[1]);
@@ -6354,30 +6578,31 @@ function parseEpicFile(filePath) {
6354
6578
  title: data.title ?? "",
6355
6579
  goal: data.goal ?? "",
6356
6580
  stories: data.stories ?? [],
6357
- status: data.status ?? "active"
6581
+ status: data.status ?? "active",
6582
+ prdId: data.prdId ?? null
6358
6583
  };
6359
6584
  } catch {
6360
6585
  return null;
6361
6586
  }
6362
6587
  }
6363
- function createEpic(epicsDir, title, goal) {
6364
- fs28.mkdirSync(epicsDir, { recursive: true });
6588
+ function createEpic(epicsDir, title, goal, prdId) {
6589
+ fs29.mkdirSync(epicsDir, { recursive: true });
6365
6590
  const id = nextEpicId(epicsDir);
6366
- const epic = { id, title, goal: goal ?? "", stories: [], status: "active" };
6367
- fs28.writeFileSync(path29.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
6591
+ const epic = { id, title, goal: goal ?? "", stories: [], status: "active", prdId: prdId ?? null };
6592
+ fs29.writeFileSync(path30.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
6368
6593
  return epic;
6369
6594
  }
6370
6595
  function listEpics(epicsDir) {
6371
- if (!fs28.existsSync(epicsDir)) return [];
6372
- 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) => {
6596
+ if (!fs29.existsSync(epicsDir)) return [];
6597
+ return fs29.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseEpicFile(path30.join(epicsDir, f))).filter((e) => e !== null).sort((a, b) => {
6373
6598
  const na = parseInt(a.id.replace("EPC-", ""), 10);
6374
6599
  const nb = parseInt(b.id.replace("EPC-", ""), 10);
6375
6600
  return na - nb;
6376
6601
  });
6377
6602
  }
6378
6603
  function getEpic(epicsDir, id) {
6379
- const filePath = path29.join(epicsDir, `${id}.md`);
6380
- if (!fs28.existsSync(filePath)) return null;
6604
+ const filePath = path30.join(epicsDir, `${id}.md`);
6605
+ if (!fs29.existsSync(filePath)) return null;
6381
6606
  return parseEpicFile(filePath);
6382
6607
  }
6383
6608
  function formatEpicCard(epic, stories) {
@@ -6399,25 +6624,188 @@ function formatEpicCard(epic, stories) {
6399
6624
  return lines.join("\n");
6400
6625
  }
6401
6626
 
6627
+ // src/product.ts
6628
+ init_js_yaml();
6629
+ import fs30 from "node:fs";
6630
+ import path31 from "node:path";
6631
+ function slugify2(title) {
6632
+ return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-");
6633
+ }
6634
+ function nextPrdId(prdsDir) {
6635
+ if (!fs30.existsSync(prdsDir)) return "PRD-1";
6636
+ const existing = fs30.readdirSync(prdsDir).filter((f) => f.endsWith(".md")).map((f) => parsePrdFile(path31.join(prdsDir, f))).filter((p) => p !== null).map((p) => parseInt(p.id.replace("PRD-", ""), 10)).filter((n) => !isNaN(n));
6637
+ const max = existing.length > 0 ? Math.max(...existing) : 0;
6638
+ return `PRD-${max + 1}`;
6639
+ }
6640
+ function prdToMarkdown(prd) {
6641
+ const frontmatter = jsYaml.dump({
6642
+ id: prd.id,
6643
+ title: prd.title,
6644
+ date: prd.date,
6645
+ status: prd.status
6646
+ });
6647
+ return `---
6648
+ ${frontmatter}---
6649
+
6650
+ # PRD: ${prd.title}
6651
+
6652
+ ## Problem Statement
6653
+ <!-- Was ist das Problem? -->
6654
+
6655
+ ## Target Users
6656
+ <!-- Wer sind die Nutzer? -->
6657
+
6658
+ ## Goals
6659
+ <!-- Messbare Ziele -->
6660
+
6661
+ ## Non-Goals
6662
+ <!-- Explizit nicht in Scope -->
6663
+
6664
+ ## Requirements
6665
+ 1.
6666
+
6667
+ ## Success Metrics
6668
+
6669
+ ## Open Questions
6670
+ `;
6671
+ }
6672
+ function parsePrdFile(filePath) {
6673
+ try {
6674
+ const content = fs30.readFileSync(filePath, "utf8");
6675
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
6676
+ if (!match) return null;
6677
+ const data = jsYaml.load(match[1]);
6678
+ if (!data || typeof data.id !== "string") return null;
6679
+ return {
6680
+ id: data.id,
6681
+ title: typeof data.title === "string" ? data.title : "",
6682
+ date: typeof data.date === "string" ? data.date : "",
6683
+ status: data.status ?? "draft",
6684
+ filepath: filePath
6685
+ };
6686
+ } catch {
6687
+ return null;
6688
+ }
6689
+ }
6690
+ function createPrd(prdsDir, title) {
6691
+ fs30.mkdirSync(prdsDir, { recursive: true });
6692
+ const id = nextPrdId(prdsDir);
6693
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6694
+ const slug = slugify2(title);
6695
+ const filename = `${today}-${slug}.md`;
6696
+ const filepath = path31.join(prdsDir, filename);
6697
+ const prd = { id, title, date: today, status: "draft", filepath };
6698
+ fs30.writeFileSync(filepath, prdToMarkdown(prd), "utf8");
6699
+ return prd;
6700
+ }
6701
+ function listPrds(prdsDir) {
6702
+ if (!fs30.existsSync(prdsDir)) return [];
6703
+ return fs30.readdirSync(prdsDir).filter((f) => f.endsWith(".md")).map((f) => parsePrdFile(path31.join(prdsDir, f))).filter((p) => p !== null && /^PRD-\d+$/.test(p.id)).sort((a, b) => {
6704
+ const na = parseInt(a.id.replace("PRD-", ""), 10);
6705
+ const nb = parseInt(b.id.replace("PRD-", ""), 10);
6706
+ return na - nb;
6707
+ });
6708
+ }
6709
+ function getPrd(prdsDir, id) {
6710
+ if (!fs30.existsSync(prdsDir)) return null;
6711
+ const files = fs30.readdirSync(prdsDir).filter((f) => f.endsWith(".md"));
6712
+ for (const f of files) {
6713
+ const prd = parsePrdFile(path31.join(prdsDir, f));
6714
+ if (prd && prd.id === id) return prd;
6715
+ }
6716
+ return null;
6717
+ }
6718
+ function generateRoadmap(forgehiveDir2) {
6719
+ const prdsDir = path31.join(forgehiveDir2, "memory", "prds");
6720
+ const epicsDir = path31.join(forgehiveDir2, "memory", "epics");
6721
+ const storiesDir = path31.join(forgehiveDir2, "memory", "stories");
6722
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6723
+ const prds = listPrds(prdsDir);
6724
+ const epics = listEpics(epicsDir);
6725
+ const stories = listStories(storiesDir);
6726
+ const lines = [];
6727
+ lines.push("# Roadmap");
6728
+ lines.push("");
6729
+ lines.push(`_Generiert am ${today} von fh product roadmap_`);
6730
+ lines.push("");
6731
+ lines.push("---");
6732
+ lines.push("");
6733
+ for (const prd of prds) {
6734
+ lines.push(`## ${prd.id}: ${prd.title} [${prd.status}]`);
6735
+ lines.push("");
6736
+ lines.push(`> PRD-Datum: ${prd.date}`);
6737
+ lines.push("");
6738
+ const linkedEpics = epics.filter((e) => e.prdId === prd.id);
6739
+ if (linkedEpics.length === 0) {
6740
+ lines.push("_Keine Epics verkn\xFCpft_");
6741
+ } else {
6742
+ for (const epic of linkedEpics) {
6743
+ lines.push(`### ${epic.id}: ${epic.title} [${epic.status}]`);
6744
+ lines.push("");
6745
+ const epicStories = stories.filter((s) => s.epicId === epic.id);
6746
+ if (epicStories.length === 0) {
6747
+ lines.push("_Keine Stories_");
6748
+ } else {
6749
+ lines.push("| Story | Status | Punkte |");
6750
+ lines.push("|---|---|---|");
6751
+ let total = 0;
6752
+ for (const s of epicStories) {
6753
+ const pts = s.points !== null ? String(s.points) : "?";
6754
+ total += s.points ?? 0;
6755
+ lines.push(`| ${s.id}: ${s.title} | ${s.status} | ${pts} |`);
6756
+ }
6757
+ lines.push("");
6758
+ lines.push(`**Gesamt: ${total} Punkte**`);
6759
+ }
6760
+ lines.push("");
6761
+ }
6762
+ }
6763
+ lines.push("---");
6764
+ lines.push("");
6765
+ }
6766
+ const orphanEpics = epics.filter((e) => !e.prdId);
6767
+ if (orphanEpics.length > 0) {
6768
+ lines.push("## Ohne PRD");
6769
+ lines.push("");
6770
+ for (const epic of orphanEpics) {
6771
+ lines.push(`### ${epic.id}: ${epic.title} [${epic.status}]`);
6772
+ lines.push("");
6773
+ const epicStories = stories.filter((s) => s.epicId === epic.id);
6774
+ if (epicStories.length === 0) {
6775
+ lines.push("_Keine Stories_");
6776
+ } else {
6777
+ lines.push("| Story | Status | Punkte |");
6778
+ lines.push("|---|---|---|");
6779
+ for (const s of epicStories) {
6780
+ const pts = s.points !== null ? String(s.points) : "?";
6781
+ lines.push(`| ${s.id}: ${s.title} | ${s.status} | ${pts} |`);
6782
+ }
6783
+ }
6784
+ lines.push("");
6785
+ }
6786
+ }
6787
+ return lines.join("\n");
6788
+ }
6789
+
6402
6790
  // src/velocity.ts
6403
- import fs29 from "node:fs";
6404
- import path30 from "node:path";
6791
+ import fs31 from "node:fs";
6792
+ import path32 from "node:path";
6405
6793
  var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
6406
6794
  function recordVelocity(velocityFile, sprint, committed, delivered) {
6407
6795
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6408
6796
  const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
6409
6797
  const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
6410
6798
  `;
6411
- if (!fs29.existsSync(velocityFile)) {
6412
- fs29.mkdirSync(path30.dirname(velocityFile), { recursive: true });
6413
- fs29.writeFileSync(velocityFile, HEADER + row, "utf8");
6799
+ if (!fs31.existsSync(velocityFile)) {
6800
+ fs31.mkdirSync(path32.dirname(velocityFile), { recursive: true });
6801
+ fs31.writeFileSync(velocityFile, HEADER + row, "utf8");
6414
6802
  } else {
6415
- fs29.appendFileSync(velocityFile, row, "utf8");
6803
+ fs31.appendFileSync(velocityFile, row, "utf8");
6416
6804
  }
6417
6805
  }
6418
6806
  function getVelocityHistory(velocityFile) {
6419
- if (!fs29.existsSync(velocityFile)) return [];
6420
- const content = fs29.readFileSync(velocityFile, "utf8");
6807
+ if (!fs31.existsSync(velocityFile)) return [];
6808
+ const content = fs31.readFileSync(velocityFile, "utf8");
6421
6809
  const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
6422
6810
  return rows.map((row) => {
6423
6811
  const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
@@ -6458,22 +6846,22 @@ function formatVelocityReport(history) {
6458
6846
 
6459
6847
  // src/docs.ts
6460
6848
  init_js_yaml();
6461
- import fs30 from "node:fs";
6462
- import path31 from "node:path";
6849
+ import fs32 from "node:fs";
6850
+ import path33 from "node:path";
6463
6851
  import { spawnSync as spawnSync11 } from "node:child_process";
6464
6852
  var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
6465
- var IGNORE_DIRS3 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
6466
- var EXPORT_PATTERNS = [
6853
+ var IGNORE_DIRS4 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
6854
+ var EXPORT_PATTERNS2 = [
6467
6855
  /^export\s+(?:async\s+)?function\s+(\w+)/gm,
6468
6856
  /^export\s+(?:const|let|var)\s+(\w+)/gm,
6469
6857
  /^export\s+(?:class|interface|type|enum)\s+(\w+)/gm,
6470
6858
  /^export\s+default\s+(?:function\s+)?(\w+)?/gm
6471
6859
  ];
6472
6860
  function readCapabilities2(forgehiveDir2) {
6473
- const capPath = path31.join(forgehiveDir2, "capabilities.yaml");
6474
- if (!fs30.existsSync(capPath)) return {};
6861
+ const capPath = path33.join(forgehiveDir2, "capabilities.yaml");
6862
+ if (!fs32.existsSync(capPath)) return {};
6475
6863
  try {
6476
- return jsYaml.load(fs30.readFileSync(capPath, "utf8")) ?? {};
6864
+ return jsYaml.load(fs32.readFileSync(capPath, "utf8")) ?? {};
6477
6865
  } catch {
6478
6866
  return {};
6479
6867
  }
@@ -6500,11 +6888,11 @@ function extractCapabilityInfo(caps) {
6500
6888
  return { language, packageManager };
6501
6889
  }
6502
6890
  function readMemoryFiles2(forgehiveDir2) {
6503
- const memDir = path31.join(forgehiveDir2, "memory");
6504
- if (!fs30.existsSync(memDir)) return {};
6891
+ const memDir = path33.join(forgehiveDir2, "memory");
6892
+ if (!fs32.existsSync(memDir)) return {};
6505
6893
  const result = {};
6506
- for (const f of fs30.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
6507
- result[f] = fs30.readFileSync(path31.join(memDir, f), "utf8");
6894
+ for (const f of fs32.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
6895
+ result[f] = fs32.readFileSync(path33.join(memDir, f), "utf8");
6508
6896
  }
6509
6897
  return result;
6510
6898
  }
@@ -6515,7 +6903,7 @@ function getRecentCommits2(projectRoot2, n = 10) {
6515
6903
  }
6516
6904
  function extractExports(content) {
6517
6905
  const exports = [];
6518
- for (const pattern of EXPORT_PATTERNS) {
6906
+ for (const pattern of EXPORT_PATTERNS2) {
6519
6907
  pattern.lastIndex = 0;
6520
6908
  let m;
6521
6909
  while ((m = pattern.exec(content)) !== null) {
@@ -6527,19 +6915,19 @@ function extractExports(content) {
6527
6915
  function walkSourceFiles(dir) {
6528
6916
  const results = [];
6529
6917
  function walk(current) {
6530
- if (!fs30.existsSync(current)) return;
6531
- for (const entry of fs30.readdirSync(current, { withFileTypes: true })) {
6532
- if (IGNORE_DIRS3.includes(entry.name)) continue;
6533
- const full = path31.join(current, entry.name);
6918
+ if (!fs32.existsSync(current)) return;
6919
+ for (const entry of fs32.readdirSync(current, { withFileTypes: true })) {
6920
+ if (IGNORE_DIRS4.includes(entry.name)) continue;
6921
+ const full = path33.join(current, entry.name);
6534
6922
  if (entry.isDirectory()) walk(full);
6535
- else if (entry.isFile() && SOURCE_EXTS.includes(path31.extname(entry.name))) results.push(full);
6923
+ else if (entry.isFile() && SOURCE_EXTS.includes(path33.extname(entry.name))) results.push(full);
6536
6924
  }
6537
6925
  }
6538
6926
  walk(dir);
6539
6927
  return results;
6540
6928
  }
6541
6929
  function generateUserGuide(projectRoot2, forgehiveDir2) {
6542
- const projectName = path31.basename(projectRoot2);
6930
+ const projectName = path33.basename(projectRoot2);
6543
6931
  const caps = readCapabilities2(forgehiveDir2);
6544
6932
  const { language: lang, packageManager, entryPoints } = extractCapabilityInfo(caps);
6545
6933
  const memFiles = readMemoryFiles2(forgehiveDir2);
@@ -6618,8 +7006,8 @@ function generateUserGuide(projectRoot2, forgehiveDir2) {
6618
7006
  return lines.join("\n");
6619
7007
  }
6620
7008
  function generateApiReference(projectRoot2) {
6621
- const srcDir = path31.join(projectRoot2, "src");
6622
- const searchDir = fs30.existsSync(srcDir) ? srcDir : projectRoot2;
7009
+ const srcDir = path33.join(projectRoot2, "src");
7010
+ const searchDir = fs32.existsSync(srcDir) ? srcDir : projectRoot2;
6623
7011
  const files = walkSourceFiles(searchDir);
6624
7012
  const lines = [];
6625
7013
  lines.push("# API Reference");
@@ -6633,13 +7021,13 @@ function generateApiReference(projectRoot2) {
6633
7021
  for (const filePath of files) {
6634
7022
  let content = "";
6635
7023
  try {
6636
- content = fs30.readFileSync(filePath, "utf8");
7024
+ content = fs32.readFileSync(filePath, "utf8");
6637
7025
  } catch {
6638
7026
  continue;
6639
7027
  }
6640
7028
  const exports = extractExports(content);
6641
7029
  if (exports.length === 0) continue;
6642
- const relPath = path31.relative(projectRoot2, filePath);
7030
+ const relPath = path33.relative(projectRoot2, filePath);
6643
7031
  lines.push(`## \`${relPath}\``);
6644
7032
  lines.push("");
6645
7033
  lines.push("**Exports:**");
@@ -6651,27 +7039,314 @@ function generateApiReference(projectRoot2) {
6651
7039
  }
6652
7040
  function listExistingDocs(projectRoot2) {
6653
7041
  const docs = [];
6654
- const docsDir = path31.join(projectRoot2, "docs");
6655
- if (fs30.existsSync(docsDir)) {
6656
- for (const f of fs30.readdirSync(docsDir)) {
6657
- if (f.endsWith(".md")) docs.push(path31.join(docsDir, f));
7042
+ const docsDir = path33.join(projectRoot2, "docs");
7043
+ if (fs32.existsSync(docsDir)) {
7044
+ for (const f of fs32.readdirSync(docsDir)) {
7045
+ if (f.endsWith(".md")) docs.push(path33.join(docsDir, f));
6658
7046
  }
6659
7047
  }
6660
7048
  const rootDocs = ["README.md", "CHANGELOG.md", "ONBOARDING.md", "CONTRIBUTING.md"];
6661
7049
  for (const f of rootDocs) {
6662
- const full = path31.join(projectRoot2, f);
6663
- if (fs30.existsSync(full)) docs.push(full);
7050
+ const full = path33.join(projectRoot2, f);
7051
+ if (fs32.existsSync(full)) docs.push(full);
6664
7052
  }
6665
7053
  return docs;
6666
7054
  }
6667
7055
 
7056
+ // src/router.ts
7057
+ import fs33 from "node:fs";
7058
+ import path34 from "node:path";
7059
+ var AGENT_KEYWORDS = {
7060
+ vera: ["security", "auth", "vulnerability", "owasp", "cve", "gdpr", "compliance", "penetration", "exploit"],
7061
+ sam: ["test", "coverage", "qa", "quality", "spec", "regression", "fixture", "assertion", "e2e"],
7062
+ suki: ["design", "ux", "user experience", "wireframe", "flow", "empathy", "persona", "interface"],
7063
+ viktor: ["architecture", "refactor", "structure", "pattern", "scalability", "coupling", "abstraction"],
7064
+ eli: ["docs", "documentation", "readme", "onboarding", "guide", "tutorial", "wiki", "changelog"],
7065
+ nora: ["research", "analyze", "compare", "benchmark", "investigate", "evidence", "survey"],
7066
+ remy: ["product", "feature", "requirement", "roadmap", "user story", "prd", "launch", "strategy"],
7067
+ kai: ["bug", "fix", "implement", "function", "error", "crash", "broken", "failing"]
7068
+ };
7069
+ var AGENT_DESCRIPTIONS = {
7070
+ vera: "Security Analyst",
7071
+ sam: "QA & Test Architect",
7072
+ suki: "UX Designer",
7073
+ viktor: "System Architect",
7074
+ eli: "Technical Writer",
7075
+ nora: "Senior Research Analyst",
7076
+ remy: "Creative Strategist",
7077
+ kai: "Senior Engineer"
7078
+ };
7079
+ var PARTY_DESCRIPTIONS = {
7080
+ build: "Viktor + Kai + Sam \u2014 Architecture + Engineering + QA",
7081
+ review: "Kai + Sam + Eli \u2014 Code Review + QA + Docs",
7082
+ refactor: "Viktor + Sam + Eli \u2014 Refactor + Tests + Docs",
7083
+ security: "Vera + Sam \u2014 Security Review + QA",
7084
+ full: "All agents"
7085
+ };
7086
+ function scoreAgents(task) {
7087
+ const lower = task.toLowerCase();
7088
+ const scores = /* @__PURE__ */ new Map();
7089
+ for (const [agent, keywords] of Object.entries(AGENT_KEYWORDS)) {
7090
+ const matched = [];
7091
+ for (const kw of keywords) {
7092
+ if (lower.includes(kw)) {
7093
+ matched.push(kw);
7094
+ }
7095
+ }
7096
+ if (matched.length > 0) {
7097
+ scores.set(agent, matched);
7098
+ }
7099
+ }
7100
+ return scores;
7101
+ }
7102
+ var _lastTask = "";
7103
+ function resolvePartySet(scores) {
7104
+ const activeAgents = [...scores.keys()];
7105
+ if (activeAgents.length >= 3) {
7106
+ return "full";
7107
+ }
7108
+ if (scores.has("vera") && _lastTask.toLowerCase().includes("review")) {
7109
+ return "security";
7110
+ }
7111
+ if (scores.has("viktor") && scores.has("sam")) {
7112
+ return "refactor";
7113
+ }
7114
+ if (scores.has("viktor") && scores.has("kai")) {
7115
+ return "build";
7116
+ }
7117
+ if (scores.has("suki") && scores.has("kai")) {
7118
+ return "build";
7119
+ }
7120
+ return null;
7121
+ }
7122
+ function routeTask(task) {
7123
+ _lastTask = task;
7124
+ const scores = scoreAgents(task);
7125
+ const partySet = resolvePartySet(scores);
7126
+ if (partySet) {
7127
+ const agentList = [...scores.keys()];
7128
+ const matchedKws = agentList.flatMap((a) => scores.get(a) ?? []);
7129
+ return {
7130
+ type: "party",
7131
+ partySet,
7132
+ confidence: "high",
7133
+ reasoning: `Multi-domain keywords erkannt (${matchedKws.slice(0, 4).join(", ")})`
7134
+ };
7135
+ }
7136
+ if (scores.size === 0) {
7137
+ return {
7138
+ type: "agent",
7139
+ agent: "kai",
7140
+ confidence: "low",
7141
+ reasoning: "Keine spezifischen Keywords erkannt \u2014 Fallback zu Kai"
7142
+ };
7143
+ }
7144
+ let topAgent = "kai";
7145
+ let topCount = 0;
7146
+ let secondCount = 0;
7147
+ for (const [agent, kws] of scores.entries()) {
7148
+ if (kws.length > topCount) {
7149
+ secondCount = topCount;
7150
+ topCount = kws.length;
7151
+ topAgent = agent;
7152
+ } else if (kws.length > secondCount) {
7153
+ secondCount = kws.length;
7154
+ }
7155
+ }
7156
+ const topKeywords = scores.get(topAgent) ?? [];
7157
+ const confidence = topCount >= 2 && topCount > secondCount ? "high" : "medium";
7158
+ return {
7159
+ type: "agent",
7160
+ agent: topAgent,
7161
+ confidence,
7162
+ reasoning: `Keywords erkannt: ${topKeywords.join(", ")}`
7163
+ };
7164
+ }
7165
+ function resolveWorktreePath(forgehiveDir2, agent) {
7166
+ if (!agent) return void 0;
7167
+ const worktreeFile = path34.join(forgehiveDir2, "agents", agent, "worktree");
7168
+ if (fs33.existsSync(worktreeFile)) {
7169
+ return fs33.readFileSync(worktreeFile, "utf8").trim();
7170
+ }
7171
+ return void 0;
7172
+ }
7173
+ function formatRoutingResult(result, worktreePath) {
7174
+ const lines = [];
7175
+ if (result.type === "party" && result.partySet) {
7176
+ const desc = PARTY_DESCRIPTIONS[result.partySet] ?? result.partySet;
7177
+ lines.push(`Empfehlung: Party-Set "${result.partySet}" \u2014 ${desc}`);
7178
+ lines.push(`Begr\xFCndung: ${result.reasoning}`);
7179
+ lines.push("");
7180
+ lines.push("Starten:");
7181
+ lines.push(` fh party run ${result.partySet}`);
7182
+ } else {
7183
+ const agent = result.agent ?? "kai";
7184
+ const desc = AGENT_DESCRIPTIONS[agent] ?? agent;
7185
+ lines.push(`Empfehlung: ${agent.charAt(0).toUpperCase() + agent.slice(1)} \u2014 ${desc}`);
7186
+ lines.push(`Begr\xFCndung: ${result.reasoning}`);
7187
+ lines.push(`Confidence: ${result.confidence}`);
7188
+ lines.push("");
7189
+ lines.push("Starten:");
7190
+ const wtPath = worktreePath ?? `<${agent}-worktree>`;
7191
+ lines.push(` cd ${wtPath} && claude`);
7192
+ }
7193
+ return lines.join("\n");
7194
+ }
7195
+
7196
+ // src/github.ts
7197
+ init_js_yaml();
7198
+ import fs34 from "node:fs";
7199
+ import path35 from "node:path";
7200
+ import https from "node:https";
7201
+ function loadGitHubConfig(forgehiveDir2) {
7202
+ const configPath = path35.join(forgehiveDir2, "github.yaml");
7203
+ if (!fs34.existsSync(configPath)) {
7204
+ throw new Error("Nicht konfiguriert. F\xFChre zuerst aus: fh github setup");
7205
+ }
7206
+ let parsed;
7207
+ try {
7208
+ parsed = jsYaml.load(fs34.readFileSync(configPath, "utf8"));
7209
+ } catch {
7210
+ throw new Error("Ung\xFCltige Konfigurationsdatei: .forgehive/github.yaml ist kein g\xFCltiges YAML.");
7211
+ }
7212
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
7213
+ throw new Error("Ung\xFCltige Konfigurationsdatei: .forgehive/github.yaml enth\xE4lt kein Objekt.");
7214
+ }
7215
+ const data = parsed;
7216
+ if (!data.repo) {
7217
+ throw new Error("Nicht konfiguriert. F\xFChre zuerst aus: fh github setup");
7218
+ }
7219
+ return { repo: data.repo };
7220
+ }
7221
+ function saveGitHubConfig(forgehiveDir2, config) {
7222
+ fs34.mkdirSync(forgehiveDir2, { recursive: true });
7223
+ const configPath = path35.join(forgehiveDir2, "github.yaml");
7224
+ fs34.writeFileSync(configPath, jsYaml.dump({ repo: config.repo }), "utf8");
7225
+ }
7226
+ function githubGet(apiPath, token) {
7227
+ return new Promise((resolve, reject) => {
7228
+ const options = {
7229
+ hostname: "api.github.com",
7230
+ path: apiPath,
7231
+ headers: {
7232
+ "Authorization": `Bearer ${token}`,
7233
+ "User-Agent": "forgehive-cli",
7234
+ "Accept": "application/vnd.github.v3+json"
7235
+ }
7236
+ };
7237
+ https.get(options, (res) => {
7238
+ let data = "";
7239
+ res.on("data", (chunk) => {
7240
+ data += chunk;
7241
+ });
7242
+ res.on("error", reject);
7243
+ res.on("end", () => {
7244
+ if (res.statusCode === 401) {
7245
+ reject(new Error("Ung\xFCltiger Token. \xDCberpr\xFCfe dein GitHub-Token."));
7246
+ return;
7247
+ }
7248
+ if (res.statusCode === 404) {
7249
+ reject(new Error(`Repository oder Ressource nicht gefunden.`));
7250
+ return;
7251
+ }
7252
+ if (res.statusCode === 403) {
7253
+ const reset = res.headers["x-ratelimit-reset"];
7254
+ const resetTime = reset ? new Date(parseInt(reset, 10) * 1e3).toLocaleTimeString() : "unbekannt";
7255
+ reject(new Error(`Rate Limit erreicht. Wird zur\xFCckgesetzt um ${resetTime}.`));
7256
+ return;
7257
+ }
7258
+ if ((res.statusCode ?? 0) >= 400) {
7259
+ reject(new Error(`GitHub API ${res.statusCode}: ${data}`));
7260
+ return;
7261
+ }
7262
+ try {
7263
+ resolve(JSON.parse(data));
7264
+ } catch (e) {
7265
+ reject(new Error(`JSON parse error: ${e.message}`));
7266
+ }
7267
+ });
7268
+ }).on("error", reject);
7269
+ });
7270
+ }
7271
+ async function fetchOpenIssues(owner, repo, token) {
7272
+ const issues = [];
7273
+ let page = 1;
7274
+ while (true) {
7275
+ const batch = await githubGet(
7276
+ `/repos/${owner}/${repo}/issues?state=open&per_page=100&page=${page}&filter=issues`,
7277
+ token
7278
+ );
7279
+ if (batch.length === 0) break;
7280
+ const filtered = batch.filter((i) => !i.pull_request);
7281
+ issues.push(...filtered);
7282
+ if (batch.length < 100) break;
7283
+ page++;
7284
+ }
7285
+ return issues;
7286
+ }
7287
+ async function fetchPR(owner, repo, number, token) {
7288
+ return githubGet(`/repos/${owner}/${repo}/pulls/${number}`, token);
7289
+ }
7290
+ async function fetchPRFiles(owner, repo, number, token) {
7291
+ const files = [];
7292
+ let page = 1;
7293
+ while (true) {
7294
+ const batch = await githubGet(
7295
+ `/repos/${owner}/${repo}/pulls/${number}/files?per_page=100&page=${page}`,
7296
+ token
7297
+ );
7298
+ if (batch.length === 0) break;
7299
+ files.push(...batch);
7300
+ if (batch.length < 100) break;
7301
+ page++;
7302
+ }
7303
+ return files;
7304
+ }
7305
+ function issueAlreadySynced(storiesDir, issueNumber) {
7306
+ if (!fs34.existsSync(storiesDir)) return false;
7307
+ const pattern = new RegExp(`GitHub: #${issueNumber}(?![\\d\\w])`);
7308
+ return fs34.readdirSync(storiesDir).filter((f) => f.endsWith(".md")).some((f) => pattern.test(fs34.readFileSync(path35.join(storiesDir, f), "utf8")));
7309
+ }
7310
+ function appendGitHubRef(storiesDir, storyId, issue, repoFullName) {
7311
+ const filePath = path35.join(storiesDir, `${storyId}.md`);
7312
+ if (!fs34.existsSync(filePath)) {
7313
+ throw new Error(`Story-Datei nicht gefunden: ${storyId}.md`);
7314
+ }
7315
+ const existing = fs34.readFileSync(filePath, "utf8");
7316
+ const ref = `
7317
+ ## GitHub
7318
+
7319
+ GitHub: #${issue.number} \u2014 ${issue.html_url}
7320
+ `;
7321
+ fs34.writeFileSync(filePath, existing.trimEnd() + "\n" + ref, "utf8");
7322
+ }
7323
+ function formatPRContext(pr, files, repoFullName) {
7324
+ const body = pr.body?.trim() || "(keine Beschreibung)";
7325
+ const fileLines = files.map((f) => `- \`${f.filename}\` (${f.status})`).join("\n");
7326
+ return [
7327
+ `# PR #${pr.number}: ${pr.title}`,
7328
+ "",
7329
+ `**Branch:** ${pr.head.ref} \u2192 ${pr.base.ref}`,
7330
+ `**Autor:** @${pr.user.login}`,
7331
+ `**Diff-URL:** https://github.com/${repoFullName}/pull/${pr.number}/files`,
7332
+ "",
7333
+ "## Beschreibung",
7334
+ "",
7335
+ body,
7336
+ "",
7337
+ "## Ge\xE4nderte Dateien",
7338
+ "",
7339
+ fileLines || "(keine Dateien)"
7340
+ ].join("\n");
7341
+ }
7342
+
6668
7343
  // src/cli.ts
6669
7344
  import { createRequire } from "node:module";
6670
7345
  var require2 = createRequire(import.meta.url);
6671
7346
  var { version } = require2("../package.json");
6672
7347
  var [, , command, subcommand, ...rest] = process.argv;
6673
7348
  var projectRoot = process.cwd();
6674
- var forgehiveDir = path32.join(projectRoot, ".forgehive");
7349
+ var forgehiveDir = path36.join(projectRoot, ".forgehive");
6675
7350
  if (command === "--version" || command === "-v") {
6676
7351
  console.log(version);
6677
7352
  process.exit(0);
@@ -6705,7 +7380,7 @@ CI
6705
7380
  fh ci --init Generate GitHub Actions workflow
6706
7381
 
6707
7382
  CODEBASE
6708
- fh map Codebase structure map
7383
+ fh map [--semantic] Codebase structure map + semantic graph
6709
7384
  fh onboard [--output path] Generate ONBOARDING.md
6710
7385
  fh changelog [--since tag] Semantic changelog from git
6711
7386
  fh metrics [--since date] Developer productivity metrics
@@ -6716,6 +7391,11 @@ CODEBASE
6716
7391
  fh docs changelog [--since tag] Generate changelog
6717
7392
  fh docs adr "<title>" Create Architecture Decision Record
6718
7393
 
7394
+ GITHUB
7395
+ fh github setup Configure GitHub token and repository
7396
+ fh github sync Import open Issues as Stories (idempotent)
7397
+ fh github pr <number> Fetch PR context for code review
7398
+
6719
7399
  SPRINT PLANNING
6720
7400
  fh story create <title> [--epic EPC-N] [--points N]
6721
7401
  fh story list [--epic EPC-N]
@@ -6729,8 +7409,10 @@ SPRINT PLANNING
6729
7409
  fh velocity record <N> --committed N --delivered N
6730
7410
 
6731
7411
  TEAM
7412
+ fh ask "<task>" Route a task to the right agent or party set
6732
7413
  fh sync push|pull [--remote origin --branch forgehive-memory]
6733
7414
  fh run <issue-url> [--agent name] [--label label]
7415
+ fh run "<task>" Freetext task \u2014 auto-routes like fh ask
6734
7416
  fh memory show|clean|export|prune|snapshot
6735
7417
  fh memory adr list|"<title>"
6736
7418
 
@@ -6751,15 +7433,15 @@ COST
6751
7433
  process.exit(0);
6752
7434
  }
6753
7435
  function loadClaudeMdBlock() {
6754
- const templatePath = path32.join(
6755
- path32.dirname(new URL(import.meta.url).pathname),
7436
+ const templatePath = path36.join(
7437
+ path36.dirname(new URL(import.meta.url).pathname),
6756
7438
  "..",
6757
7439
  "forgehive",
6758
7440
  "templates",
6759
7441
  "claude-md.block.md"
6760
7442
  );
6761
- if (!fs31.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6762
- return fs31.readFileSync(templatePath, "utf8");
7443
+ if (!fs35.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
7444
+ return fs35.readFileSync(templatePath, "utf8");
6763
7445
  }
6764
7446
  async function promptConfirm(question) {
6765
7447
  if (!process.stdin.isTTY) return false;
@@ -6788,7 +7470,7 @@ if (command === "init") {
6788
7470
  console.error(" Installation: https://git-scm.com");
6789
7471
  process.exit(1);
6790
7472
  }
6791
- const forgehiveDirExists = fs31.existsSync(forgehiveDir);
7473
+ const forgehiveDirExists = fs35.existsSync(forgehiveDir);
6792
7474
  if (forgehiveDirExists && !rest.includes("--force")) {
6793
7475
  console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
6794
7476
  console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
@@ -6806,9 +7488,9 @@ if (command === "init") {
6806
7488
  const block = loadClaudeMdBlock();
6807
7489
  writeForgehiveDir(projectRoot, scanResult, capMap, block);
6808
7490
  const hash = computeHash(projectRoot);
6809
- fs31.writeFileSync(path32.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6810
- const runtimeDir = path32.join(
6811
- path32.dirname(new URL(import.meta.url).pathname),
7491
+ fs35.writeFileSync(path36.join(forgehiveDir, ".scan-hash"), hash, "utf8");
7492
+ const runtimeDir = path36.join(
7493
+ path36.dirname(new URL(import.meta.url).pathname),
6812
7494
  "..",
6813
7495
  "forgehive"
6814
7496
  );
@@ -6850,7 +7532,7 @@ if (command === "init") {
6850
7532
  process.exit(1);
6851
7533
  }
6852
7534
  } else if (command === "memory") {
6853
- if (!fs31.existsSync(forgehiveDir)) {
7535
+ if (!fs35.existsSync(forgehiveDir)) {
6854
7536
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6855
7537
  process.exit(1);
6856
7538
  }
@@ -6859,7 +7541,7 @@ if (command === "init") {
6859
7541
  } else if (subcommand === "clean") {
6860
7542
  cleanMemory(forgehiveDir);
6861
7543
  } else if (subcommand === "export") {
6862
- const outputPath = rest[0] ?? path32.join(projectRoot, "forgehive-memory-export.md");
7544
+ const outputPath = rest[0] ?? path36.join(projectRoot, "forgehive-memory-export.md");
6863
7545
  try {
6864
7546
  exportMemory(forgehiveDir, outputPath);
6865
7547
  } catch (err) {
@@ -6907,7 +7589,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6907
7589
  } else if (subcommand === "snapshot") {
6908
7590
  const snapAction = rest[0];
6909
7591
  if (snapAction === "export") {
6910
- const outPath = rest[1] ?? path32.join(
7592
+ const outPath = rest[1] ?? path36.join(
6911
7593
  projectRoot,
6912
7594
  `forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
6913
7595
  );
@@ -6947,11 +7629,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6947
7629
  process.exit(1);
6948
7630
  }
6949
7631
  } else if (command === "scan" && subcommand === "--update") {
6950
- if (!fs31.existsSync(forgehiveDir)) {
7632
+ if (!fs35.existsSync(forgehiveDir)) {
6951
7633
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6952
7634
  process.exit(1);
6953
7635
  }
6954
- const savedHash = fs31.existsSync(path32.join(forgehiveDir, ".scan-hash")) ? fs31.readFileSync(path32.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
7636
+ const savedHash = fs35.existsSync(path36.join(forgehiveDir, ".scan-hash")) ? fs35.readFileSync(path36.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
6955
7637
  const currentHash = computeHash(projectRoot);
6956
7638
  if (savedHash === currentHash) {
6957
7639
  console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
@@ -6959,7 +7641,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6959
7641
  }
6960
7642
  console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
6961
7643
  const oldDoc = jsYaml.load(
6962
- fs31.readFileSync(path32.join(forgehiveDir, "capabilities.yaml"), "utf8")
7644
+ fs35.readFileSync(path36.join(forgehiveDir, "capabilities.yaml"), "utf8")
6963
7645
  );
6964
7646
  const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
6965
7647
  const scanResult = scan(projectRoot);
@@ -6979,16 +7661,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6979
7661
  console.log();
6980
7662
  const block = loadClaudeMdBlock();
6981
7663
  writeForgehiveDir(projectRoot, scanResult, newMap, block);
6982
- fs31.writeFileSync(path32.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
7664
+ fs35.writeFileSync(path36.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6983
7665
  console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
6984
7666
  console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
6985
7667
  }
6986
7668
  } else if (command === "scan" && subcommand === "--check") {
6987
- if (!fs31.existsSync(path32.join(forgehiveDir, ".scan-hash"))) {
7669
+ if (!fs35.existsSync(path36.join(forgehiveDir, ".scan-hash"))) {
6988
7670
  console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
6989
7671
  process.exit(1);
6990
7672
  }
6991
- const saved = fs31.readFileSync(path32.join(forgehiveDir, ".scan-hash"), "utf8").trim();
7673
+ const saved = fs35.readFileSync(path36.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6992
7674
  const current = computeHash(projectRoot);
6993
7675
  if (saved !== current) {
6994
7676
  console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
@@ -6997,7 +7679,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6997
7679
  }
6998
7680
  console.log("\u2713 capabilities.yaml ist aktuell");
6999
7681
  } else if (command === "skills") {
7000
- if (!fs31.existsSync(forgehiveDir)) {
7682
+ if (!fs35.existsSync(forgehiveDir)) {
7001
7683
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7002
7684
  process.exit(1);
7003
7685
  }
@@ -7033,7 +7715,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
7033
7715
  process.exit(1);
7034
7716
  }
7035
7717
  } else if (command === "party") {
7036
- if (!fs31.existsSync(forgehiveDir)) {
7718
+ if (!fs35.existsSync(forgehiveDir)) {
7037
7719
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7038
7720
  process.exit(1);
7039
7721
  }
@@ -7154,7 +7836,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
7154
7836
  }
7155
7837
  }
7156
7838
  } else if (command === "wire") {
7157
- if (!fs31.existsSync(forgehiveDir)) {
7839
+ if (!fs35.existsSync(forgehiveDir)) {
7158
7840
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7159
7841
  process.exit(1);
7160
7842
  }
@@ -7193,7 +7875,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7193
7875
  const limitIdx = allArgs.indexOf("--limit");
7194
7876
  const alertIdx = allArgs.indexOf("--alert");
7195
7877
  if (limitIdx !== -1) {
7196
- if (!fs31.existsSync(forgehiveDir)) {
7878
+ if (!fs35.existsSync(forgehiveDir)) {
7197
7879
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7198
7880
  process.exit(1);
7199
7881
  }
@@ -7219,14 +7901,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7219
7901
  }
7220
7902
  const sessions = parseCostSessions(projectRoot);
7221
7903
  console.log(formatCostReport(sessions, range));
7222
- if (fs31.existsSync(forgehiveDir)) {
7904
+ if (fs35.existsSync(forgehiveDir)) {
7223
7905
  const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
7224
7906
  const status = checkSpendStatus(forgehiveDir, total);
7225
7907
  if (status.message) console.log("\n" + status.message);
7226
7908
  }
7227
7909
  }
7228
7910
  } else if (command === "watch") {
7229
- if (!fs31.existsSync(forgehiveDir)) {
7911
+ if (!fs35.existsSync(forgehiveDir)) {
7230
7912
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7231
7913
  process.exit(1);
7232
7914
  }
@@ -7242,7 +7924,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7242
7924
  process.exit(0);
7243
7925
  });
7244
7926
  } else if (command === "mcp") {
7245
- if (!fs31.existsSync(forgehiveDir)) {
7927
+ if (!fs35.existsSync(forgehiveDir)) {
7246
7928
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7247
7929
  process.exit(1);
7248
7930
  }
@@ -7353,7 +8035,7 @@ Setze diese Umgebungsvariablen:`);
7353
8035
  process.exit(1);
7354
8036
  }
7355
8037
  } else if (command === "security") {
7356
- if (!fs31.existsSync(forgehiveDir)) {
8038
+ if (!fs35.existsSync(forgehiveDir)) {
7357
8039
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7358
8040
  process.exit(1);
7359
8041
  }
@@ -7439,8 +8121,8 @@ Setze diese Umgebungsvariablen:`);
7439
8121
  `);
7440
8122
  const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
7441
8123
  const text = formatSecurityReport(report);
7442
- const reportPath = path32.join(forgehiveDir, "security-report.md");
7443
- fs31.writeFileSync(reportPath, text, "utf8");
8124
+ const reportPath = path36.join(forgehiveDir, "security-report.md");
8125
+ fs35.writeFileSync(reportPath, text, "utf8");
7444
8126
  console.log(text);
7445
8127
  console.log(`
7446
8128
  \u2713 Report gespeichert: ${reportPath}`);
@@ -7452,8 +8134,8 @@ Setze diese Umgebungsvariablen:`);
7452
8134
  } else if (subcommand === "permissions") {
7453
8135
  const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
7454
8136
  writePermissions2(forgehiveDir);
7455
- const permPath = path32.join(forgehiveDir, "harness", "permissions.yaml");
7456
- console.log(fs31.readFileSync(permPath, "utf8"));
8137
+ const permPath = path36.join(forgehiveDir, "harness", "permissions.yaml");
8138
+ console.log(fs35.readFileSync(permPath, "utf8"));
7457
8139
  } else {
7458
8140
  console.error(`Unbekannter security-Subcommand: ${subcommand}`);
7459
8141
  console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
@@ -7465,36 +8147,62 @@ Setze diese Umgebungsvariablen:`);
7465
8147
  const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
7466
8148
  const initFlag = allCiArgs.includes("--init");
7467
8149
  if (initFlag) {
7468
- const ghDir = path32.join(projectRoot, ".github", "workflows");
7469
- fs31.mkdirSync(ghDir, { recursive: true });
7470
- const outPath = path32.join(ghDir, "forgehive.yml");
7471
- fs31.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
8150
+ const ghDir = path36.join(projectRoot, ".github", "workflows");
8151
+ fs35.mkdirSync(ghDir, { recursive: true });
8152
+ const outPath = path36.join(ghDir, "forgehive.yml");
8153
+ fs35.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
7472
8154
  console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
7473
8155
  } else {
7474
8156
  const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
7475
8157
  const output = formatCiReport(report, format);
7476
8158
  console.log(output);
7477
8159
  if (format === "json") {
7478
- fs31.mkdirSync(forgehiveDir, { recursive: true });
7479
- fs31.writeFileSync(path32.join(forgehiveDir, "ci-report.json"), output, "utf8");
8160
+ fs35.mkdirSync(forgehiveDir, { recursive: true });
8161
+ fs35.writeFileSync(path36.join(forgehiveDir, "ci-report.json"), output, "utf8");
7480
8162
  }
7481
8163
  if (report.status === "fail") process.exit(1);
7482
8164
  }
7483
8165
  } else if (command === "map") {
8166
+ const semanticFlag = rest.includes("--semantic");
7484
8167
  const map2 = generateMap(projectRoot);
7485
- const md = formatMap(map2);
7486
- const mapPath = path32.join(forgehiveDir, "map.md");
7487
- fs31.mkdirSync(forgehiveDir, { recursive: true });
7488
- fs31.writeFileSync(mapPath, md, "utf8");
7489
- console.log(md);
7490
- console.log(`
8168
+ const semantic = buildSemanticMap(projectRoot);
8169
+ const mapPath = path36.join(forgehiveDir, "map.md");
8170
+ fs35.mkdirSync(forgehiveDir, { recursive: true });
8171
+ if (semanticFlag) {
8172
+ const fullMd = formatMap(map2, semantic);
8173
+ const marker = "\n## Semantic Graph\n";
8174
+ const markerIdx = fullMd.indexOf(marker);
8175
+ const semanticMd = markerIdx >= 0 ? "## Semantic Graph\n" + fullMd.slice(markerIdx + marker.length) : fullMd;
8176
+ fs35.writeFileSync(mapPath, fullMd, "utf8");
8177
+ console.log(semanticMd);
8178
+ console.log(`
8179
+ \u2714 Codebase-Map gespeichert (Semantic Graph hervorgehoben): ${mapPath}`);
8180
+ } else {
8181
+ const md = formatMap(map2, semantic);
8182
+ fs35.writeFileSync(mapPath, md, "utf8");
8183
+ console.log(md);
8184
+ console.log(`
7491
8185
  \u2714 Codebase-Map gespeichert: ${mapPath}`);
8186
+ }
7492
8187
  } else if (command === "onboard") {
7493
8188
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7494
- const outputPath = outputArg ?? path32.join(projectRoot, "ONBOARDING.md");
8189
+ const outputPath = outputArg ?? path36.join(projectRoot, "ONBOARDING.md");
7495
8190
  const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
7496
- fs31.writeFileSync(outputPath, doc, "utf8");
8191
+ fs35.writeFileSync(outputPath, doc, "utf8");
7497
8192
  console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
8193
+ } else if (command === "ask") {
8194
+ const taskArg = subcommand ? [subcommand, ...rest.filter((r) => !r.startsWith("--"))].join(" ") : rest.filter((r) => !r.startsWith("--")).join(" ");
8195
+ if (!taskArg.trim()) {
8196
+ console.error('Usage: fh ask "<aufgabenbeschreibung>"');
8197
+ process.exit(1);
8198
+ }
8199
+ const askResult = routeTask(taskArg);
8200
+ const askWorktree = resolveWorktreePath(forgehiveDir, askResult.agent);
8201
+ console.log(`
8202
+ Task: "${taskArg}"
8203
+ `);
8204
+ console.log(formatRoutingResult(askResult, askWorktree));
8205
+ console.log();
7498
8206
  } else if (command === "changelog") {
7499
8207
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
7500
8208
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
@@ -7503,28 +8211,28 @@ Setze diese Umgebungsvariablen:`);
7503
8211
  const commits = parseGitLog(rawLog);
7504
8212
  let version2 = "unreleased";
7505
8213
  try {
7506
- const pkgPath = path32.join(projectRoot, "package.json");
7507
- if (fs31.existsSync(pkgPath)) {
7508
- const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
8214
+ const pkgPath = path36.join(projectRoot, "package.json");
8215
+ if (fs35.existsSync(pkgPath)) {
8216
+ const pkg = JSON.parse(fs35.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
7509
8217
  version2 = pkg.version ?? "unreleased";
7510
8218
  }
7511
8219
  } catch {
7512
8220
  }
7513
8221
  const md = formatChangelog(commits, version2);
7514
- const outputPath = outputArg ?? path32.join(projectRoot, "CHANGELOG.md");
8222
+ const outputPath = outputArg ?? path36.join(projectRoot, "CHANGELOG.md");
7515
8223
  let existing = "";
7516
- if (fs31.existsSync(outputPath)) existing = fs31.readFileSync(outputPath, "utf8");
7517
- fs31.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
8224
+ if (fs35.existsSync(outputPath)) existing = fs35.readFileSync(outputPath, "utf8");
8225
+ fs35.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
7518
8226
  console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
7519
8227
  console.log(` ${commits.length} Commits verarbeitet`);
7520
8228
  } else if (command === "metrics") {
7521
8229
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
7522
8230
  const rawLog = getMetricsGitLog(projectRoot, sinceArg);
7523
8231
  const stats = parseCommitStats(rawLog);
7524
- const md = formatMetrics(stats, path32.basename(projectRoot));
7525
- const metricsPath = path32.join(forgehiveDir, "metrics.md");
7526
- fs31.mkdirSync(forgehiveDir, { recursive: true });
7527
- fs31.writeFileSync(metricsPath, md, "utf8");
8232
+ const md = formatMetrics(stats, path36.basename(projectRoot));
8233
+ const metricsPath = path36.join(forgehiveDir, "metrics.md");
8234
+ fs35.mkdirSync(forgehiveDir, { recursive: true });
8235
+ fs35.writeFileSync(metricsPath, md, "utf8");
7528
8236
  console.log(md);
7529
8237
  console.log(`
7530
8238
  \u2714 Metrics gespeichert: ${metricsPath}`);
@@ -7552,14 +8260,26 @@ Setze diese Umgebungsvariablen:`);
7552
8260
  console.error("Usage: fh run <issue-url> [--agent <name>]");
7553
8261
  process.exit(1);
7554
8262
  }
7555
- const agentArg = rest.includes("--agent") ? rest[rest.indexOf("--agent") + 1] : void 0;
7556
- const labelArg = rest.includes("--label") ? rest[rest.indexOf("--label") + 1] : void 0;
7557
- const agentId = agentArg ?? resolveAgent(labelArg);
7558
- const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
7559
- console.log(`\u2714 ${result.message}`);
7560
- console.log(` fh run status \u2014 aktive Sessions anzeigen`);
8263
+ const looksLikeUrl = issueUrl.startsWith("http://") || issueUrl.startsWith("https://") || /^[\w.-]+\/[\w.-]+#\d+$/.test(issueUrl);
8264
+ if (!looksLikeUrl) {
8265
+ const taskDescription = [issueUrl, ...rest.filter((r) => !r.startsWith("--"))].join(" ");
8266
+ const runRouteResult = routeTask(taskDescription);
8267
+ const runWorktree = resolveWorktreePath(forgehiveDir, runRouteResult.agent);
8268
+ console.log(`
8269
+ Task: "${taskDescription}"
8270
+ `);
8271
+ console.log(formatRoutingResult(runRouteResult, runWorktree));
8272
+ console.log();
8273
+ } else {
8274
+ const agentArg = rest.includes("--agent") ? rest[rest.indexOf("--agent") + 1] : void 0;
8275
+ const labelArg = rest.includes("--label") ? rest[rest.indexOf("--label") + 1] : void 0;
8276
+ const agentId = agentArg ?? resolveAgent(labelArg);
8277
+ const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
8278
+ console.log(`\u2714 ${result.message}`);
8279
+ console.log(` fh run status \u2014 aktive Sessions anzeigen`);
8280
+ }
7561
8281
  } else if (command === "story") {
7562
- const storiesDir = path32.join(forgehiveDir, "memory", "stories");
8282
+ const storiesDir = path36.join(forgehiveDir, "memory", "stories");
7563
8283
  if (subcommand === "create") {
7564
8284
  const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7565
8285
  const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
@@ -7570,7 +8290,7 @@ Setze diese Umgebungsvariablen:`);
7570
8290
  }
7571
8291
  const story = createStory(storiesDir, title, epicArg);
7572
8292
  if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
7573
- console.log(`\u2714 ${story.id} erstellt: ${path32.join(storiesDir, story.id + ".md")}`);
8293
+ console.log(`\u2714 ${story.id} erstellt: ${path36.join(storiesDir, story.id + ".md")}`);
7574
8294
  console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
7575
8295
  } else if (subcommand === "list") {
7576
8296
  const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
@@ -7619,17 +8339,32 @@ Setze diese Umgebungsvariablen:`);
7619
8339
  console.error("Verf\xFCgbar: fh story create | list | show | done");
7620
8340
  }
7621
8341
  } else if (command === "epic") {
7622
- const epicsDir = path32.join(forgehiveDir, "memory", "epics");
7623
- const storiesDir = path32.join(forgehiveDir, "memory", "stories");
8342
+ const epicsDir = path36.join(forgehiveDir, "memory", "epics");
8343
+ const storiesDir = path36.join(forgehiveDir, "memory", "stories");
7624
8344
  if (subcommand === "create") {
7625
- const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7626
8345
  const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
8346
+ const prdArg = rest.includes("--prd") ? rest[rest.indexOf("--prd") + 1] : void 0;
8347
+ const flagsWithValues = /* @__PURE__ */ new Set();
8348
+ for (const flag of ["--goal", "--prd"]) {
8349
+ const idx = rest.indexOf(flag);
8350
+ if (idx >= 0) {
8351
+ flagsWithValues.add(flag);
8352
+ if (rest[idx + 1]) flagsWithValues.add(rest[idx + 1]);
8353
+ }
8354
+ }
8355
+ const title = rest.filter((r) => !flagsWithValues.has(r) && !r.startsWith("--")).join(" ");
7627
8356
  if (!title) {
7628
- console.error("Usage: fh epic create <titel> [--goal <ziel>]");
8357
+ console.error("Usage: fh epic create <titel> [--goal <ziel>] [--prd PRD-N]");
7629
8358
  process.exit(1);
7630
8359
  }
7631
- const epic = createEpic(epicsDir, title, goalArg);
7632
- console.log(`\u2714 ${epic.id} erstellt: ${path32.join(epicsDir, epic.id + ".md")}`);
8360
+ if (prdArg) {
8361
+ const prdsDir = path36.join(forgehiveDir, "memory", "prds");
8362
+ const prd = getPrd(prdsDir, prdArg);
8363
+ if (!prd) console.warn(`\u26A0 PRD ${prdArg} nicht gefunden \u2014 Epic wird trotzdem erstellt`);
8364
+ }
8365
+ const epic = createEpic(epicsDir, title, goalArg, prdArg);
8366
+ console.log(`\u2714 ${epic.id} erstellt: ${path36.join(epicsDir, epic.id + ".md")}`);
8367
+ if (prdArg) console.log(` Verkn\xFCpft mit: ${prdArg}`);
7633
8368
  } else if (subcommand === "list") {
7634
8369
  const epics = listEpics(epicsDir);
7635
8370
  if (epics.length === 0) {
@@ -7654,8 +8389,94 @@ Setze diese Umgebungsvariablen:`);
7654
8389
  } else {
7655
8390
  console.error("Verf\xFCgbar: fh epic create | list | show");
7656
8391
  }
8392
+ } else if (command === "product") {
8393
+ const prdsDir = path36.join(forgehiveDir, "memory", "prds");
8394
+ if (subcommand === "prd") {
8395
+ const title = rest.filter((r) => !r.startsWith("--")).join(" ");
8396
+ if (!title) {
8397
+ console.error('Usage: fh product prd "<titel>"');
8398
+ process.exit(1);
8399
+ }
8400
+ const prd = createPrd(prdsDir, title);
8401
+ const relPath = path36.relative(projectRoot, prd.filepath);
8402
+ console.log(`\u2714 ${prd.id} erstellt: ${relPath}`);
8403
+ console.log(` Bearbeite die Datei um Anforderungen zu definieren.`);
8404
+ } else if (subcommand === "list") {
8405
+ const prds = listPrds(prdsDir);
8406
+ if (prds.length === 0) {
8407
+ console.log("Keine PRDs gefunden.");
8408
+ } else {
8409
+ console.log("PRDs:");
8410
+ for (const p of prds) {
8411
+ const status = `[${p.status}]`.padEnd(12);
8412
+ console.log(` ${p.id.padEnd(6)} ${status} ${p.date} ${p.title}`);
8413
+ }
8414
+ }
8415
+ } else if (subcommand === "show") {
8416
+ const id = rest[0];
8417
+ if (!id) {
8418
+ console.error("Usage: fh product show <PRD-N>");
8419
+ process.exit(1);
8420
+ }
8421
+ const prd = getPrd(prdsDir, id);
8422
+ if (!prd) {
8423
+ console.error(`${id} nicht gefunden`);
8424
+ process.exit(1);
8425
+ }
8426
+ console.log(fs35.readFileSync(prd.filepath, "utf8"));
8427
+ } else if (subcommand === "status") {
8428
+ const epicsDir = path36.join(forgehiveDir, "memory", "epics");
8429
+ const storiesDir = path36.join(forgehiveDir, "memory", "stories");
8430
+ const prds = listPrds(prdsDir);
8431
+ const epics = listEpics(epicsDir);
8432
+ const stories = listStories(storiesDir);
8433
+ console.log("Pipeline:\n");
8434
+ for (const prd of prds) {
8435
+ console.log(` ${prd.id}: ${prd.title} [${prd.status}]`);
8436
+ const linkedEpics = epics.filter((e) => e.prdId === prd.id);
8437
+ if (linkedEpics.length === 0) {
8438
+ console.log(` \u2514\u2500\u2500 Keine Epics verkn\xFCpft`);
8439
+ } else {
8440
+ for (let ei = 0; ei < linkedEpics.length; ei++) {
8441
+ const epic = linkedEpics[ei];
8442
+ const epicPrefix = ei < linkedEpics.length - 1 ? "\u251C\u2500\u2500" : "\u2514\u2500\u2500";
8443
+ console.log(` ${epicPrefix} ${epic.id}: ${epic.title} [${epic.status}]`);
8444
+ const epicStories = stories.filter((s) => s.epicId === epic.id);
8445
+ const isLastEpic = ei === linkedEpics.length - 1;
8446
+ if (epicStories.length === 0) {
8447
+ const pipeOrSpaceEmpty = isLastEpic ? " " : "\u2502 ";
8448
+ console.log(` ${pipeOrSpaceEmpty}\u2514\u2500\u2500 Keine Stories`);
8449
+ } else {
8450
+ for (let si = 0; si < epicStories.length; si++) {
8451
+ const story = epicStories[si];
8452
+ const storyPrefix = si < epicStories.length - 1 ? "\u251C\u2500\u2500" : "\u2514\u2500\u2500";
8453
+ const doneMark = story.status === "done" ? " \u2713" : "";
8454
+ const pipeOrSpace = isLastEpic ? " " : "\u2502 ";
8455
+ console.log(` ${pipeOrSpace}${storyPrefix} ${story.id}: ${story.title} [${story.status}]${doneMark}`);
8456
+ }
8457
+ }
8458
+ }
8459
+ }
8460
+ console.log();
8461
+ }
8462
+ const orphanEpics = epics.filter((e) => !e.prdId);
8463
+ if (orphanEpics.length > 0) {
8464
+ console.log(" Epics ohne PRD:");
8465
+ for (const epic of orphanEpics) {
8466
+ const epicStories = stories.filter((s) => s.epicId === epic.id);
8467
+ console.log(` ${epic.id}: ${epic.title} [${epic.status}] (${epicStories.length} Stories)`);
8468
+ }
8469
+ }
8470
+ } else if (subcommand === "roadmap") {
8471
+ const content = generateRoadmap(forgehiveDir);
8472
+ const roadmapPath = path36.join(projectRoot, "ROADMAP.md");
8473
+ fs35.writeFileSync(roadmapPath, content, "utf8");
8474
+ console.log(`\u2714 ROADMAP.md generiert: ${roadmapPath}`);
8475
+ } else {
8476
+ console.error("Verf\xFCgbar: fh product prd | list | show | status | roadmap");
8477
+ }
7657
8478
  } else if (command === "velocity") {
7658
- const velocityFile = path32.join(forgehiveDir, "memory", "velocity.md");
8479
+ const velocityFile = path36.join(forgehiveDir, "memory", "velocity.md");
7659
8480
  if (subcommand === "record") {
7660
8481
  const sprintNum = parseInt(rest[0] ?? "0", 10);
7661
8482
  const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
@@ -7674,7 +8495,7 @@ Setze diese Umgebungsvariablen:`);
7674
8495
  console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
7675
8496
  }
7676
8497
  } else if (command === "docs") {
7677
- const docsDir = path32.join(projectRoot, "docs");
8498
+ const docsDir = path36.join(projectRoot, "docs");
7678
8499
  if (!subcommand || subcommand === "list") {
7679
8500
  const existing = listExistingDocs(projectRoot);
7680
8501
  if (existing.length === 0) {
@@ -7688,29 +8509,29 @@ Setze diese Umgebungsvariablen:`);
7688
8509
  console.log(" fh docs adr <titel> \u2014 Architecture Decision Record");
7689
8510
  } else {
7690
8511
  console.log(`Vorhandene Dokumentation (${existing.length} Dateien):`);
7691
- for (const d of existing) console.log(` ${path32.relative(projectRoot, d)}`);
8512
+ for (const d of existing) console.log(` ${path36.relative(projectRoot, d)}`);
7692
8513
  console.log("");
7693
8514
  console.log("Aktualisieren: fh docs user | api | onboard | changelog");
7694
8515
  }
7695
8516
  } else if (subcommand === "user") {
7696
- fs31.mkdirSync(docsDir, { recursive: true });
8517
+ fs35.mkdirSync(docsDir, { recursive: true });
7697
8518
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7698
- const outputPath = outputArg ?? path32.join(docsDir, "user-guide.md");
8519
+ const outputPath = outputArg ?? path36.join(docsDir, "user-guide.md");
7699
8520
  const guide = generateUserGuide(projectRoot, forgehiveDir);
7700
- fs31.writeFileSync(outputPath, guide, "utf8");
8521
+ fs35.writeFileSync(outputPath, guide, "utf8");
7701
8522
  console.log(`\u2714 User Guide geschrieben: ${outputPath}`);
7702
8523
  } else if (subcommand === "api") {
7703
- fs31.mkdirSync(docsDir, { recursive: true });
8524
+ fs35.mkdirSync(docsDir, { recursive: true });
7704
8525
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7705
- const outputPath = outputArg ?? path32.join(docsDir, "api.md");
8526
+ const outputPath = outputArg ?? path36.join(docsDir, "api.md");
7706
8527
  const ref = generateApiReference(projectRoot);
7707
- fs31.writeFileSync(outputPath, ref, "utf8");
8528
+ fs35.writeFileSync(outputPath, ref, "utf8");
7708
8529
  console.log(`\u2714 API-Referenz geschrieben: ${outputPath}`);
7709
8530
  } else if (subcommand === "onboard") {
7710
8531
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7711
- const outputPath = outputArg ?? path32.join(projectRoot, "ONBOARDING.md");
8532
+ const outputPath = outputArg ?? path36.join(projectRoot, "ONBOARDING.md");
7712
8533
  const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
7713
- fs31.writeFileSync(outputPath, doc, "utf8");
8534
+ fs35.writeFileSync(outputPath, doc, "utf8");
7714
8535
  console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
7715
8536
  } else if (subcommand === "changelog") {
7716
8537
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
@@ -7720,15 +8541,15 @@ Setze diese Umgebungsvariablen:`);
7720
8541
  const commits = parseGitLog(rawLog);
7721
8542
  let pkg = {};
7722
8543
  try {
7723
- pkg = JSON.parse(fs31.readFileSync(path32.join(projectRoot, "package.json"), "utf8"));
8544
+ pkg = JSON.parse(fs35.readFileSync(path36.join(projectRoot, "package.json"), "utf8"));
7724
8545
  } catch {
7725
8546
  }
7726
8547
  const pkgVersion = pkg.version ?? "unreleased";
7727
8548
  const md = formatChangelog(commits, pkgVersion);
7728
- const outputPath = outputArg ?? path32.join(projectRoot, "CHANGELOG.md");
8549
+ const outputPath = outputArg ?? path36.join(projectRoot, "CHANGELOG.md");
7729
8550
  let existing = "";
7730
- if (fs31.existsSync(outputPath)) existing = fs31.readFileSync(outputPath, "utf8");
7731
- fs31.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
8551
+ if (fs35.existsSync(outputPath)) existing = fs35.readFileSync(outputPath, "utf8");
8552
+ fs35.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
7732
8553
  console.log(`\u2714 CHANGELOG.md aktualisiert (${commits.length} Commits)`);
7733
8554
  } else if (subcommand === "adr") {
7734
8555
  const title = rest.join(" ");
@@ -7736,9 +8557,9 @@ Setze diese Umgebungsvariablen:`);
7736
8557
  console.error("Usage: fh docs adr <titel>");
7737
8558
  process.exit(1);
7738
8559
  }
7739
- const adrsDir = path32.join(forgehiveDir, "memory", "adrs");
7740
- fs31.mkdirSync(adrsDir, { recursive: true });
7741
- const existing = fs31.readdirSync(adrsDir).filter((f) => f.endsWith(".md")).length;
8560
+ const adrsDir = path36.join(forgehiveDir, "memory", "adrs");
8561
+ fs35.mkdirSync(adrsDir, { recursive: true });
8562
+ const existing = fs35.readdirSync(adrsDir).filter((f) => f.endsWith(".md")).length;
7742
8563
  const adrId = String(existing + 1).padStart(4, "0");
7743
8564
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7744
8565
  const filename = `${adrId}-${slug}.md`;
@@ -7759,14 +8580,125 @@ Setze diese Umgebungsvariablen:`);
7759
8580
 
7760
8581
  (Beschreibe die Auswirkungen dieser Entscheidung.)
7761
8582
  `;
7762
- fs31.writeFileSync(path32.join(adrsDir, filename), content, "utf8");
8583
+ fs35.writeFileSync(path36.join(adrsDir, filename), content, "utf8");
7763
8584
  console.log(`\u2714 ADR erstellt: .forgehive/memory/adrs/${filename}`);
7764
8585
  } else {
7765
8586
  console.error("Verf\xFCgbar: fh docs [list|user|api|onboard|changelog|adr <titel>]");
7766
8587
  }
8588
+ } else if (command === "github") {
8589
+ const storiesDir = path36.join(forgehiveDir, "memory", "stories");
8590
+ if (subcommand === "setup") {
8591
+ const readline = await import("node:readline/promises");
8592
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8593
+ let token;
8594
+ let repo;
8595
+ try {
8596
+ token = (await rl.question(" GitHub-Token (ghp_...): ")).trim();
8597
+ repo = (await rl.question(" Repository (owner/repo): ")).trim();
8598
+ } catch (e) {
8599
+ console.error(` \u2717 Eingabe fehlgeschlagen: ${e.message}`);
8600
+ process.exit(1);
8601
+ } finally {
8602
+ rl.close();
8603
+ }
8604
+ if (!token) {
8605
+ console.error(" \u2717 Kein Token eingegeben.");
8606
+ process.exit(1);
8607
+ }
8608
+ if (!token.startsWith("ghp_") && !token.startsWith("github_pat_") && !token.startsWith("gho_")) {
8609
+ console.warn(" \u26A0 Token-Format unbekannt \u2014 erwartet ghp_*, github_pat_*, oder gho_*");
8610
+ }
8611
+ const repoParts = repo.split("/");
8612
+ if (repoParts.length !== 2 || repoParts[0].length === 0 || repoParts[1].length === 0) {
8613
+ console.error(` \u2717 Ung\xFCltiges Repo-Format: "${repo}" \u2014 erwartet: owner/repo`);
8614
+ process.exit(1);
8615
+ }
8616
+ setCredentials("github", { GITHUB_TOKEN: token });
8617
+ saveGitHubConfig(forgehiveDir, { repo });
8618
+ console.log(" \u2713 Token gespeichert");
8619
+ console.log(" \u2713 Repo gespeichert in .forgehive/github.yaml");
8620
+ } else if (subcommand === "sync") {
8621
+ let config;
8622
+ try {
8623
+ config = loadGitHubConfig(forgehiveDir);
8624
+ } catch (e) {
8625
+ console.error(` \u2717 ${e.message}`);
8626
+ process.exit(1);
8627
+ }
8628
+ const creds = getCredentials("github");
8629
+ if (!creds?.GITHUB_TOKEN) {
8630
+ console.error(" \u2717 Kein GitHub-Token gefunden. F\xFChre zuerst aus: fh github setup");
8631
+ process.exit(1);
8632
+ }
8633
+ const [owner, repo] = config.repo.split("/");
8634
+ console.log(` Lade Issues von ${config.repo}...`);
8635
+ let issues;
8636
+ try {
8637
+ issues = await fetchOpenIssues(owner, repo, creds.GITHUB_TOKEN);
8638
+ } catch (e) {
8639
+ console.error(` \u2717 ${e.message}`);
8640
+ process.exit(1);
8641
+ }
8642
+ let created = 0;
8643
+ let skipped = 0;
8644
+ for (const issue of issues) {
8645
+ if (issueAlreadySynced(storiesDir, issue.number)) {
8646
+ console.log(` \u2013 #${issue.number} "${issue.title}" \u2192 bereits vorhanden (\xFCbersprungen)`);
8647
+ skipped++;
8648
+ continue;
8649
+ }
8650
+ const story = createStory(storiesDir, issue.title);
8651
+ appendGitHubRef(storiesDir, story.id, issue, config.repo);
8652
+ console.log(` \u2713 #${issue.number} "${issue.title}" \u2192 ${story.id} erstellt`);
8653
+ created++;
8654
+ }
8655
+ console.log(`
8656
+ ${created} ${created === 1 ? "Story" : "Stories"} erstellt, ${skipped} \xFCbersprungen.`);
8657
+ } else if (subcommand === "pr") {
8658
+ const prNumberRaw = rest[0];
8659
+ if (!prNumberRaw || isNaN(parseInt(prNumberRaw, 10))) {
8660
+ console.error(" Usage: fh github pr <number>");
8661
+ process.exit(1);
8662
+ }
8663
+ const prNumber = parseInt(prNumberRaw, 10);
8664
+ let config;
8665
+ try {
8666
+ config = loadGitHubConfig(forgehiveDir);
8667
+ } catch (e) {
8668
+ console.error(` \u2717 ${e.message}`);
8669
+ process.exit(1);
8670
+ }
8671
+ const creds = getCredentials("github");
8672
+ if (!creds?.GITHUB_TOKEN) {
8673
+ console.error(" \u2717 Kein GitHub-Token gefunden. F\xFChre zuerst aus: fh github setup");
8674
+ process.exit(1);
8675
+ }
8676
+ const [owner, repo] = config.repo.split("/");
8677
+ console.log(` Lade PR #${prNumber} von ${config.repo}...`);
8678
+ let pr, files;
8679
+ try {
8680
+ [pr, files] = await Promise.all([
8681
+ fetchPR(owner, repo, prNumber, creds.GITHUB_TOKEN),
8682
+ fetchPRFiles(owner, repo, prNumber, creds.GITHUB_TOKEN)
8683
+ ]);
8684
+ } catch (e) {
8685
+ console.error(` \u2717 ${e.message}`);
8686
+ process.exit(1);
8687
+ }
8688
+ const contextMd = formatPRContext(pr, files, config.repo);
8689
+ fs35.mkdirSync(forgehiveDir, { recursive: true });
8690
+ const outputPath = path36.join(forgehiveDir, `github-pr-${prNumber}.md`);
8691
+ fs35.writeFileSync(outputPath, contextMd, "utf8");
8692
+ console.log(` \u2713 PR-Kontext gespeichert: .forgehive/github-pr-${prNumber}.md`);
8693
+ console.log("");
8694
+ console.log(` PR context gespeichert. Starte Review: fh party run review`);
8695
+ } else {
8696
+ console.log("Verf\xFCgbar: fh github setup | fh github sync | fh github pr <number>");
8697
+ console.log("Hilfe: fh --help");
8698
+ }
7767
8699
  } else {
7768
8700
  console.error("Unbekannter Befehl: " + command);
7769
- console.error("Verf\xFCgbar: init | confirm | rollback | scan | status | ci | map | onboard | changelog | metrics | docs | story [create|list|show|sprint|done] | epic [create|list|show] | velocity [show|record] | sync [push|pull] | run <issue-url> | cost | memory | skills | party | wire | mcp | security");
8701
+ console.error("Verf\xFCgbar: init | confirm | rollback | scan | status | ci | map | onboard | changelog | metrics | docs | story [create|list|show|sprint|done] | epic [create|list|show] | velocity [show|record] | sync [push|pull] | run <issue-url> | github [setup|sync|pr] | cost | memory | skills | party | wire | mcp | security");
7770
8702
  console.error("Hilfe: fh --help");
7771
8703
  process.exit(1);
7772
8704
  }