forgehive 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2753,7 +2753,7 @@ var init_harness = __esm({
2753
2753
  init_js_yaml();
2754
2754
  import fs35 from "node:fs";
2755
2755
  import path36 from "node:path";
2756
- import { spawnSync as spawnSync12 } from "node:child_process";
2756
+ import { spawnSync as spawnSync13 } from "node:child_process";
2757
2757
  import { createInterface } from "node:readline";
2758
2758
 
2759
2759
  // src/scanner.ts
@@ -4860,6 +4860,109 @@ function countMdFiles(dir) {
4860
4860
  if (!fs15.existsSync(dir)) return 0;
4861
4861
  return fs15.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
4862
4862
  }
4863
+ function readYamlFrontmatterFiles(dir) {
4864
+ if (!fs15.existsSync(dir)) return [];
4865
+ const results = [];
4866
+ for (const filename of fs15.readdirSync(dir)) {
4867
+ if (!filename.endsWith(".md")) continue;
4868
+ try {
4869
+ const content = fs15.readFileSync(path15.join(dir, filename), "utf8");
4870
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
4871
+ if (!match) continue;
4872
+ const parsed = jsYaml.load(match[1]);
4873
+ if (parsed && typeof parsed === "object") results.push(parsed);
4874
+ } catch {
4875
+ }
4876
+ }
4877
+ return results;
4878
+ }
4879
+ function formatSprintSection(forgehiveDir2) {
4880
+ const storiesDir = path15.join(forgehiveDir2, "memory", "stories");
4881
+ if (!fs15.existsSync(storiesDir)) return "";
4882
+ const stories = readYamlFrontmatterFiles(storiesDir);
4883
+ const open = stories.filter((s) => s.status !== "done");
4884
+ const inSprint = stories.filter((s) => s.status === "in-sprint");
4885
+ const next = inSprint[0] ?? open[0] ?? null;
4886
+ const epicsDir = path15.join(forgehiveDir2, "memory", "epics");
4887
+ const activeEpics = readYamlFrontmatterFiles(epicsDir).filter((e) => e.status === "active" || e.status === "in-progress");
4888
+ const lines = ["SPRINT"];
4889
+ lines.push(` \u25CF ${open.length} offen \u25CF ${inSprint.length} in-sprint`);
4890
+ if (activeEpics.length > 0) {
4891
+ lines.push(` Epic: ${activeEpics[0].id} ${activeEpics[0].title}`);
4892
+ }
4893
+ if (next) {
4894
+ const pts = next.points ? ` [${next.points} pts]` : "";
4895
+ lines.push(` \u2192 n\xE4chstes: ${next.id} "${next.title}"${pts}`);
4896
+ }
4897
+ return lines.join("\n");
4898
+ }
4899
+ function formatCodeHealthSection(projectRoot2, forgehiveDir2) {
4900
+ const lines = ["CODE HEALTH"];
4901
+ const gitResult = spawnSync4("git", ["status", "--porcelain"], {
4902
+ cwd: projectRoot2,
4903
+ encoding: "utf8"
4904
+ });
4905
+ if (gitResult.status === 0) {
4906
+ const changed = gitResult.stdout.trim().split("\n").filter(Boolean).length;
4907
+ lines.push(
4908
+ changed === 0 ? " \u2713 Arbeitsbaum sauber" : ` \u26A0 ${changed} ge\xE4nderte Datei(en) \u2014 nicht committed`
4909
+ );
4910
+ }
4911
+ const secPath = path15.join(forgehiveDir2, "security-last-scan.txt");
4912
+ if (fs15.existsSync(secPath)) {
4913
+ try {
4914
+ const ts = fs15.readFileSync(secPath, "utf8").trim();
4915
+ const days = Math.floor((Date.now() - new Date(ts).getTime()) / 864e5);
4916
+ lines.push(
4917
+ days > 7 ? ` \u26A0 Security-Scan: ${days} Tage alt \u2014 fh security scan` : ` \u2713 Security-Scan: ${days} Tage alt`
4918
+ );
4919
+ } catch {
4920
+ }
4921
+ }
4922
+ const prCachePath = path15.join(forgehiveDir2, "github-pr-cache.json");
4923
+ if (fs15.existsSync(prCachePath)) {
4924
+ try {
4925
+ const cache = JSON.parse(fs15.readFileSync(prCachePath, "utf8"));
4926
+ if (cache.open.length > 0) {
4927
+ lines.push(` \u25CF ${cache.open.length} offene PR(s): #${cache.open.join(", #")}`);
4928
+ }
4929
+ } catch {
4930
+ }
4931
+ }
4932
+ return lines.join("\n");
4933
+ }
4934
+ function formatWatchSection(forgehiveDir2) {
4935
+ const logPath = path15.join(forgehiveDir2, "watch-events.jsonl");
4936
+ if (!fs15.existsSync(logPath)) return "";
4937
+ const events = fs15.readFileSync(logPath, "utf8").trim().split("\n").filter(Boolean).map((line) => {
4938
+ try {
4939
+ return JSON.parse(line);
4940
+ } catch {
4941
+ return null;
4942
+ }
4943
+ }).filter((e) => e !== null).slice(-5).reverse();
4944
+ if (events.length === 0) return "";
4945
+ const lines = ["WATCH (letzte Ereignisse)"];
4946
+ for (const e of events) {
4947
+ const time = new Date(e.ts).toLocaleTimeString("de", { hour: "2-digit", minute: "2-digit" });
4948
+ if (e.type === "stack-update") {
4949
+ lines.push(` [${time}] \u2713 Stack-Update (${e.file ?? "?"}) \u2014 capabilities.yaml aktualisiert`);
4950
+ } else {
4951
+ const verdict = e.accepted === true ? "\u2192 gestartet" : e.accepted === false ? "\u2192 abgelehnt" : "\u2192 vorgeschlagen";
4952
+ lines.push(` [${time}] \u26A0 ${e.file ?? e.type} ${verdict}: ${e.agent ?? "?"}`);
4953
+ }
4954
+ }
4955
+ return lines.join("\n");
4956
+ }
4957
+ function formatDashboard(projectRoot2, forgehiveDir2) {
4958
+ const sections = [
4959
+ formatSprintSection(forgehiveDir2),
4960
+ formatCodeHealthSection(projectRoot2, forgehiveDir2),
4961
+ formatWatchSection(forgehiveDir2)
4962
+ ].filter(Boolean);
4963
+ if (sections.length === 0) return "";
4964
+ return "\n" + sections.join("\n\n");
4965
+ }
4863
4966
  function checkDrift(projectRoot2, forgehiveDir2) {
4864
4967
  const scanPath = path15.join(forgehiveDir2, "scan-result.yaml");
4865
4968
  if (!fs15.existsSync(scanPath)) {
@@ -4974,6 +5077,7 @@ function projectStatus(projectRoot2, forgehiveDir2) {
4974
5077
  // src/watch.ts
4975
5078
  import fs16 from "node:fs";
4976
5079
  import path16 from "node:path";
5080
+ import { spawnSync as spawnSync5 } from "node:child_process";
4977
5081
  function checkProjectHash(projectRoot2, forgehiveDir2, claudeMdBlock) {
4978
5082
  const hashPath = path16.join(forgehiveDir2, ".scan-hash");
4979
5083
  const savedHash = fs16.existsSync(hashPath) ? fs16.readFileSync(hashPath, "utf8").trim() : null;
@@ -5031,6 +5135,91 @@ function defaultOnUpdate(changed) {
5031
5135
  console.log("[forgehive] \u2713 Codebase changed \u2014 capabilities.yaml aktualisiert");
5032
5136
  }
5033
5137
  }
5138
+ function appendWatchEvent(forgehiveDir2, event) {
5139
+ const logPath = path16.join(forgehiveDir2, "watch-events.jsonl");
5140
+ fs16.appendFileSync(logPath, JSON.stringify(event) + "\n", "utf8");
5141
+ try {
5142
+ const stat = fs16.statSync(logPath);
5143
+ if (stat.size > 25e3) {
5144
+ const lines = fs16.readFileSync(logPath, "utf8").trim().split("\n").filter(Boolean);
5145
+ if (lines.length >= 500) {
5146
+ fs16.writeFileSync(logPath, lines.slice(-500).join("\n") + "\n", "utf8");
5147
+ }
5148
+ }
5149
+ } catch {
5150
+ }
5151
+ }
5152
+ function detectErrorType(output) {
5153
+ const lower = output.toLowerCase();
5154
+ if (lower.includes("failing") || lower.includes("assertionerror") || lower.includes("not ok")) {
5155
+ return { agent: "sam", reason: "Test-Fehler erkannt" };
5156
+ }
5157
+ if (lower.includes("error ts") || lower.includes("type error")) {
5158
+ return { agent: "kai", reason: "TypeScript-Fehler erkannt" };
5159
+ }
5160
+ if (lower.includes("secret") || lower.includes("vulnerability") || lower.includes("cve")) {
5161
+ return { agent: "vera", reason: "Sicherheitsproblem erkannt" };
5162
+ }
5163
+ if (lower.includes("error") || lower.includes("failed to compile")) {
5164
+ return { agent: "kai", reason: "Build-Fehler erkannt" };
5165
+ }
5166
+ return null;
5167
+ }
5168
+ function smartWatch(projectRoot2, forgehiveDir2, testCmd, onSuggest) {
5169
+ const srcDir = path16.join(projectRoot2, "src");
5170
+ const testDir = path16.join(projectRoot2, "test");
5171
+ const watchDirs = [srcDir, testDir].filter((d) => fs16.existsSync(d));
5172
+ let debounce = null;
5173
+ const watchers = [];
5174
+ function runAndAnalyze(changedFile) {
5175
+ const result = spawnSync5(testCmd, {
5176
+ cwd: projectRoot2,
5177
+ encoding: "utf8",
5178
+ shell: true
5179
+ });
5180
+ if (result.status === 0) return;
5181
+ const output = (result.stdout ?? "") + (result.stderr ?? "");
5182
+ const detection = detectErrorType(output);
5183
+ if (!detection) return;
5184
+ const relFile = path16.relative(projectRoot2, changedFile);
5185
+ console.log(`
5186
+ \u26A0 ${relFile} \u2014 ${detection.reason}`);
5187
+ console.log(` \u2192 ${detection.agent} ist der richtige Agent.`);
5188
+ void onSuggest(detection.agent, detection.reason, relFile).then((accepted) => {
5189
+ appendWatchEvent(forgehiveDir2, {
5190
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5191
+ type: "test-failure",
5192
+ file: relFile,
5193
+ agent: detection.agent,
5194
+ accepted
5195
+ });
5196
+ });
5197
+ }
5198
+ function schedule(changedFile) {
5199
+ if (debounce) clearTimeout(debounce);
5200
+ debounce = setTimeout(() => runAndAnalyze(changedFile), 1500);
5201
+ }
5202
+ for (const dir of watchDirs) {
5203
+ try {
5204
+ const watcher = fs16.watch(dir, { recursive: true }, (_evt, filename) => {
5205
+ if (filename && (filename.endsWith(".ts") || filename.endsWith(".js"))) {
5206
+ schedule(path16.join(dir, filename));
5207
+ }
5208
+ });
5209
+ watchers.push(watcher);
5210
+ } catch {
5211
+ }
5212
+ }
5213
+ return () => {
5214
+ if (debounce) clearTimeout(debounce);
5215
+ for (const w of watchers) {
5216
+ try {
5217
+ w.close();
5218
+ } catch {
5219
+ }
5220
+ }
5221
+ };
5222
+ }
5034
5223
 
5035
5224
  // src/cost.ts
5036
5225
  import fs17 from "node:fs";
@@ -5200,7 +5389,7 @@ function checkMcpTrust(packageName) {
5200
5389
  // src/mcp-registry.ts
5201
5390
  import fs19 from "node:fs";
5202
5391
  import path19 from "node:path";
5203
- import { spawnSync as spawnSync5 } from "node:child_process";
5392
+ import { spawnSync as spawnSync6 } from "node:child_process";
5204
5393
  function parseRegistryResponse(raw) {
5205
5394
  if (!raw || typeof raw !== "object") return [];
5206
5395
  const data = raw;
@@ -5214,7 +5403,7 @@ function parseRegistryResponse(raw) {
5214
5403
  }
5215
5404
  function searchRegistry(query) {
5216
5405
  const url = `https://registry.smithery.ai/servers?q=${encodeURIComponent(query)}&pageSize=10`;
5217
- const result = spawnSync5("curl", [
5406
+ const result = spawnSync6("curl", [
5218
5407
  "-s",
5219
5408
  "--max-time",
5220
5409
  "10",
@@ -5277,7 +5466,7 @@ function addMcpFromRegistry(projectRoot2, forgehiveDir2, packageName, envKeys) {
5277
5466
  // src/security-scan.ts
5278
5467
  import fs20 from "node:fs";
5279
5468
  import path20 from "node:path";
5280
- import { spawnSync as spawnSync6 } from "node:child_process";
5469
+ import { spawnSync as spawnSync7 } from "node:child_process";
5281
5470
  var SECRET_PATTERNS = [
5282
5471
  { name: "Anthropic Key", pattern: /sk-ant-[a-zA-Z0-9-]{20,}/g },
5283
5472
  { name: "OpenAI Key", pattern: /sk-(?!ant-)[a-zA-Z0-9]{20,}/g },
@@ -5412,7 +5601,7 @@ function scanSast(projectRoot2) {
5412
5601
  function scanDeps(projectRoot2) {
5413
5602
  const pkgJsonPath = path20.join(projectRoot2, "package.json");
5414
5603
  if (!fs20.existsSync(pkgJsonPath)) return [];
5415
- const result = spawnSync6("npm", ["audit", "--json"], {
5604
+ const result = spawnSync7("npm", ["audit", "--json"], {
5416
5605
  cwd: projectRoot2,
5417
5606
  encoding: "utf8",
5418
5607
  timeout: 3e4
@@ -5996,9 +6185,9 @@ function buildSemanticMap(projectRoot2) {
5996
6185
  init_js_yaml();
5997
6186
  import fs25 from "node:fs";
5998
6187
  import path26 from "node:path";
5999
- import { spawnSync as spawnSync7 } from "node:child_process";
6188
+ import { spawnSync as spawnSync8 } from "node:child_process";
6000
6189
  function getRecentCommits(projectRoot2, n = 20) {
6001
- const result = spawnSync7("git", ["log", `--oneline`, `-${n}`], {
6190
+ const result = spawnSync8("git", ["log", `--oneline`, `-${n}`], {
6002
6191
  cwd: projectRoot2,
6003
6192
  encoding: "utf8"
6004
6193
  });
@@ -6103,7 +6292,7 @@ function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
6103
6292
  }
6104
6293
 
6105
6294
  // src/changelog.ts
6106
- import { spawnSync as spawnSync8 } from "node:child_process";
6295
+ import { spawnSync as spawnSync9 } from "node:child_process";
6107
6296
  var TYPE_LABELS = {
6108
6297
  feat: "Added",
6109
6298
  fix: "Fixed",
@@ -6167,12 +6356,12 @@ function formatChangelog(commits, version2) {
6167
6356
  function getGitLogSince(projectRoot2, since) {
6168
6357
  const args = ["log", "--oneline", "--no-merges"];
6169
6358
  if (since) args.push(`${since}..HEAD`);
6170
- const result = spawnSync8("git", args, { cwd: projectRoot2, encoding: "utf8" });
6359
+ const result = spawnSync9("git", args, { cwd: projectRoot2, encoding: "utf8" });
6171
6360
  if (result.status !== 0) return "";
6172
6361
  return result.stdout.trim();
6173
6362
  }
6174
6363
  function getLatestTag(projectRoot2) {
6175
- const result = spawnSync8("git", ["describe", "--tags", "--abbrev=0"], {
6364
+ const result = spawnSync9("git", ["describe", "--tags", "--abbrev=0"], {
6176
6365
  cwd: projectRoot2,
6177
6366
  encoding: "utf8"
6178
6367
  });
@@ -6181,7 +6370,7 @@ function getLatestTag(projectRoot2) {
6181
6370
  }
6182
6371
 
6183
6372
  // src/metrics.ts
6184
- import { spawnSync as spawnSync9 } from "node:child_process";
6373
+ import { spawnSync as spawnSync10 } from "node:child_process";
6185
6374
  function parseCommitStats(rawLog) {
6186
6375
  const lines = rawLog.split("\n").map((l) => l.trim()).filter(Boolean);
6187
6376
  const byAuthor = {};
@@ -6238,7 +6427,7 @@ function formatMetrics(stats, projectName) {
6238
6427
  function getMetricsGitLog(projectRoot2, since) {
6239
6428
  const args = ["log", "--no-merges", "--format=%as %an %s"];
6240
6429
  if (since) args.push(`--since=${since}`);
6241
- const result = spawnSync9("git", args, { cwd: projectRoot2, encoding: "utf8" });
6430
+ const result = spawnSync10("git", args, { cwd: projectRoot2, encoding: "utf8" });
6242
6431
  if (result.status !== 0) return "";
6243
6432
  return result.stdout.trim();
6244
6433
  }
@@ -6246,18 +6435,18 @@ function getMetricsGitLog(projectRoot2, since) {
6246
6435
  // src/sync.ts
6247
6436
  import fs26 from "node:fs";
6248
6437
  import path27 from "node:path";
6249
- import { spawnSync as spawnSync10 } from "node:child_process";
6438
+ import { spawnSync as spawnSync11 } from "node:child_process";
6250
6439
  function getSyncStatus(forgehiveDir2) {
6251
6440
  const memDir = path27.join(forgehiveDir2, "memory");
6252
6441
  const files = fs26.existsSync(memDir) ? fs26.readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
6253
6442
  const projectRoot2 = path27.dirname(forgehiveDir2);
6254
- const configResult = spawnSync10(
6443
+ const configResult = spawnSync11(
6255
6444
  "git",
6256
6445
  ["config", "--get", "forgehive.sync-remote"],
6257
6446
  { cwd: projectRoot2, encoding: "utf8" }
6258
6447
  );
6259
6448
  const remote = configResult.status === 0 ? configResult.stdout.trim() : null;
6260
- const branchResult = spawnSync10(
6449
+ const branchResult = spawnSync11(
6261
6450
  "git",
6262
6451
  ["config", "--get", "forgehive.sync-branch"],
6263
6452
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6275,7 +6464,7 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6275
6464
  if (files.length === 0) {
6276
6465
  return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
6277
6466
  }
6278
- const addResult = spawnSync10(
6467
+ const addResult = spawnSync11(
6279
6468
  "git",
6280
6469
  ["add", path27.join(".forgehive", "memory")],
6281
6470
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6283,7 +6472,7 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6283
6472
  if (addResult.status !== 0) {
6284
6473
  return { success: false, message: `git add failed: ${addResult.stderr}`, filesCommitted: 0 };
6285
6474
  }
6286
- const commitResult = spawnSync10(
6475
+ const commitResult = spawnSync11(
6287
6476
  "git",
6288
6477
  ["commit", "-m", `chore: sync forgehive memory [${(/* @__PURE__ */ new Date()).toISOString()}]`],
6289
6478
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6292,7 +6481,7 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6292
6481
  if (!commitOk) {
6293
6482
  return { success: false, message: `git commit failed: ${commitResult.stderr}`, filesCommitted: 0 };
6294
6483
  }
6295
- const pushResult = spawnSync10(
6484
+ const pushResult = spawnSync11(
6296
6485
  "git",
6297
6486
  ["push", remote, `HEAD:${branch}`],
6298
6487
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6300,15 +6489,15 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6300
6489
  if (pushResult.status !== 0) {
6301
6490
  return { success: false, message: `git push failed: ${pushResult.stderr}`, filesCommitted: 0 };
6302
6491
  }
6303
- spawnSync10("git", ["config", "forgehive.sync-remote", remote], { cwd: projectRoot2 });
6304
- spawnSync10("git", ["config", "forgehive.sync-branch", branch], { cwd: projectRoot2 });
6492
+ spawnSync11("git", ["config", "forgehive.sync-remote", remote], { cwd: projectRoot2 });
6493
+ spawnSync11("git", ["config", "forgehive.sync-branch", branch], { cwd: projectRoot2 });
6305
6494
  return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
6306
6495
  }
6307
6496
  function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
6308
6497
  const projectRoot2 = path27.dirname(forgehiveDir2);
6309
6498
  const memDir = path27.join(forgehiveDir2, "memory");
6310
6499
  fs26.mkdirSync(memDir, { recursive: true });
6311
- const fetchResult = spawnSync10(
6500
+ const fetchResult = spawnSync11(
6312
6501
  "git",
6313
6502
  ["fetch", remote, branch],
6314
6503
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6316,7 +6505,7 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6316
6505
  if (fetchResult.status !== 0) {
6317
6506
  return { success: false, message: `git fetch failed: ${fetchResult.stderr}`, filesImported: [] };
6318
6507
  }
6319
- const listResult = spawnSync10(
6508
+ const listResult = spawnSync11(
6320
6509
  "git",
6321
6510
  ["ls-tree", "--name-only", `${remote}/${branch}`, ".forgehive/memory/"],
6322
6511
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6330,7 +6519,7 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
6330
6519
  const filename = path27.basename(remotePath);
6331
6520
  const localPath = path27.join(memDir, filename);
6332
6521
  if (!fs26.existsSync(localPath)) {
6333
- const contentResult = spawnSync10(
6522
+ const contentResult = spawnSync11(
6334
6523
  "git",
6335
6524
  ["show", `${remote}/${branch}:${remotePath}`],
6336
6525
  { cwd: projectRoot2, encoding: "utf8" }
@@ -6848,7 +7037,7 @@ function formatVelocityReport(history) {
6848
7037
  init_js_yaml();
6849
7038
  import fs32 from "node:fs";
6850
7039
  import path33 from "node:path";
6851
- import { spawnSync as spawnSync11 } from "node:child_process";
7040
+ import { spawnSync as spawnSync12 } from "node:child_process";
6852
7041
  var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
6853
7042
  var IGNORE_DIRS4 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
6854
7043
  var EXPORT_PATTERNS2 = [
@@ -6897,7 +7086,7 @@ function readMemoryFiles2(forgehiveDir2) {
6897
7086
  return result;
6898
7087
  }
6899
7088
  function getRecentCommits2(projectRoot2, n = 10) {
6900
- const result = spawnSync11("git", ["log", "--oneline", `-${n}`], { cwd: projectRoot2, encoding: "utf8" });
7089
+ const result = spawnSync12("git", ["log", "--oneline", `-${n}`], { cwd: projectRoot2, encoding: "utf8" });
6901
7090
  if (result.status !== 0) return [];
6902
7091
  return result.stdout.trim().split("\n").filter(Boolean);
6903
7092
  }
@@ -7339,6 +7528,21 @@ function formatPRContext(pr, files, repoFullName) {
7339
7528
  fileLines || "(keine Dateien)"
7340
7529
  ].join("\n");
7341
7530
  }
7531
+ async function fetchOpenPRs(owner, repo, token) {
7532
+ const prs = [];
7533
+ let page = 1;
7534
+ while (true) {
7535
+ const batch = await githubGet(
7536
+ `/repos/${owner}/${repo}/pulls?state=open&per_page=100&page=${page}`,
7537
+ token
7538
+ );
7539
+ if (batch.length === 0) break;
7540
+ prs.push(...batch);
7541
+ if (batch.length < 100) break;
7542
+ page++;
7543
+ }
7544
+ return prs.map((pr) => pr.number);
7545
+ }
7342
7546
 
7343
7547
  // src/cli.ts
7344
7548
  import { createRequire } from "node:module";
@@ -7363,6 +7567,7 @@ SETUP
7363
7567
  fh confirm Activate capabilities (draft \u2192 confirmed)
7364
7568
  fh rollback Remove forgehive from the project
7365
7569
  fh status Show current project state
7570
+ fh watch [--smart] Observe project; --smart runs tests on change and suggests agents
7366
7571
  fh scan --update Re-scan project after changes
7367
7572
  fh scan --check Check if scan is still current
7368
7573
 
@@ -7450,7 +7655,7 @@ async function promptConfirm(question) {
7450
7655
  rl.question(question, (answer) => {
7451
7656
  rl.close();
7452
7657
  const a = answer.trim().toLowerCase();
7453
- resolve(a === "y" || a === "yes" || a === "");
7658
+ resolve(a === "y" || a === "yes" || a === "j" || a === "ja" || a === "");
7454
7659
  });
7455
7660
  });
7456
7661
  }
@@ -7464,7 +7669,7 @@ function buildCapabilitySummary(ids) {
7464
7669
  }
7465
7670
  if (command === "init") {
7466
7671
  (async () => {
7467
- const gitCheck = spawnSync12("git", ["--version"], { stdio: "ignore" });
7672
+ const gitCheck = spawnSync13("git", ["--version"], { stdio: "ignore" });
7468
7673
  if (gitCheck.error || gitCheck.status !== 0) {
7469
7674
  console.error("Fehler: git nicht gefunden.");
7470
7675
  console.error(" Installation: https://git-scm.com");
@@ -7870,6 +8075,8 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7870
8075
  }
7871
8076
  } else if (command === "status") {
7872
8077
  console.log(projectStatus(projectRoot, forgehiveDir));
8078
+ const dashboard = formatDashboard(projectRoot, forgehiveDir);
8079
+ if (dashboard) console.log(dashboard);
7873
8080
  } else if (command === "cost") {
7874
8081
  const allArgs = [subcommand, ...rest].filter((a) => Boolean(a));
7875
8082
  const limitIdx = allArgs.indexOf("--limit");
@@ -7912,17 +8119,44 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
7912
8119
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7913
8120
  process.exit(1);
7914
8121
  }
7915
- const block = loadClaudeMdBlock();
7916
- console.log("\u{1F441} ForgeHive watch gestartet \u2014 beobachte Projekt-Dateien (Ctrl+C zum Beenden)\n");
7917
- const stop = watchProject(projectRoot, forgehiveDir, block);
7918
- process.on("SIGINT", () => {
7919
- stop();
7920
- process.exit(0);
7921
- });
7922
- process.on("SIGTERM", () => {
7923
- stop();
7924
- process.exit(0);
7925
- });
8122
+ const isSmartMode = subcommand === "--smart" || rest.includes("--smart");
8123
+ if (isSmartMode) {
8124
+ let testCmd = "npm test";
8125
+ const capsPath = path36.join(forgehiveDir, "capabilities.yaml");
8126
+ if (fs35.existsSync(capsPath)) {
8127
+ try {
8128
+ const caps = jsYaml.load(fs35.readFileSync(capsPath, "utf8"));
8129
+ if (caps?.test_command) testCmd = caps.test_command;
8130
+ } catch {
8131
+ }
8132
+ }
8133
+ console.log(`\u{1F441} ForgeHive Smart Watch \u2014 Testbefehl: ${testCmd}`);
8134
+ console.log(" \xDCberwache src/ und test/ auf \xC4nderungen (Ctrl+C zum Beenden)\n");
8135
+ const stop = smartWatch(projectRoot, forgehiveDir, testCmd, async (agent, _reason, file) => {
8136
+ console.log(` Soll ich ${agent} starten f\xFCr ${file}?`);
8137
+ return promptConfirm(" [j/n] ");
8138
+ });
8139
+ process.on("SIGINT", () => {
8140
+ stop();
8141
+ process.exit(0);
8142
+ });
8143
+ process.on("SIGTERM", () => {
8144
+ stop();
8145
+ process.exit(0);
8146
+ });
8147
+ } else {
8148
+ const block = loadClaudeMdBlock();
8149
+ console.log("\u{1F441} ForgeHive watch gestartet \u2014 beobachte Projekt-Dateien (Ctrl+C zum Beenden)\n");
8150
+ const stop = watchProject(projectRoot, forgehiveDir, block);
8151
+ process.on("SIGINT", () => {
8152
+ stop();
8153
+ process.exit(0);
8154
+ });
8155
+ process.on("SIGTERM", () => {
8156
+ stop();
8157
+ process.exit(0);
8158
+ });
8159
+ }
7926
8160
  } else if (command === "mcp") {
7927
8161
  if (!fs35.existsSync(forgehiveDir)) {
7928
8162
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
@@ -8088,6 +8322,11 @@ Setze diese Umgebungsvariablen:`);
8088
8322
  high: high.length
8089
8323
  }
8090
8324
  });
8325
+ fs35.writeFileSync(
8326
+ path36.join(forgehiveDir, "security-last-scan.txt"),
8327
+ (/* @__PURE__ */ new Date()).toISOString(),
8328
+ "utf8"
8329
+ );
8091
8330
  if (secrets.length > 0 || critical.length > 0 || high.length > 0) {
8092
8331
  process.exit(1);
8093
8332
  }
@@ -8654,6 +8893,16 @@ Task: "${taskDescription}"
8654
8893
  }
8655
8894
  console.log(`
8656
8895
  ${created} ${created === 1 ? "Story" : "Stories"} erstellt, ${skipped} \xFCbersprungen.`);
8896
+ try {
8897
+ const [syncOwner, syncRepo] = config.repo.split("/");
8898
+ const openPrNumbers = await fetchOpenPRs(syncOwner, syncRepo, creds.GITHUB_TOKEN);
8899
+ fs35.writeFileSync(
8900
+ path36.join(forgehiveDir, "github-pr-cache.json"),
8901
+ JSON.stringify({ open: openPrNumbers, ts: (/* @__PURE__ */ new Date()).toISOString() }),
8902
+ "utf8"
8903
+ );
8904
+ } catch {
8905
+ }
8657
8906
  } else if (subcommand === "pr") {
8658
8907
  const prNumberRaw = rest[0];
8659
8908
  if (!prNumberRaw || isNaN(parseInt(prNumberRaw, 10))) {
@@ -9,6 +9,7 @@ This project uses **forgehive** for structured AI-assisted development.
9
9
  - If `status: confirmed` → load silently and apply throughout the session
10
10
  2. Read `.forgehive/memory/MEMORY.md` — follow the index links to load project context
11
11
  3. Run `fh scan --check` to verify the stack snapshot is current
12
+ 4. Run `fh status` for the daily overview (sprint, code health, watch events)
12
13
 
13
14
  ### During the Session
14
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgehive",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Context-aware AI development environment — one binary, your stack.",
5
5
  "type": "module",
6
6
  "bin": {