forgehive 0.7.8 → 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 +142 -13
  2. package/dist/cli.js +1218 -226
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -2751,8 +2751,10 @@ 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
+ import { spawnSync as spawnSync12 } from "node:child_process";
2757
+ import { createInterface } from "node:readline";
2756
2758
 
2757
2759
  // src/scanner.ts
2758
2760
  import fs from "node:fs";
@@ -5734,7 +5736,44 @@ function generateMap(projectRoot2) {
5734
5736
  totalLines: files.reduce((sum, f) => sum + f.lines, 0)
5735
5737
  };
5736
5738
  }
5737
- 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) {
5738
5777
  const lines = [];
5739
5778
  lines.push("# Codebase Map");
5740
5779
  lines.push(`Generated: ${map2.generatedAt}`);
@@ -5764,13 +5803,199 @@ function formatMap(map2) {
5764
5803
  } else {
5765
5804
  lines.push("No internal imports detected.");
5766
5805
  }
5806
+ if (semantic) {
5807
+ lines.push("");
5808
+ lines.push(formatSemanticBlock(semantic, map2.projectRoot));
5809
+ }
5767
5810
  return lines.join("\n");
5768
5811
  }
5769
5812
 
5770
- // src/onboard.ts
5771
- init_js_yaml();
5813
+ // src/semantic-map.ts
5772
5814
  import fs24 from "node:fs";
5773
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";
5774
5999
  import { spawnSync as spawnSync7 } from "node:child_process";
5775
6000
  function getRecentCommits(projectRoot2, n = 20) {
5776
6001
  const result = spawnSync7("git", ["log", `--oneline`, `-${n}`], {
@@ -5781,30 +6006,30 @@ function getRecentCommits(projectRoot2, n = 20) {
5781
6006
  return result.stdout.trim().split("\n").filter(Boolean);
5782
6007
  }
5783
6008
  function readCapabilities(forgehiveDir2) {
5784
- const capPath = path25.join(forgehiveDir2, "capabilities.yaml");
5785
- if (!fs24.existsSync(capPath)) return {};
6009
+ const capPath = path26.join(forgehiveDir2, "capabilities.yaml");
6010
+ if (!fs25.existsSync(capPath)) return {};
5786
6011
  try {
5787
- return jsYaml.load(fs24.readFileSync(capPath, "utf8")) ?? {};
6012
+ return jsYaml.load(fs25.readFileSync(capPath, "utf8")) ?? {};
5788
6013
  } catch {
5789
6014
  return {};
5790
6015
  }
5791
6016
  }
5792
6017
  function readMemoryFiles(forgehiveDir2) {
5793
- const memDir = path25.join(forgehiveDir2, "memory");
5794
- if (!fs24.existsSync(memDir)) return {};
6018
+ const memDir = path26.join(forgehiveDir2, "memory");
6019
+ if (!fs25.existsSync(memDir)) return {};
5795
6020
  const result = {};
5796
- for (const f of fs24.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
5797
- 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");
5798
6023
  }
5799
6024
  return result;
5800
6025
  }
5801
6026
  function listAdrs2(forgehiveDir2) {
5802
- const adrsDir = path25.join(forgehiveDir2, "memory", "adrs");
5803
- if (!fs24.existsSync(adrsDir)) return [];
5804
- 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"));
5805
6030
  }
5806
6031
  function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
5807
- const projectName = path25.basename(projectRoot2);
6032
+ const projectName = path26.basename(projectRoot2);
5808
6033
  const caps = readCapabilities(forgehiveDir2);
5809
6034
  const memFiles = readMemoryFiles(forgehiveDir2);
5810
6035
  const commits = getRecentCommits(projectRoot2);
@@ -6019,13 +6244,13 @@ function getMetricsGitLog(projectRoot2, since) {
6019
6244
  }
6020
6245
 
6021
6246
  // src/sync.ts
6022
- import fs25 from "node:fs";
6023
- import path26 from "node:path";
6247
+ import fs26 from "node:fs";
6248
+ import path27 from "node:path";
6024
6249
  import { spawnSync as spawnSync10 } from "node:child_process";
6025
6250
  function getSyncStatus(forgehiveDir2) {
6026
- const memDir = path26.join(forgehiveDir2, "memory");
6027
- const files = fs25.existsSync(memDir) ? fs25.readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
6028
- 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);
6029
6254
  const configResult = spawnSync10(
6030
6255
  "git",
6031
6256
  ["config", "--get", "forgehive.sync-remote"],
@@ -6041,18 +6266,18 @@ function getSyncStatus(forgehiveDir2) {
6041
6266
  return { files, hasRemote: remote !== null, remote, branch };
6042
6267
  }
6043
6268
  function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
6044
- const projectRoot2 = path26.dirname(forgehiveDir2);
6045
- const memDir = path26.join(forgehiveDir2, "memory");
6046
- if (!fs25.existsSync(memDir)) {
6269
+ const projectRoot2 = path27.dirname(forgehiveDir2);
6270
+ const memDir = path27.join(forgehiveDir2, "memory");
6271
+ if (!fs26.existsSync(memDir)) {
6047
6272
  return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
6048
6273
  }
6049
- const files = fs25.readdirSync(memDir).filter((f) => f.endsWith(".md"));
6274
+ const files = fs26.readdirSync(memDir).filter((f) => f.endsWith(".md"));
6050
6275
  if (files.length === 0) {
6051
6276
  return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
6052
6277
  }
6053
6278
  const addResult = spawnSync10(
6054
6279
  "git",
6055
- ["add", path26.join(".forgehive", "memory")],
6280
+ ["add", path27.join(".forgehive", "memory")],
6056
6281
  { cwd: projectRoot2, encoding: "utf8" }
6057
6282
  );
6058
6283
  if (addResult.status !== 0) {
@@ -6080,9 +6305,9 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6080
6305
  return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
6081
6306
  }
6082
6307
  function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
6083
- const projectRoot2 = path26.dirname(forgehiveDir2);
6084
- const memDir = path26.join(forgehiveDir2, "memory");
6085
- fs25.mkdirSync(memDir, { recursive: true });
6308
+ const projectRoot2 = path27.dirname(forgehiveDir2);
6309
+ const memDir = path27.join(forgehiveDir2, "memory");
6310
+ fs26.mkdirSync(memDir, { recursive: true });
6086
6311
  const fetchResult = spawnSync10(
6087
6312
  "git",
6088
6313
  ["fetch", remote, branch],
@@ -6102,16 +6327,16 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6102
6327
  const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
6103
6328
  const imported = [];
6104
6329
  for (const remotePath of remoteFiles) {
6105
- const filename = path26.basename(remotePath);
6106
- const localPath = path26.join(memDir, filename);
6107
- if (!fs25.existsSync(localPath)) {
6330
+ const filename = path27.basename(remotePath);
6331
+ const localPath = path27.join(memDir, filename);
6332
+ if (!fs26.existsSync(localPath)) {
6108
6333
  const contentResult = spawnSync10(
6109
6334
  "git",
6110
6335
  ["show", `${remote}/${branch}:${remotePath}`],
6111
6336
  { cwd: projectRoot2, encoding: "utf8" }
6112
6337
  );
6113
6338
  if (contentResult.status === 0) {
6114
- fs25.writeFileSync(localPath, contentResult.stdout, "utf8");
6339
+ fs26.writeFileSync(localPath, contentResult.stdout, "utf8");
6115
6340
  imported.push(filename);
6116
6341
  }
6117
6342
  }
@@ -6124,8 +6349,8 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6124
6349
  }
6125
6350
 
6126
6351
  // src/background.ts
6127
- import fs26 from "node:fs";
6128
- import path27 from "node:path";
6352
+ import fs27 from "node:fs";
6353
+ import path28 from "node:path";
6129
6354
  import { spawn } from "node:child_process";
6130
6355
  var AGENT_ROLES = {
6131
6356
  kai: "Senior Engineer \u2014 implements features and fixes bugs",
@@ -6172,23 +6397,23 @@ Instructions:
6172
6397
  Work autonomously. Do not ask for clarification \u2014 use your best judgment based on the issue description and codebase context.`;
6173
6398
  }
6174
6399
  function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
6175
- const logsDir = path27.join(forgehiveDir2, "background-runs");
6176
- fs26.mkdirSync(logsDir, { recursive: true });
6400
+ const logsDir = path28.join(forgehiveDir2, "background-runs");
6401
+ fs27.mkdirSync(logsDir, { recursive: true });
6177
6402
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6178
- const logFile = path27.join(logsDir, `${agentId}-${timestamp2}.log`);
6403
+ const logFile = path28.join(logsDir, `${agentId}-${timestamp2}.log`);
6179
6404
  const prompt = buildAgentPrompt(issueUrl, agentId);
6180
- const logStream = fs26.openSync(logFile, "w");
6405
+ const logStream = fs27.openSync(logFile, "w");
6181
6406
  const child = spawn(
6182
6407
  "claude",
6183
6408
  ["-p", prompt, "--output-format", "text"],
6184
6409
  {
6185
- cwd: path27.dirname(forgehiveDir2),
6410
+ cwd: path28.dirname(forgehiveDir2),
6186
6411
  detached: true,
6187
6412
  stdio: ["ignore", logStream, logStream]
6188
6413
  }
6189
6414
  );
6190
6415
  child.unref();
6191
- fs26.closeSync(logStream);
6416
+ fs27.closeSync(logStream);
6192
6417
  return {
6193
6418
  pid: child.pid,
6194
6419
  logFile,
@@ -6198,11 +6423,11 @@ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
6198
6423
 
6199
6424
  // src/stories.ts
6200
6425
  init_js_yaml();
6201
- import fs27 from "node:fs";
6202
- import path28 from "node:path";
6426
+ import fs28 from "node:fs";
6427
+ import path29 from "node:path";
6203
6428
  function nextStoryId(storiesDir) {
6204
- if (!fs27.existsSync(storiesDir)) return "US-1";
6205
- const existing = fs27.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseInt(f.replace("US-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
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));
6206
6431
  const max = existing.length > 0 ? Math.max(...existing) : 0;
6207
6432
  return `US-${max + 1}`;
6208
6433
  }
@@ -6234,7 +6459,7 @@ ${acLines || "- [ ] (noch nicht definiert)"}
6234
6459
  }
6235
6460
  function parseStoryFile(filePath) {
6236
6461
  try {
6237
- const content = fs27.readFileSync(filePath, "utf8");
6462
+ const content = fs28.readFileSync(filePath, "utf8");
6238
6463
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6239
6464
  if (!match) return null;
6240
6465
  const data = jsYaml.load(match[1]);
@@ -6254,7 +6479,7 @@ function parseStoryFile(filePath) {
6254
6479
  }
6255
6480
  }
6256
6481
  function createStory(storiesDir, title, epicId) {
6257
- fs27.mkdirSync(storiesDir, { recursive: true });
6482
+ fs28.mkdirSync(storiesDir, { recursive: true });
6258
6483
  const id = nextStoryId(storiesDir);
6259
6484
  const story = {
6260
6485
  id,
@@ -6267,33 +6492,33 @@ function createStory(storiesDir, title, epicId) {
6267
6492
  epicId: epicId ?? null,
6268
6493
  status: "backlog"
6269
6494
  };
6270
- fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6495
+ fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6271
6496
  return story;
6272
6497
  }
6273
6498
  function listStories(storiesDir) {
6274
- if (!fs27.existsSync(storiesDir)) return [];
6275
- return fs27.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseStoryFile(path28.join(storiesDir, f))).filter((s) => s !== null).sort((a, b) => {
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) => {
6276
6501
  const na = parseInt(a.id.replace("US-", ""), 10);
6277
6502
  const nb = parseInt(b.id.replace("US-", ""), 10);
6278
6503
  return na - nb;
6279
6504
  });
6280
6505
  }
6281
6506
  function getStory(storiesDir, id) {
6282
- const filePath = path28.join(storiesDir, `${id}.md`);
6283
- if (!fs27.existsSync(filePath)) return null;
6507
+ const filePath = path29.join(storiesDir, `${id}.md`);
6508
+ if (!fs28.existsSync(filePath)) return null;
6284
6509
  return parseStoryFile(filePath);
6285
6510
  }
6286
6511
  function updateStoryPoints(storiesDir, id, points) {
6287
6512
  const story = getStory(storiesDir, id);
6288
6513
  if (!story) throw new Error(`Story ${id} nicht gefunden`);
6289
6514
  story.points = points;
6290
- fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6515
+ fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6291
6516
  }
6292
6517
  function updateStoryStatus(storiesDir, id, status) {
6293
6518
  const story = getStory(storiesDir, id);
6294
6519
  if (!story) throw new Error(`Story ${id} nicht gefunden`);
6295
6520
  story.status = status;
6296
- fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6521
+ fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6297
6522
  }
6298
6523
  function formatStoryCard(story) {
6299
6524
  const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
@@ -6312,11 +6537,11 @@ function formatStoryCard(story) {
6312
6537
 
6313
6538
  // src/epics.ts
6314
6539
  init_js_yaml();
6315
- import fs28 from "node:fs";
6316
- import path29 from "node:path";
6540
+ import fs29 from "node:fs";
6541
+ import path30 from "node:path";
6317
6542
  function nextEpicId(epicsDir) {
6318
- if (!fs28.existsSync(epicsDir)) return "EPC-1";
6319
- const existing = fs28.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseInt(f.replace("EPC-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
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));
6320
6545
  const max = existing.length > 0 ? Math.max(...existing) : 0;
6321
6546
  return `EPC-${max + 1}`;
6322
6547
  }
@@ -6326,7 +6551,8 @@ function epicToMarkdown(epic) {
6326
6551
  title: epic.title,
6327
6552
  goal: epic.goal,
6328
6553
  stories: epic.stories,
6329
- status: epic.status
6554
+ status: epic.status,
6555
+ prdId: epic.prdId
6330
6556
  });
6331
6557
  const storyLines = epic.stories.map((s) => `- ${s}`).join("\n");
6332
6558
  return `---
@@ -6343,7 +6569,7 @@ ${storyLines || "(noch keine Stories)"}
6343
6569
  }
6344
6570
  function parseEpicFile(filePath) {
6345
6571
  try {
6346
- const content = fs28.readFileSync(filePath, "utf8");
6572
+ const content = fs29.readFileSync(filePath, "utf8");
6347
6573
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6348
6574
  if (!match) return null;
6349
6575
  const data = jsYaml.load(match[1]);
@@ -6352,30 +6578,31 @@ function parseEpicFile(filePath) {
6352
6578
  title: data.title ?? "",
6353
6579
  goal: data.goal ?? "",
6354
6580
  stories: data.stories ?? [],
6355
- status: data.status ?? "active"
6581
+ status: data.status ?? "active",
6582
+ prdId: data.prdId ?? null
6356
6583
  };
6357
6584
  } catch {
6358
6585
  return null;
6359
6586
  }
6360
6587
  }
6361
- function createEpic(epicsDir, title, goal) {
6362
- fs28.mkdirSync(epicsDir, { recursive: true });
6588
+ function createEpic(epicsDir, title, goal, prdId) {
6589
+ fs29.mkdirSync(epicsDir, { recursive: true });
6363
6590
  const id = nextEpicId(epicsDir);
6364
- const epic = { id, title, goal: goal ?? "", stories: [], status: "active" };
6365
- fs28.writeFileSync(path29.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
6591
+ const epic = { id, title, goal: goal ?? "", stories: [], status: "active", prdId: prdId ?? null };
6592
+ fs29.writeFileSync(path30.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
6366
6593
  return epic;
6367
6594
  }
6368
6595
  function listEpics(epicsDir) {
6369
- if (!fs28.existsSync(epicsDir)) return [];
6370
- return fs28.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseEpicFile(path29.join(epicsDir, f))).filter((e) => e !== null).sort((a, b) => {
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) => {
6371
6598
  const na = parseInt(a.id.replace("EPC-", ""), 10);
6372
6599
  const nb = parseInt(b.id.replace("EPC-", ""), 10);
6373
6600
  return na - nb;
6374
6601
  });
6375
6602
  }
6376
6603
  function getEpic(epicsDir, id) {
6377
- const filePath = path29.join(epicsDir, `${id}.md`);
6378
- if (!fs28.existsSync(filePath)) return null;
6604
+ const filePath = path30.join(epicsDir, `${id}.md`);
6605
+ if (!fs29.existsSync(filePath)) return null;
6379
6606
  return parseEpicFile(filePath);
6380
6607
  }
6381
6608
  function formatEpicCard(epic, stories) {
@@ -6397,25 +6624,188 @@ function formatEpicCard(epic, stories) {
6397
6624
  return lines.join("\n");
6398
6625
  }
6399
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
+
6400
6790
  // src/velocity.ts
6401
- import fs29 from "node:fs";
6402
- import path30 from "node:path";
6791
+ import fs31 from "node:fs";
6792
+ import path32 from "node:path";
6403
6793
  var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
6404
6794
  function recordVelocity(velocityFile, sprint, committed, delivered) {
6405
6795
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6406
6796
  const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
6407
6797
  const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
6408
6798
  `;
6409
- if (!fs29.existsSync(velocityFile)) {
6410
- fs29.mkdirSync(path30.dirname(velocityFile), { recursive: true });
6411
- 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");
6412
6802
  } else {
6413
- fs29.appendFileSync(velocityFile, row, "utf8");
6803
+ fs31.appendFileSync(velocityFile, row, "utf8");
6414
6804
  }
6415
6805
  }
6416
6806
  function getVelocityHistory(velocityFile) {
6417
- if (!fs29.existsSync(velocityFile)) return [];
6418
- const content = fs29.readFileSync(velocityFile, "utf8");
6807
+ if (!fs31.existsSync(velocityFile)) return [];
6808
+ const content = fs31.readFileSync(velocityFile, "utf8");
6419
6809
  const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
6420
6810
  return rows.map((row) => {
6421
6811
  const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
@@ -6456,22 +6846,22 @@ function formatVelocityReport(history) {
6456
6846
 
6457
6847
  // src/docs.ts
6458
6848
  init_js_yaml();
6459
- import fs30 from "node:fs";
6460
- import path31 from "node:path";
6849
+ import fs32 from "node:fs";
6850
+ import path33 from "node:path";
6461
6851
  import { spawnSync as spawnSync11 } from "node:child_process";
6462
6852
  var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
6463
- var IGNORE_DIRS3 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
6464
- var EXPORT_PATTERNS = [
6853
+ var IGNORE_DIRS4 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
6854
+ var EXPORT_PATTERNS2 = [
6465
6855
  /^export\s+(?:async\s+)?function\s+(\w+)/gm,
6466
6856
  /^export\s+(?:const|let|var)\s+(\w+)/gm,
6467
6857
  /^export\s+(?:class|interface|type|enum)\s+(\w+)/gm,
6468
6858
  /^export\s+default\s+(?:function\s+)?(\w+)?/gm
6469
6859
  ];
6470
6860
  function readCapabilities2(forgehiveDir2) {
6471
- const capPath = path31.join(forgehiveDir2, "capabilities.yaml");
6472
- if (!fs30.existsSync(capPath)) return {};
6861
+ const capPath = path33.join(forgehiveDir2, "capabilities.yaml");
6862
+ if (!fs32.existsSync(capPath)) return {};
6473
6863
  try {
6474
- return jsYaml.load(fs30.readFileSync(capPath, "utf8")) ?? {};
6864
+ return jsYaml.load(fs32.readFileSync(capPath, "utf8")) ?? {};
6475
6865
  } catch {
6476
6866
  return {};
6477
6867
  }
@@ -6498,11 +6888,11 @@ function extractCapabilityInfo(caps) {
6498
6888
  return { language, packageManager };
6499
6889
  }
6500
6890
  function readMemoryFiles2(forgehiveDir2) {
6501
- const memDir = path31.join(forgehiveDir2, "memory");
6502
- if (!fs30.existsSync(memDir)) return {};
6891
+ const memDir = path33.join(forgehiveDir2, "memory");
6892
+ if (!fs32.existsSync(memDir)) return {};
6503
6893
  const result = {};
6504
- for (const f of fs30.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
6505
- 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");
6506
6896
  }
6507
6897
  return result;
6508
6898
  }
@@ -6513,7 +6903,7 @@ function getRecentCommits2(projectRoot2, n = 10) {
6513
6903
  }
6514
6904
  function extractExports(content) {
6515
6905
  const exports = [];
6516
- for (const pattern of EXPORT_PATTERNS) {
6906
+ for (const pattern of EXPORT_PATTERNS2) {
6517
6907
  pattern.lastIndex = 0;
6518
6908
  let m;
6519
6909
  while ((m = pattern.exec(content)) !== null) {
@@ -6525,19 +6915,19 @@ function extractExports(content) {
6525
6915
  function walkSourceFiles(dir) {
6526
6916
  const results = [];
6527
6917
  function walk(current) {
6528
- if (!fs30.existsSync(current)) return;
6529
- for (const entry of fs30.readdirSync(current, { withFileTypes: true })) {
6530
- if (IGNORE_DIRS3.includes(entry.name)) continue;
6531
- 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);
6532
6922
  if (entry.isDirectory()) walk(full);
6533
- 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);
6534
6924
  }
6535
6925
  }
6536
6926
  walk(dir);
6537
6927
  return results;
6538
6928
  }
6539
6929
  function generateUserGuide(projectRoot2, forgehiveDir2) {
6540
- const projectName = path31.basename(projectRoot2);
6930
+ const projectName = path33.basename(projectRoot2);
6541
6931
  const caps = readCapabilities2(forgehiveDir2);
6542
6932
  const { language: lang, packageManager, entryPoints } = extractCapabilityInfo(caps);
6543
6933
  const memFiles = readMemoryFiles2(forgehiveDir2);
@@ -6616,8 +7006,8 @@ function generateUserGuide(projectRoot2, forgehiveDir2) {
6616
7006
  return lines.join("\n");
6617
7007
  }
6618
7008
  function generateApiReference(projectRoot2) {
6619
- const srcDir = path31.join(projectRoot2, "src");
6620
- const searchDir = fs30.existsSync(srcDir) ? srcDir : projectRoot2;
7009
+ const srcDir = path33.join(projectRoot2, "src");
7010
+ const searchDir = fs32.existsSync(srcDir) ? srcDir : projectRoot2;
6621
7011
  const files = walkSourceFiles(searchDir);
6622
7012
  const lines = [];
6623
7013
  lines.push("# API Reference");
@@ -6631,13 +7021,13 @@ function generateApiReference(projectRoot2) {
6631
7021
  for (const filePath of files) {
6632
7022
  let content = "";
6633
7023
  try {
6634
- content = fs30.readFileSync(filePath, "utf8");
7024
+ content = fs32.readFileSync(filePath, "utf8");
6635
7025
  } catch {
6636
7026
  continue;
6637
7027
  }
6638
7028
  const exports = extractExports(content);
6639
7029
  if (exports.length === 0) continue;
6640
- const relPath = path31.relative(projectRoot2, filePath);
7030
+ const relPath = path33.relative(projectRoot2, filePath);
6641
7031
  lines.push(`## \`${relPath}\``);
6642
7032
  lines.push("");
6643
7033
  lines.push("**Exports:**");
@@ -6649,27 +7039,314 @@ function generateApiReference(projectRoot2) {
6649
7039
  }
6650
7040
  function listExistingDocs(projectRoot2) {
6651
7041
  const docs = [];
6652
- const docsDir = path31.join(projectRoot2, "docs");
6653
- if (fs30.existsSync(docsDir)) {
6654
- for (const f of fs30.readdirSync(docsDir)) {
6655
- 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));
6656
7046
  }
6657
7047
  }
6658
7048
  const rootDocs = ["README.md", "CHANGELOG.md", "ONBOARDING.md", "CONTRIBUTING.md"];
6659
7049
  for (const f of rootDocs) {
6660
- const full = path31.join(projectRoot2, f);
6661
- if (fs30.existsSync(full)) docs.push(full);
7050
+ const full = path33.join(projectRoot2, f);
7051
+ if (fs32.existsSync(full)) docs.push(full);
6662
7052
  }
6663
7053
  return docs;
6664
7054
  }
6665
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
+
6666
7343
  // src/cli.ts
6667
7344
  import { createRequire } from "node:module";
6668
7345
  var require2 = createRequire(import.meta.url);
6669
7346
  var { version } = require2("../package.json");
6670
7347
  var [, , command, subcommand, ...rest] = process.argv;
6671
7348
  var projectRoot = process.cwd();
6672
- var forgehiveDir = path32.join(projectRoot, ".forgehive");
7349
+ var forgehiveDir = path36.join(projectRoot, ".forgehive");
6673
7350
  if (command === "--version" || command === "-v") {
6674
7351
  console.log(version);
6675
7352
  process.exit(0);
@@ -6682,7 +7359,7 @@ USAGE
6682
7359
  fh <command> [subcommand] [options]
6683
7360
 
6684
7361
  SETUP
6685
- fh init Set up forgehive in the current project
7362
+ fh init [--yes] Set up forgehive in the current project
6686
7363
  fh confirm Activate capabilities (draft \u2192 confirmed)
6687
7364
  fh rollback Remove forgehive from the project
6688
7365
  fh status Show current project state
@@ -6703,7 +7380,7 @@ CI
6703
7380
  fh ci --init Generate GitHub Actions workflow
6704
7381
 
6705
7382
  CODEBASE
6706
- fh map Codebase structure map
7383
+ fh map [--semantic] Codebase structure map + semantic graph
6707
7384
  fh onboard [--output path] Generate ONBOARDING.md
6708
7385
  fh changelog [--since tag] Semantic changelog from git
6709
7386
  fh metrics [--since date] Developer productivity metrics
@@ -6714,6 +7391,11 @@ CODEBASE
6714
7391
  fh docs changelog [--since tag] Generate changelog
6715
7392
  fh docs adr "<title>" Create Architecture Decision Record
6716
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
+
6717
7399
  SPRINT PLANNING
6718
7400
  fh story create <title> [--epic EPC-N] [--points N]
6719
7401
  fh story list [--epic EPC-N]
@@ -6727,8 +7409,10 @@ SPRINT PLANNING
6727
7409
  fh velocity record <N> --committed N --delivered N
6728
7410
 
6729
7411
  TEAM
7412
+ fh ask "<task>" Route a task to the right agent or party set
6730
7413
  fh sync push|pull [--remote origin --branch forgehive-memory]
6731
7414
  fh run <issue-url> [--agent name] [--label label]
7415
+ fh run "<task>" Freetext task \u2014 auto-routes like fh ask
6732
7416
  fh memory show|clean|export|prune|snapshot
6733
7417
  fh memory adr list|"<title>"
6734
7418
 
@@ -6749,51 +7433,87 @@ COST
6749
7433
  process.exit(0);
6750
7434
  }
6751
7435
  function loadClaudeMdBlock() {
6752
- const templatePath = path32.join(
6753
- path32.dirname(new URL(import.meta.url).pathname),
7436
+ const templatePath = path36.join(
7437
+ path36.dirname(new URL(import.meta.url).pathname),
6754
7438
  "..",
6755
7439
  "forgehive",
6756
7440
  "templates",
6757
7441
  "claude-md.block.md"
6758
7442
  );
6759
- if (!fs31.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6760
- return fs31.readFileSync(templatePath, "utf8");
7443
+ if (!fs35.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
7444
+ return fs35.readFileSync(templatePath, "utf8");
7445
+ }
7446
+ async function promptConfirm(question) {
7447
+ if (!process.stdin.isTTY) return false;
7448
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
7449
+ return new Promise((resolve) => {
7450
+ rl.question(question, (answer) => {
7451
+ rl.close();
7452
+ const a = answer.trim().toLowerCase();
7453
+ resolve(a === "y" || a === "yes" || a === "");
7454
+ });
7455
+ });
6761
7456
  }
6762
- if (command === "init") {
6763
- const forgehiveDirExists = fs31.existsSync(forgehiveDir);
6764
- if (forgehiveDirExists && !rest.includes("--force")) {
6765
- console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
6766
- console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
6767
- console.log(` Nutze 'fh scan --update' um nur den Scan zu aktualisieren.`);
6768
- process.exit(0);
7457
+ function buildCapabilitySummary(ids) {
7458
+ if (ids.length === 0) return " Erkannt: (keine Capabilities)";
7459
+ const lines = [];
7460
+ for (let i = 0; i < ids.length && lines.length < 4; i += 4) {
7461
+ lines.push(" \u2022 " + ids.slice(i, i + 4).join(" \xB7 "));
6769
7462
  }
6770
- console.log("\u{1F50D} Analysiere Projekt...\n");
6771
- const scanResult = scan(projectRoot);
6772
- const tierCount = [1, 2, 3].map((t) => scanResult.signals.filter((s) => s.tier === t).length);
6773
- console.log(` \u2713 ${tierCount[0]} Technologie-Signale erkannt`);
6774
- console.log(` \u2713 ${tierCount[1]} Infrastruktur-Signale erkannt`);
6775
- console.log(` \u2713 ${tierCount[2]} Kontext-Signale erkannt`);
6776
- console.log();
6777
- const capMap = mapSignalsToCapabilities(scanResult);
6778
- const block = loadClaudeMdBlock();
6779
- writeForgehiveDir(projectRoot, scanResult, capMap, block);
6780
- const hash = computeHash(projectRoot);
6781
- fs31.writeFileSync(path32.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6782
- const runtimeDir = path32.join(
6783
- path32.dirname(new URL(import.meta.url).pathname),
6784
- "..",
6785
- "forgehive"
6786
- );
6787
- initForgehiveRuntime(forgehiveDir, runtimeDir);
6788
- console.log("\u2713 .forgehive/ eingerichtet");
6789
- console.log("\u2713 CLAUDE.md aktualisiert\n");
6790
- console.log("Erkannte Capabilities:");
6791
- for (const cap of capMap.confirmed) {
6792
- console.log(` + ${cap.id} (aus ${cap.source})`);
6793
- }
6794
- console.log("\nN\xE4chster Schritt:");
6795
- console.log(" Pr\xFCfe .forgehive/capabilities.yaml und f\xFChre dann aus:");
6796
- console.log(" fh confirm\n");
7463
+ return " Erkannt:\n" + lines.join("\n");
7464
+ }
7465
+ if (command === "init") {
7466
+ (async () => {
7467
+ const gitCheck = spawnSync12("git", ["--version"], { stdio: "ignore" });
7468
+ if (gitCheck.error || gitCheck.status !== 0) {
7469
+ console.error("Fehler: git nicht gefunden.");
7470
+ console.error(" Installation: https://git-scm.com");
7471
+ process.exit(1);
7472
+ }
7473
+ const forgehiveDirExists = fs35.existsSync(forgehiveDir);
7474
+ if (forgehiveDirExists && !rest.includes("--force")) {
7475
+ console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
7476
+ console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
7477
+ console.log(` Nutze 'fh scan --update' um nur den Scan zu aktualisieren.`);
7478
+ process.exit(0);
7479
+ }
7480
+ console.log("\u{1F50D} Analysiere Projekt...\n");
7481
+ const scanResult = scan(projectRoot);
7482
+ const tierCount = [1, 2, 3].map((t) => scanResult.signals.filter((s) => s.tier === t).length);
7483
+ console.log(` \u2713 ${tierCount[0]} Technologie-Signale erkannt`);
7484
+ console.log(` \u2713 ${tierCount[1]} Infrastruktur-Signale erkannt`);
7485
+ console.log(` \u2713 ${tierCount[2]} Kontext-Signale erkannt`);
7486
+ console.log();
7487
+ const capMap = mapSignalsToCapabilities(scanResult);
7488
+ const block = loadClaudeMdBlock();
7489
+ writeForgehiveDir(projectRoot, scanResult, capMap, block);
7490
+ const hash = computeHash(projectRoot);
7491
+ fs35.writeFileSync(path36.join(forgehiveDir, ".scan-hash"), hash, "utf8");
7492
+ const runtimeDir = path36.join(
7493
+ path36.dirname(new URL(import.meta.url).pathname),
7494
+ "..",
7495
+ "forgehive"
7496
+ );
7497
+ initForgehiveRuntime(forgehiveDir, runtimeDir);
7498
+ console.log("\u2713 forgehive initialisiert\n");
7499
+ console.log(buildCapabilitySummary(capMap.confirmed.map((c) => c.id)));
7500
+ console.log();
7501
+ if (subcommand === "--yes" || rest.includes("--yes")) {
7502
+ confirm(projectRoot);
7503
+ console.log("\u2713 Capabilities best\xE4tigt\n");
7504
+ } else {
7505
+ const ok = await promptConfirm(" Capabilities best\xE4tigen? [Y/n] ");
7506
+ if (ok) {
7507
+ confirm(projectRoot);
7508
+ console.log("\u2713 Capabilities best\xE4tigt\n");
7509
+ } else {
7510
+ console.log(" \xDCberpr\xFCfe .forgehive/capabilities.yaml, dann: fh confirm\n");
7511
+ }
7512
+ }
7513
+ })().catch((err) => {
7514
+ console.error(`Fehler: ${err.message}`);
7515
+ process.exit(1);
7516
+ });
6797
7517
  } else if (command === "confirm") {
6798
7518
  try {
6799
7519
  confirm(projectRoot);
@@ -6812,7 +7532,7 @@ if (command === "init") {
6812
7532
  process.exit(1);
6813
7533
  }
6814
7534
  } else if (command === "memory") {
6815
- if (!fs31.existsSync(forgehiveDir)) {
7535
+ if (!fs35.existsSync(forgehiveDir)) {
6816
7536
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6817
7537
  process.exit(1);
6818
7538
  }
@@ -6821,7 +7541,7 @@ if (command === "init") {
6821
7541
  } else if (subcommand === "clean") {
6822
7542
  cleanMemory(forgehiveDir);
6823
7543
  } else if (subcommand === "export") {
6824
- const outputPath = rest[0] ?? path32.join(projectRoot, "forgehive-memory-export.md");
7544
+ const outputPath = rest[0] ?? path36.join(projectRoot, "forgehive-memory-export.md");
6825
7545
  try {
6826
7546
  exportMemory(forgehiveDir, outputPath);
6827
7547
  } catch (err) {
@@ -6869,7 +7589,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6869
7589
  } else if (subcommand === "snapshot") {
6870
7590
  const snapAction = rest[0];
6871
7591
  if (snapAction === "export") {
6872
- const outPath = rest[1] ?? path32.join(
7592
+ const outPath = rest[1] ?? path36.join(
6873
7593
  projectRoot,
6874
7594
  `forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
6875
7595
  );
@@ -6909,11 +7629,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6909
7629
  process.exit(1);
6910
7630
  }
6911
7631
  } else if (command === "scan" && subcommand === "--update") {
6912
- if (!fs31.existsSync(forgehiveDir)) {
7632
+ if (!fs35.existsSync(forgehiveDir)) {
6913
7633
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6914
7634
  process.exit(1);
6915
7635
  }
6916
- 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;
6917
7637
  const currentHash = computeHash(projectRoot);
6918
7638
  if (savedHash === currentHash) {
6919
7639
  console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
@@ -6921,7 +7641,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6921
7641
  }
6922
7642
  console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
6923
7643
  const oldDoc = jsYaml.load(
6924
- fs31.readFileSync(path32.join(forgehiveDir, "capabilities.yaml"), "utf8")
7644
+ fs35.readFileSync(path36.join(forgehiveDir, "capabilities.yaml"), "utf8")
6925
7645
  );
6926
7646
  const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
6927
7647
  const scanResult = scan(projectRoot);
@@ -6941,16 +7661,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6941
7661
  console.log();
6942
7662
  const block = loadClaudeMdBlock();
6943
7663
  writeForgehiveDir(projectRoot, scanResult, newMap, block);
6944
- fs31.writeFileSync(path32.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
7664
+ fs35.writeFileSync(path36.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6945
7665
  console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
6946
7666
  console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
6947
7667
  }
6948
7668
  } else if (command === "scan" && subcommand === "--check") {
6949
- if (!fs31.existsSync(path32.join(forgehiveDir, ".scan-hash"))) {
7669
+ if (!fs35.existsSync(path36.join(forgehiveDir, ".scan-hash"))) {
6950
7670
  console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
6951
7671
  process.exit(1);
6952
7672
  }
6953
- const saved = fs31.readFileSync(path32.join(forgehiveDir, ".scan-hash"), "utf8").trim();
7673
+ const saved = fs35.readFileSync(path36.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6954
7674
  const current = computeHash(projectRoot);
6955
7675
  if (saved !== current) {
6956
7676
  console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
@@ -6959,7 +7679,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6959
7679
  }
6960
7680
  console.log("\u2713 capabilities.yaml ist aktuell");
6961
7681
  } else if (command === "skills") {
6962
- if (!fs31.existsSync(forgehiveDir)) {
7682
+ if (!fs35.existsSync(forgehiveDir)) {
6963
7683
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6964
7684
  process.exit(1);
6965
7685
  }
@@ -6995,7 +7715,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6995
7715
  process.exit(1);
6996
7716
  }
6997
7717
  } else if (command === "party") {
6998
- if (!fs31.existsSync(forgehiveDir)) {
7718
+ if (!fs35.existsSync(forgehiveDir)) {
6999
7719
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7000
7720
  process.exit(1);
7001
7721
  }
@@ -7116,7 +7836,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
7116
7836
  }
7117
7837
  }
7118
7838
  } else if (command === "wire") {
7119
- if (!fs31.existsSync(forgehiveDir)) {
7839
+ if (!fs35.existsSync(forgehiveDir)) {
7120
7840
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7121
7841
  process.exit(1);
7122
7842
  }
@@ -7155,7 +7875,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7155
7875
  const limitIdx = allArgs.indexOf("--limit");
7156
7876
  const alertIdx = allArgs.indexOf("--alert");
7157
7877
  if (limitIdx !== -1) {
7158
- if (!fs31.existsSync(forgehiveDir)) {
7878
+ if (!fs35.existsSync(forgehiveDir)) {
7159
7879
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7160
7880
  process.exit(1);
7161
7881
  }
@@ -7181,14 +7901,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7181
7901
  }
7182
7902
  const sessions = parseCostSessions(projectRoot);
7183
7903
  console.log(formatCostReport(sessions, range));
7184
- if (fs31.existsSync(forgehiveDir)) {
7904
+ if (fs35.existsSync(forgehiveDir)) {
7185
7905
  const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
7186
7906
  const status = checkSpendStatus(forgehiveDir, total);
7187
7907
  if (status.message) console.log("\n" + status.message);
7188
7908
  }
7189
7909
  }
7190
7910
  } else if (command === "watch") {
7191
- if (!fs31.existsSync(forgehiveDir)) {
7911
+ if (!fs35.existsSync(forgehiveDir)) {
7192
7912
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7193
7913
  process.exit(1);
7194
7914
  }
@@ -7204,7 +7924,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7204
7924
  process.exit(0);
7205
7925
  });
7206
7926
  } else if (command === "mcp") {
7207
- if (!fs31.existsSync(forgehiveDir)) {
7927
+ if (!fs35.existsSync(forgehiveDir)) {
7208
7928
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7209
7929
  process.exit(1);
7210
7930
  }
@@ -7315,7 +8035,7 @@ Setze diese Umgebungsvariablen:`);
7315
8035
  process.exit(1);
7316
8036
  }
7317
8037
  } else if (command === "security") {
7318
- if (!fs31.existsSync(forgehiveDir)) {
8038
+ if (!fs35.existsSync(forgehiveDir)) {
7319
8039
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7320
8040
  process.exit(1);
7321
8041
  }
@@ -7401,8 +8121,8 @@ Setze diese Umgebungsvariablen:`);
7401
8121
  `);
7402
8122
  const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
7403
8123
  const text = formatSecurityReport(report);
7404
- const reportPath = path32.join(forgehiveDir, "security-report.md");
7405
- fs31.writeFileSync(reportPath, text, "utf8");
8124
+ const reportPath = path36.join(forgehiveDir, "security-report.md");
8125
+ fs35.writeFileSync(reportPath, text, "utf8");
7406
8126
  console.log(text);
7407
8127
  console.log(`
7408
8128
  \u2713 Report gespeichert: ${reportPath}`);
@@ -7414,8 +8134,8 @@ Setze diese Umgebungsvariablen:`);
7414
8134
  } else if (subcommand === "permissions") {
7415
8135
  const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
7416
8136
  writePermissions2(forgehiveDir);
7417
- const permPath = path32.join(forgehiveDir, "harness", "permissions.yaml");
7418
- console.log(fs31.readFileSync(permPath, "utf8"));
8137
+ const permPath = path36.join(forgehiveDir, "harness", "permissions.yaml");
8138
+ console.log(fs35.readFileSync(permPath, "utf8"));
7419
8139
  } else {
7420
8140
  console.error(`Unbekannter security-Subcommand: ${subcommand}`);
7421
8141
  console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
@@ -7427,36 +8147,62 @@ Setze diese Umgebungsvariablen:`);
7427
8147
  const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
7428
8148
  const initFlag = allCiArgs.includes("--init");
7429
8149
  if (initFlag) {
7430
- const ghDir = path32.join(projectRoot, ".github", "workflows");
7431
- fs31.mkdirSync(ghDir, { recursive: true });
7432
- const outPath = path32.join(ghDir, "forgehive.yml");
7433
- 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");
7434
8154
  console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
7435
8155
  } else {
7436
8156
  const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
7437
8157
  const output = formatCiReport(report, format);
7438
8158
  console.log(output);
7439
8159
  if (format === "json") {
7440
- fs31.mkdirSync(forgehiveDir, { recursive: true });
7441
- 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");
7442
8162
  }
7443
8163
  if (report.status === "fail") process.exit(1);
7444
8164
  }
7445
8165
  } else if (command === "map") {
8166
+ const semanticFlag = rest.includes("--semantic");
7446
8167
  const map2 = generateMap(projectRoot);
7447
- const md = formatMap(map2);
7448
- const mapPath = path32.join(forgehiveDir, "map.md");
7449
- fs31.mkdirSync(forgehiveDir, { recursive: true });
7450
- fs31.writeFileSync(mapPath, md, "utf8");
7451
- console.log(md);
7452
- 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(`
7453
8185
  \u2714 Codebase-Map gespeichert: ${mapPath}`);
8186
+ }
7454
8187
  } else if (command === "onboard") {
7455
8188
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7456
- const outputPath = outputArg ?? path32.join(projectRoot, "ONBOARDING.md");
8189
+ const outputPath = outputArg ?? path36.join(projectRoot, "ONBOARDING.md");
7457
8190
  const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
7458
- fs31.writeFileSync(outputPath, doc, "utf8");
8191
+ fs35.writeFileSync(outputPath, doc, "utf8");
7459
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();
7460
8206
  } else if (command === "changelog") {
7461
8207
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
7462
8208
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
@@ -7465,28 +8211,28 @@ Setze diese Umgebungsvariablen:`);
7465
8211
  const commits = parseGitLog(rawLog);
7466
8212
  let version2 = "unreleased";
7467
8213
  try {
7468
- const pkgPath = path32.join(projectRoot, "package.json");
7469
- if (fs31.existsSync(pkgPath)) {
7470
- 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, ""));
7471
8217
  version2 = pkg.version ?? "unreleased";
7472
8218
  }
7473
8219
  } catch {
7474
8220
  }
7475
8221
  const md = formatChangelog(commits, version2);
7476
- const outputPath = outputArg ?? path32.join(projectRoot, "CHANGELOG.md");
8222
+ const outputPath = outputArg ?? path36.join(projectRoot, "CHANGELOG.md");
7477
8223
  let existing = "";
7478
- if (fs31.existsSync(outputPath)) existing = fs31.readFileSync(outputPath, "utf8");
7479
- 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");
7480
8226
  console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
7481
8227
  console.log(` ${commits.length} Commits verarbeitet`);
7482
8228
  } else if (command === "metrics") {
7483
8229
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
7484
8230
  const rawLog = getMetricsGitLog(projectRoot, sinceArg);
7485
8231
  const stats = parseCommitStats(rawLog);
7486
- const md = formatMetrics(stats, path32.basename(projectRoot));
7487
- const metricsPath = path32.join(forgehiveDir, "metrics.md");
7488
- fs31.mkdirSync(forgehiveDir, { recursive: true });
7489
- 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");
7490
8236
  console.log(md);
7491
8237
  console.log(`
7492
8238
  \u2714 Metrics gespeichert: ${metricsPath}`);
@@ -7514,14 +8260,26 @@ Setze diese Umgebungsvariablen:`);
7514
8260
  console.error("Usage: fh run <issue-url> [--agent <name>]");
7515
8261
  process.exit(1);
7516
8262
  }
7517
- const agentArg = rest.includes("--agent") ? rest[rest.indexOf("--agent") + 1] : void 0;
7518
- const labelArg = rest.includes("--label") ? rest[rest.indexOf("--label") + 1] : void 0;
7519
- const agentId = agentArg ?? resolveAgent(labelArg);
7520
- const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
7521
- console.log(`\u2714 ${result.message}`);
7522
- 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
+ }
7523
8281
  } else if (command === "story") {
7524
- const storiesDir = path32.join(forgehiveDir, "memory", "stories");
8282
+ const storiesDir = path36.join(forgehiveDir, "memory", "stories");
7525
8283
  if (subcommand === "create") {
7526
8284
  const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7527
8285
  const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
@@ -7532,7 +8290,7 @@ Setze diese Umgebungsvariablen:`);
7532
8290
  }
7533
8291
  const story = createStory(storiesDir, title, epicArg);
7534
8292
  if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
7535
- 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")}`);
7536
8294
  console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
7537
8295
  } else if (subcommand === "list") {
7538
8296
  const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
@@ -7581,17 +8339,32 @@ Setze diese Umgebungsvariablen:`);
7581
8339
  console.error("Verf\xFCgbar: fh story create | list | show | done");
7582
8340
  }
7583
8341
  } else if (command === "epic") {
7584
- const epicsDir = path32.join(forgehiveDir, "memory", "epics");
7585
- const storiesDir = path32.join(forgehiveDir, "memory", "stories");
8342
+ const epicsDir = path36.join(forgehiveDir, "memory", "epics");
8343
+ const storiesDir = path36.join(forgehiveDir, "memory", "stories");
7586
8344
  if (subcommand === "create") {
7587
- const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7588
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(" ");
7589
8356
  if (!title) {
7590
- console.error("Usage: fh epic create <titel> [--goal <ziel>]");
8357
+ console.error("Usage: fh epic create <titel> [--goal <ziel>] [--prd PRD-N]");
7591
8358
  process.exit(1);
7592
8359
  }
7593
- const epic = createEpic(epicsDir, title, goalArg);
7594
- 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}`);
7595
8368
  } else if (subcommand === "list") {
7596
8369
  const epics = listEpics(epicsDir);
7597
8370
  if (epics.length === 0) {
@@ -7616,8 +8389,94 @@ Setze diese Umgebungsvariablen:`);
7616
8389
  } else {
7617
8390
  console.error("Verf\xFCgbar: fh epic create | list | show");
7618
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
+ }
7619
8478
  } else if (command === "velocity") {
7620
- const velocityFile = path32.join(forgehiveDir, "memory", "velocity.md");
8479
+ const velocityFile = path36.join(forgehiveDir, "memory", "velocity.md");
7621
8480
  if (subcommand === "record") {
7622
8481
  const sprintNum = parseInt(rest[0] ?? "0", 10);
7623
8482
  const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
@@ -7636,7 +8495,7 @@ Setze diese Umgebungsvariablen:`);
7636
8495
  console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
7637
8496
  }
7638
8497
  } else if (command === "docs") {
7639
- const docsDir = path32.join(projectRoot, "docs");
8498
+ const docsDir = path36.join(projectRoot, "docs");
7640
8499
  if (!subcommand || subcommand === "list") {
7641
8500
  const existing = listExistingDocs(projectRoot);
7642
8501
  if (existing.length === 0) {
@@ -7650,30 +8509,29 @@ Setze diese Umgebungsvariablen:`);
7650
8509
  console.log(" fh docs adr <titel> \u2014 Architecture Decision Record");
7651
8510
  } else {
7652
8511
  console.log(`Vorhandene Dokumentation (${existing.length} Dateien):`);
7653
- for (const d of existing) console.log(` ${path32.relative(projectRoot, d)}`);
8512
+ for (const d of existing) console.log(` ${path36.relative(projectRoot, d)}`);
7654
8513
  console.log("");
7655
8514
  console.log("Aktualisieren: fh docs user | api | onboard | changelog");
7656
8515
  }
7657
8516
  } else if (subcommand === "user") {
7658
- fs31.mkdirSync(docsDir, { recursive: true });
8517
+ fs35.mkdirSync(docsDir, { recursive: true });
7659
8518
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7660
- const outputPath = outputArg ?? path32.join(docsDir, "user-guide.md");
8519
+ const outputPath = outputArg ?? path36.join(docsDir, "user-guide.md");
7661
8520
  const guide = generateUserGuide(projectRoot, forgehiveDir);
7662
- fs31.writeFileSync(outputPath, guide, "utf8");
8521
+ fs35.writeFileSync(outputPath, guide, "utf8");
7663
8522
  console.log(`\u2714 User Guide geschrieben: ${outputPath}`);
7664
8523
  } else if (subcommand === "api") {
7665
- fs31.mkdirSync(docsDir, { recursive: true });
8524
+ fs35.mkdirSync(docsDir, { recursive: true });
7666
8525
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7667
- const outputPath = outputArg ?? path32.join(docsDir, "api.md");
8526
+ const outputPath = outputArg ?? path36.join(docsDir, "api.md");
7668
8527
  const ref = generateApiReference(projectRoot);
7669
- fs31.writeFileSync(outputPath, ref, "utf8");
8528
+ fs35.writeFileSync(outputPath, ref, "utf8");
7670
8529
  console.log(`\u2714 API-Referenz geschrieben: ${outputPath}`);
7671
8530
  } else if (subcommand === "onboard") {
7672
8531
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7673
- const outputPath = outputArg ?? path32.join(projectRoot, "ONBOARDING.md");
7674
- fs31.mkdirSync(path32.dirname(outputPath), { recursive: true });
8532
+ const outputPath = outputArg ?? path36.join(projectRoot, "ONBOARDING.md");
7675
8533
  const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
7676
- fs31.writeFileSync(outputPath, doc, "utf8");
8534
+ fs35.writeFileSync(outputPath, doc, "utf8");
7677
8535
  console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
7678
8536
  } else if (subcommand === "changelog") {
7679
8537
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
@@ -7681,16 +8539,17 @@ Setze diese Umgebungsvariablen:`);
7681
8539
  const since = sinceArg ?? getLatestTag(projectRoot) ?? void 0;
7682
8540
  const rawLog = getGitLogSince(projectRoot, since);
7683
8541
  const commits = parseGitLog(rawLog);
7684
- let pkgVersion = "unreleased";
8542
+ let pkg = {};
7685
8543
  try {
7686
- pkgVersion = JSON.parse(fs31.readFileSync(path32.join(projectRoot, "package.json"), "utf8")).version ?? "unreleased";
8544
+ pkg = JSON.parse(fs35.readFileSync(path36.join(projectRoot, "package.json"), "utf8"));
7687
8545
  } catch {
7688
8546
  }
8547
+ const pkgVersion = pkg.version ?? "unreleased";
7689
8548
  const md = formatChangelog(commits, pkgVersion);
7690
- const outputPath = outputArg ?? path32.join(projectRoot, "CHANGELOG.md");
8549
+ const outputPath = outputArg ?? path36.join(projectRoot, "CHANGELOG.md");
7691
8550
  let existing = "";
7692
- if (fs31.existsSync(outputPath)) existing = fs31.readFileSync(outputPath, "utf8");
7693
- 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");
7694
8553
  console.log(`\u2714 CHANGELOG.md aktualisiert (${commits.length} Commits)`);
7695
8554
  } else if (subcommand === "adr") {
7696
8555
  const title = rest.join(" ");
@@ -7698,15 +8557,148 @@ Setze diese Umgebungsvariablen:`);
7698
8557
  console.error("Usage: fh docs adr <titel>");
7699
8558
  process.exit(1);
7700
8559
  }
7701
- const adrPath = createAdr(forgehiveDir, projectRoot, title);
7702
- const adrFilename = path32.relative(projectRoot, adrPath);
7703
- console.log(`\u2714 ADR erstellt: ${adrFilename}`);
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;
8563
+ const adrId = String(existing + 1).padStart(4, "0");
8564
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
8565
+ const filename = `${adrId}-${slug}.md`;
8566
+ const content = `# ADR-${adrId}: ${title}
8567
+
8568
+ **Datum:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
8569
+ **Status:** proposed
8570
+
8571
+ ## Kontext
8572
+
8573
+ (Beschreibe das Problem oder die Situation.)
8574
+
8575
+ ## Entscheidung
8576
+
8577
+ (Beschreibe die getroffene Entscheidung.)
8578
+
8579
+ ## Konsequenzen
8580
+
8581
+ (Beschreibe die Auswirkungen dieser Entscheidung.)
8582
+ `;
8583
+ fs35.writeFileSync(path36.join(adrsDir, filename), content, "utf8");
8584
+ console.log(`\u2714 ADR erstellt: .forgehive/memory/adrs/${filename}`);
7704
8585
  } else {
7705
8586
  console.error("Verf\xFCgbar: fh docs [list|user|api|onboard|changelog|adr <titel>]");
7706
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
+ }
7707
8699
  } else {
7708
8700
  console.error("Unbekannter Befehl: " + command);
7709
- 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");
7710
8702
  console.error("Hilfe: fh --help");
7711
8703
  process.exit(1);
7712
8704
  }