forgehive 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +288 -39
- package/docs/user-guide.md +87 -2
- package/forgehive/templates/claude-md.block.md +1 -0
- package/package.json +1 -1
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
6188
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
6000
6189
|
function getRecentCommits(projectRoot2, n = 20) {
|
|
6001
|
-
const result =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
6304
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
7089
|
+
const result = spawnSync12("git", ["log", "--oneline", `-${n}`], { cwd: projectRoot2, encoding: "utf8" });
|
|
6901
7090
|
if (result.status !== 0) return [];
|
|
6902
7091
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
6903
7092
|
}
|
|
@@ -7339,6 +7528,21 @@ function formatPRContext(pr, files, repoFullName) {
|
|
|
7339
7528
|
fileLines || "(keine Dateien)"
|
|
7340
7529
|
].join("\n");
|
|
7341
7530
|
}
|
|
7531
|
+
async function fetchOpenPRs(owner, repo, token) {
|
|
7532
|
+
const prs = [];
|
|
7533
|
+
let page = 1;
|
|
7534
|
+
while (true) {
|
|
7535
|
+
const batch = await githubGet(
|
|
7536
|
+
`/repos/${owner}/${repo}/pulls?state=open&per_page=100&page=${page}`,
|
|
7537
|
+
token
|
|
7538
|
+
);
|
|
7539
|
+
if (batch.length === 0) break;
|
|
7540
|
+
prs.push(...batch);
|
|
7541
|
+
if (batch.length < 100) break;
|
|
7542
|
+
page++;
|
|
7543
|
+
}
|
|
7544
|
+
return prs.map((pr) => pr.number);
|
|
7545
|
+
}
|
|
7342
7546
|
|
|
7343
7547
|
// src/cli.ts
|
|
7344
7548
|
import { createRequire } from "node:module";
|
|
@@ -7363,6 +7567,7 @@ SETUP
|
|
|
7363
7567
|
fh confirm Activate capabilities (draft \u2192 confirmed)
|
|
7364
7568
|
fh rollback Remove forgehive from the project
|
|
7365
7569
|
fh status Show current project state
|
|
7570
|
+
fh watch [--smart] Observe project; --smart runs tests on change and suggests agents
|
|
7366
7571
|
fh scan --update Re-scan project after changes
|
|
7367
7572
|
fh scan --check Check if scan is still current
|
|
7368
7573
|
|
|
@@ -7450,7 +7655,7 @@ async function promptConfirm(question) {
|
|
|
7450
7655
|
rl.question(question, (answer) => {
|
|
7451
7656
|
rl.close();
|
|
7452
7657
|
const a = answer.trim().toLowerCase();
|
|
7453
|
-
resolve(a === "y" || a === "yes" || a === "");
|
|
7658
|
+
resolve(a === "y" || a === "yes" || a === "j" || a === "ja" || a === "");
|
|
7454
7659
|
});
|
|
7455
7660
|
});
|
|
7456
7661
|
}
|
|
@@ -7464,14 +7669,14 @@ function buildCapabilitySummary(ids) {
|
|
|
7464
7669
|
}
|
|
7465
7670
|
if (command === "init") {
|
|
7466
7671
|
(async () => {
|
|
7467
|
-
const gitCheck =
|
|
7672
|
+
const gitCheck = spawnSync13("git", ["--version"], { stdio: "ignore" });
|
|
7468
7673
|
if (gitCheck.error || gitCheck.status !== 0) {
|
|
7469
7674
|
console.error("Fehler: git nicht gefunden.");
|
|
7470
7675
|
console.error(" Installation: https://git-scm.com");
|
|
7471
7676
|
process.exit(1);
|
|
7472
7677
|
}
|
|
7473
7678
|
const forgehiveDirExists = fs35.existsSync(forgehiveDir);
|
|
7474
|
-
if (forgehiveDirExists && !rest.includes("--force")) {
|
|
7679
|
+
if (forgehiveDirExists && subcommand !== "--force" && !rest.includes("--force")) {
|
|
7475
7680
|
console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
|
|
7476
7681
|
console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
|
|
7477
7682
|
console.log(` Nutze 'fh scan --update' um nur den Scan zu aktualisieren.`);
|
|
@@ -7870,6 +8075,8 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7870
8075
|
}
|
|
7871
8076
|
} else if (command === "status") {
|
|
7872
8077
|
console.log(projectStatus(projectRoot, forgehiveDir));
|
|
8078
|
+
const dashboard = formatDashboard(projectRoot, forgehiveDir);
|
|
8079
|
+
if (dashboard) console.log(dashboard);
|
|
7873
8080
|
} else if (command === "cost") {
|
|
7874
8081
|
const allArgs = [subcommand, ...rest].filter((a) => Boolean(a));
|
|
7875
8082
|
const limitIdx = allArgs.indexOf("--limit");
|
|
@@ -7912,17 +8119,44 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7912
8119
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7913
8120
|
process.exit(1);
|
|
7914
8121
|
}
|
|
7915
|
-
const
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
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))) {
|
package/docs/user-guide.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# forgehive — User Guide
|
|
2
2
|
|
|
3
|
-
> Version 0.
|
|
3
|
+
> Version 0.8.0 · [npm](https://www.npmjs.com/package/forgehive) · `npm install -g forgehive`
|
|
4
4
|
|
|
5
5
|
forgehive (`fh`) gives Claude Code persistent memory about your project. Without it, Claude forgets everything between sessions — your stack, your conventions, your decisions. With it, Claude opens every session already knowing your codebase.
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ Requires **Node.js ≥ 18** and **Claude Code** (`claude` CLI). Installs two ali
|
|
|
17
17
|
Verify:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
fh --version # prints 0.
|
|
20
|
+
fh --version # prints 0.8.0
|
|
21
21
|
fh --help # full command reference
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -63,6 +63,7 @@ my-project/
|
|
|
63
63
|
adrs/ ← Architecture Decision Records
|
|
64
64
|
stories/ ← story cards (US-N.md)
|
|
65
65
|
epics/ ← epic cards (EPC-N.md)
|
|
66
|
+
prds/ ← Product Requirements Documents (PRD-N.md)
|
|
66
67
|
velocity.md ← sprint velocity history
|
|
67
68
|
skills/expert/ ← 16 preinstalled expert skills
|
|
68
69
|
AGENTS.md
|
|
@@ -196,6 +197,7 @@ Lightweight agile tracking inside `.forgehive/memory/`:
|
|
|
196
197
|
```bash
|
|
197
198
|
# Epics
|
|
198
199
|
fh epic create "User Authentication" --goal "Users can log in securely"
|
|
200
|
+
fh epic create "User Authentication" --prd PRD-1 # link to a PRD
|
|
199
201
|
fh epic list
|
|
200
202
|
fh epic show EPC-1
|
|
201
203
|
|
|
@@ -216,6 +218,83 @@ In a Claude Code session, run `/fh-sprint` to let Claude read the backlog and ve
|
|
|
216
218
|
|
|
217
219
|
---
|
|
218
220
|
|
|
221
|
+
### GitHub Integration
|
|
222
|
+
|
|
223
|
+
Connect forgehive to GitHub to sync Issues as Story cards and pull PR context.
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
fh github setup # interactive: GitHub token + owner/repo → stored in ~/.forgehive/credentials.json
|
|
227
|
+
fh github sync # fetch open Issues → create Story cards (idempotent, skips already-synced)
|
|
228
|
+
fh github pr 42 # fetch PR #42 metadata + changed files → .forgehive/github-pr-42.md
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Setup is a one-time step.** `fh github setup` prompts for a personal access token (stored at `~/.forgehive/credentials.json`) and the repository in `owner/repo` format (stored at `.forgehive/github.yaml`).
|
|
232
|
+
|
|
233
|
+
**Issue sync** creates one Story card per open GitHub Issue. It's idempotent — run it multiple times without duplicating cards. Already-synced issues (detected by `GitHub: #N` in the story file) are skipped.
|
|
234
|
+
|
|
235
|
+
**PR context** writes a `.forgehive/github-pr-N.md` file with the PR title, body, author, and full list of changed files — useful to share with a Review Party:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
fh github pr 42
|
|
239
|
+
fh party run review # Kai + Sam + Eli each read the PR context
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### Intelligent Routing — fh ask
|
|
245
|
+
|
|
246
|
+
Not sure which agent to use? Describe the task:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
fh ask "fix the login timeout bug"
|
|
250
|
+
fh ask "write integration tests for the payment service"
|
|
251
|
+
fh ask "design the new onboarding flow"
|
|
252
|
+
fh ask "review the auth module for security issues"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`fh ask` analyzes the task description using keyword matching and returns:
|
|
256
|
+
- **Which agent or party** to use
|
|
257
|
+
- **Confidence level** (high / medium / low)
|
|
258
|
+
- **Why** — which keywords triggered the recommendation
|
|
259
|
+
- **Exact command** to run — either `cd <worktree> && claude` for a single agent or `fh party run <set>` for a party
|
|
260
|
+
|
|
261
|
+
`fh run "<freetext>"` does the same when the argument isn't a URL:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
fh run "refactor the database layer" # routes to best agent automatically
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### Product Workflow
|
|
270
|
+
|
|
271
|
+
`fh product` manages the full PRD → Epic → Story pipeline.
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# PRDs
|
|
275
|
+
fh product prd "User Authentication Redesign" # create PRD-N (auto-incremented)
|
|
276
|
+
fh product list # list all PRDs with status and date
|
|
277
|
+
fh product show PRD-1 # print full PRD content
|
|
278
|
+
|
|
279
|
+
# Pipeline view
|
|
280
|
+
fh product status # tree: PRDs → Epics → Stories with status
|
|
281
|
+
fh product roadmap # write ROADMAP.md to project root
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Typical flow:**
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
fh product prd "Onboarding Redesign"
|
|
288
|
+
fh epic create "Welcome Flow" --prd PRD-1 --goal "Users complete setup in < 5 min"
|
|
289
|
+
fh story create "As a new user I want to see a setup checklist" --epic EPC-1 --points 3
|
|
290
|
+
fh product status # see the full pipeline as a tree
|
|
291
|
+
fh product roadmap # generate ROADMAP.md
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`fh epic create --prd PRD-N` links the epic to a PRD at creation time. Epics without a PRD link appear as orphans in `fh product status`.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
219
298
|
## Party Mode — Parallel Agent Sessions
|
|
220
299
|
|
|
221
300
|
Party Mode runs specialized agent sets in parallel, each in an isolated git worktree. Use this for reviews, design sessions, or full-release audits.
|
|
@@ -340,3 +419,9 @@ Run `fh party cleanup` to remove all finished worktrees.
|
|
|
340
419
|
|
|
341
420
|
**`fh cost` shows no data**
|
|
342
421
|
Cost tracking reads `~/.claude/` — requires Claude Code to be installed and to have run at least one session.
|
|
422
|
+
|
|
423
|
+
**`fh github sync` creates duplicate stories**
|
|
424
|
+
This shouldn't happen — `fh github sync` checks for `GitHub: #N` in existing story files and skips them. If you see duplicates, check that the story file contains the `GitHub: #N` reference line.
|
|
425
|
+
|
|
426
|
+
**`fh github setup` says "Nicht konfiguriert"**
|
|
427
|
+
Run `fh github setup` to configure the token and repository.
|
|
@@ -9,6 +9,7 @@ This project uses **forgehive** for structured AI-assisted development.
|
|
|
9
9
|
- If `status: confirmed` → load silently and apply throughout the session
|
|
10
10
|
2. Read `.forgehive/memory/MEMORY.md` — follow the index links to load project context
|
|
11
11
|
3. Run `fh scan --check` to verify the stack snapshot is current
|
|
12
|
+
4. Run `fh status` for the daily overview (sprint, code health, watch events)
|
|
12
13
|
|
|
13
14
|
### During the Session
|
|
14
15
|
|