forgehive 0.6.3 → 0.7.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
@@ -2751,8 +2751,8 @@ var init_harness = __esm({
2751
2751
 
2752
2752
  // src/cli.ts
2753
2753
  init_js_yaml();
2754
- import fs23 from "node:fs";
2755
- import path23 from "node:path";
2754
+ import fs27 from "node:fs";
2755
+ import path28 from "node:path";
2756
2756
 
2757
2757
  // src/scanner.ts
2758
2758
  import fs from "node:fs";
@@ -5581,24 +5581,639 @@ function formatSecurityReport(report) {
5581
5581
  return lines.join("\n");
5582
5582
  }
5583
5583
 
5584
+ // src/ci.ts
5585
+ import path23 from "node:path";
5586
+ function generateCiReport(projectRoot2, forgehiveDir2, failOn = "high") {
5587
+ const secrets = scanSecrets(projectRoot2);
5588
+ const sast = scanSast(projectRoot2);
5589
+ const deps = scanDeps(projectRoot2);
5590
+ let failedOn = null;
5591
+ if (secrets.length > 0) {
5592
+ failedOn = "secrets";
5593
+ } else if (failOn === "critical" && (sast.some((f) => f.severity === "CRITICAL") || deps.some((d) => d.severity === "critical"))) {
5594
+ failedOn = "critical-severity";
5595
+ } else if (failOn === "high" && (sast.some((f) => f.severity === "CRITICAL" || f.severity === "HIGH") || deps.some((d) => d.severity === "critical" || d.severity === "high"))) {
5596
+ failedOn = "high-severity";
5597
+ } else if (failOn === "any" && (sast.length > 0 || deps.length > 0)) {
5598
+ failedOn = "findings";
5599
+ }
5600
+ return {
5601
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5602
+ project: path23.basename(projectRoot2),
5603
+ status: failedOn ? "fail" : "pass",
5604
+ secrets,
5605
+ sast,
5606
+ deps,
5607
+ failedOn
5608
+ };
5609
+ }
5610
+ function formatCiReport(report, format = "markdown") {
5611
+ if (format === "json") return JSON.stringify(report, null, 2);
5612
+ const lines = [];
5613
+ lines.push("# forgehive CI Report");
5614
+ lines.push(`**Project:** ${report.project}`);
5615
+ lines.push(
5616
+ `**Status:** ${report.status === "pass" ? "PASS" : "FAIL"}`
5617
+ );
5618
+ lines.push(`**Time:** ${report.timestamp}`);
5619
+ lines.push("");
5620
+ if (report.secrets.length > 0) {
5621
+ lines.push("## Secrets Found");
5622
+ for (const s of report.secrets)
5623
+ lines.push(
5624
+ `- **${s.patternName}** in \`${s.file}:${s.line}\` \u2014 \`${s.match}\``
5625
+ );
5626
+ lines.push("");
5627
+ }
5628
+ if (report.sast.length > 0) {
5629
+ lines.push("## SAST Findings");
5630
+ for (const f of report.sast)
5631
+ lines.push(
5632
+ `- **${f.severity}** \`${f.ruleId}\` in \`${f.file}:${f.line}\` \u2014 ${f.snippet.slice(0, 80)}`
5633
+ );
5634
+ lines.push("");
5635
+ }
5636
+ if (report.deps.length > 0) {
5637
+ lines.push("## Dependency Vulnerabilities");
5638
+ for (const d of report.deps)
5639
+ lines.push(
5640
+ `- **${d.severity}** ${d.package} \u2014 ${d.description}`
5641
+ );
5642
+ lines.push("");
5643
+ }
5644
+ if (report.status === "pass") lines.push("All checks passed.");
5645
+ return lines.join("\n");
5646
+ }
5647
+ function getGithubActionsTemplate() {
5648
+ return `name: forgehive CI
5649
+
5650
+ on:
5651
+ pull_request:
5652
+ branches: [main, master]
5653
+ push:
5654
+ branches: [main, master]
5655
+
5656
+ jobs:
5657
+ security:
5658
+ runs-on: ubuntu-latest
5659
+ steps:
5660
+ - uses: actions/checkout@v4
5661
+ - uses: actions/setup-node@v4
5662
+ with:
5663
+ node-version: '20'
5664
+ - run: npm install -g forgehive
5665
+ - run: fh ci --format json --fail-on high
5666
+ - name: Upload CI report
5667
+ if: always()
5668
+ uses: actions/upload-artifact@v4
5669
+ with:
5670
+ name: forgehive-ci-report
5671
+ path: .forgehive/ci-report.json
5672
+ `;
5673
+ }
5674
+
5675
+ // src/map.ts
5676
+ import fs23 from "node:fs";
5677
+ import path24 from "node:path";
5678
+ var MAP_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"];
5679
+ var IGNORE_DIRS2 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
5680
+ var IMPORT_PATTERNS = [
5681
+ /^import\s+.*?from\s+['"]([^'"]+)['"]/gm,
5682
+ /^import\s+['"]([^'"]+)['"]/gm,
5683
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm
5684
+ ];
5685
+ function extractImports(content) {
5686
+ const imports = [];
5687
+ for (const pattern of IMPORT_PATTERNS) {
5688
+ pattern.lastIndex = 0;
5689
+ let m;
5690
+ while ((m = pattern.exec(content)) !== null) {
5691
+ if (m[1].startsWith(".")) imports.push(m[1]);
5692
+ }
5693
+ }
5694
+ return [...new Set(imports)];
5695
+ }
5696
+ function walkFiles(dir) {
5697
+ const results = [];
5698
+ function walk(current) {
5699
+ for (const entry of fs23.readdirSync(current, { withFileTypes: true })) {
5700
+ if (IGNORE_DIRS2.includes(entry.name)) continue;
5701
+ const full = path24.join(current, entry.name);
5702
+ if (entry.isDirectory()) walk(full);
5703
+ else if (entry.isFile() && MAP_EXTS.includes(path24.extname(entry.name)))
5704
+ results.push(full);
5705
+ }
5706
+ }
5707
+ walk(dir);
5708
+ return results;
5709
+ }
5710
+ function generateMap(projectRoot2) {
5711
+ const filePaths = walkFiles(projectRoot2);
5712
+ const files = filePaths.map((filePath) => {
5713
+ let content = "";
5714
+ try {
5715
+ content = fs23.readFileSync(filePath, "utf8");
5716
+ } catch {
5717
+ }
5718
+ const rawLines = content.split("\n");
5719
+ const lines = rawLines[rawLines.length - 1] === "" ? rawLines.length - 1 : rawLines.length;
5720
+ const imports = extractImports(content);
5721
+ return {
5722
+ path: filePath,
5723
+ relativePath: path24.relative(projectRoot2, filePath),
5724
+ lines,
5725
+ imports
5726
+ };
5727
+ });
5728
+ files.sort((a, b) => b.lines - a.lines);
5729
+ return {
5730
+ projectRoot: projectRoot2,
5731
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5732
+ files,
5733
+ totalFiles: files.length,
5734
+ totalLines: files.reduce((sum, f) => sum + f.lines, 0)
5735
+ };
5736
+ }
5737
+ function formatMap(map2) {
5738
+ const lines = [];
5739
+ lines.push("# Codebase Map");
5740
+ lines.push(`Generated: ${map2.generatedAt}`);
5741
+ lines.push(`Total: ${map2.totalFiles} files, ${map2.totalLines} lines`);
5742
+ lines.push("");
5743
+ lines.push("## Files");
5744
+ lines.push("");
5745
+ lines.push("| File | Lines | Imports |");
5746
+ lines.push("|---|---|---|");
5747
+ for (const f of map2.files) {
5748
+ const imps = f.imports.length > 0 ? f.imports.slice(0, 3).join(", ") : "\u2014";
5749
+ lines.push(`| \`${f.relativePath}\` | ${f.lines} | ${imps} |`);
5750
+ }
5751
+ lines.push("");
5752
+ lines.push("## Most Imported");
5753
+ lines.push("");
5754
+ const importCounts = {};
5755
+ for (const f of map2.files) {
5756
+ for (const imp of f.imports) {
5757
+ importCounts[imp] = (importCounts[imp] || 0) + 1;
5758
+ }
5759
+ }
5760
+ const sorted = Object.entries(importCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
5761
+ if (sorted.length > 0) {
5762
+ for (const [imp, count] of sorted)
5763
+ lines.push(`- \`${imp}\` \u2014 imported ${count}\xD7`);
5764
+ } else {
5765
+ lines.push("No internal imports detected.");
5766
+ }
5767
+ return lines.join("\n");
5768
+ }
5769
+
5770
+ // src/onboard.ts
5771
+ init_js_yaml();
5772
+ import fs24 from "node:fs";
5773
+ import path25 from "node:path";
5774
+ import { spawnSync as spawnSync7 } from "node:child_process";
5775
+ function getRecentCommits(projectRoot2, n = 20) {
5776
+ const result = spawnSync7("git", ["log", `--oneline`, `-${n}`], {
5777
+ cwd: projectRoot2,
5778
+ encoding: "utf8"
5779
+ });
5780
+ if (result.status !== 0) return [];
5781
+ return result.stdout.trim().split("\n").filter(Boolean);
5782
+ }
5783
+ function readCapabilities(forgehiveDir2) {
5784
+ const capPath = path25.join(forgehiveDir2, "capabilities.yaml");
5785
+ if (!fs24.existsSync(capPath)) return {};
5786
+ try {
5787
+ return jsYaml.load(fs24.readFileSync(capPath, "utf8")) ?? {};
5788
+ } catch {
5789
+ return {};
5790
+ }
5791
+ }
5792
+ function readMemoryFiles(forgehiveDir2) {
5793
+ const memDir = path25.join(forgehiveDir2, "memory");
5794
+ if (!fs24.existsSync(memDir)) return {};
5795
+ 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");
5798
+ }
5799
+ return result;
5800
+ }
5801
+ 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"));
5805
+ }
5806
+ function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
5807
+ const projectName = path25.basename(projectRoot2);
5808
+ const caps = readCapabilities(forgehiveDir2);
5809
+ const memFiles = readMemoryFiles(forgehiveDir2);
5810
+ const commits = getRecentCommits(projectRoot2);
5811
+ const adrs = listAdrs2(forgehiveDir2);
5812
+ const lines = [];
5813
+ lines.push(`# Onboarding: ${projectName}`);
5814
+ lines.push("");
5815
+ lines.push(`> Generated by forgehive on ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
5816
+ lines.push("");
5817
+ lines.push("## Tech Stack");
5818
+ lines.push("");
5819
+ if (caps.language) lines.push(`- **Language:** ${caps.language}`);
5820
+ if (caps.framework) lines.push(`- **Framework:** ${caps.framework}`);
5821
+ if (caps.packageManager) lines.push(`- **Package Manager:** ${caps.packageManager}`);
5822
+ if (caps.testFramework) lines.push(`- **Tests:** ${caps.testFramework}`);
5823
+ if (caps.entryPoints) {
5824
+ const eps = caps.entryPoints;
5825
+ if (Array.isArray(eps) && eps.length > 0)
5826
+ lines.push(`- **Entry Points:** ${eps.join(", ")}`);
5827
+ }
5828
+ lines.push("");
5829
+ lines.push("## Getting Started");
5830
+ lines.push("");
5831
+ lines.push("```bash");
5832
+ lines.push("# Clone the repo");
5833
+ lines.push(`git clone <repo-url>`);
5834
+ lines.push(`cd ${projectName}`);
5835
+ lines.push("");
5836
+ const pm = caps.packageManager || "npm";
5837
+ lines.push(`# Install dependencies`);
5838
+ lines.push(pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm install" : "npm install");
5839
+ lines.push("```");
5840
+ lines.push("");
5841
+ if (memFiles["project.md"]) {
5842
+ lines.push("## Project Context");
5843
+ lines.push("");
5844
+ const projectContent = memFiles["project.md"].replace(/^---[\s\S]*?---\n/m, "").trim();
5845
+ lines.push(projectContent);
5846
+ lines.push("");
5847
+ }
5848
+ if (memFiles["stack.md"]) {
5849
+ lines.push("## Stack Notes");
5850
+ lines.push("");
5851
+ const stackContent = memFiles["stack.md"].replace(/^---[\s\S]*?---\n/m, "").trim();
5852
+ lines.push(stackContent);
5853
+ lines.push("");
5854
+ }
5855
+ if (adrs.length > 0) {
5856
+ lines.push("## Architecture Decisions");
5857
+ lines.push("");
5858
+ for (const adr of adrs) lines.push(`- ${adr.replace(".md", "")}`);
5859
+ lines.push("");
5860
+ lines.push(`See \`.forgehive/memory/adrs/\` for full decision records.`);
5861
+ lines.push("");
5862
+ }
5863
+ if (commits.length > 0) {
5864
+ lines.push("## Recent Activity");
5865
+ lines.push("");
5866
+ for (const c of commits.slice(0, 10)) lines.push(`- ${c}`);
5867
+ lines.push("");
5868
+ }
5869
+ lines.push("## forgehive");
5870
+ lines.push("");
5871
+ lines.push("This project uses [forgehive](https://www.npmjs.com/package/forgehive) for AI-assisted development.");
5872
+ lines.push("");
5873
+ lines.push("```bash");
5874
+ lines.push("npm install -g forgehive");
5875
+ lines.push("fh status # check project health");
5876
+ lines.push("```");
5877
+ return lines.join("\n");
5878
+ }
5879
+
5880
+ // src/changelog.ts
5881
+ import { spawnSync as spawnSync8 } from "node:child_process";
5882
+ var TYPE_LABELS = {
5883
+ feat: "Added",
5884
+ fix: "Fixed",
5885
+ perf: "Improved",
5886
+ refactor: "Changed",
5887
+ chore: "Maintenance",
5888
+ docs: "Documentation",
5889
+ test: "Tests",
5890
+ ci: "CI",
5891
+ other: "Other"
5892
+ };
5893
+ function parseGitLog(rawLog) {
5894
+ return rawLog.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
5895
+ const spaceIdx = line.indexOf(" ");
5896
+ const hash = line.slice(0, spaceIdx);
5897
+ const rest2 = line.slice(spaceIdx + 1);
5898
+ const conventionalMatch = rest2.match(/^(\w+)(?:\(([^)]+)\))?\s*:\s*(.+)$/);
5899
+ if (conventionalMatch) {
5900
+ return {
5901
+ hash,
5902
+ type: conventionalMatch[1],
5903
+ scope: conventionalMatch[2] ?? null,
5904
+ message: conventionalMatch[3]
5905
+ };
5906
+ }
5907
+ return { hash, type: "other", scope: null, message: rest2 };
5908
+ });
5909
+ }
5910
+ function groupByType(commits) {
5911
+ const groups = {};
5912
+ for (const commit of commits) {
5913
+ if (!groups[commit.type]) groups[commit.type] = [];
5914
+ groups[commit.type].push(commit);
5915
+ }
5916
+ return groups;
5917
+ }
5918
+ function formatChangelog(commits, version) {
5919
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5920
+ const lines = [];
5921
+ lines.push(`## [${version}] \u2014 ${date}`);
5922
+ lines.push("");
5923
+ if (commits.length === 0) {
5924
+ lines.push("No changes.");
5925
+ return lines.join("\n");
5926
+ }
5927
+ const groups = groupByType(commits);
5928
+ const typeOrder = ["feat", "fix", "perf", "refactor", "docs", "chore", "test", "ci", "other"];
5929
+ for (const type2 of typeOrder) {
5930
+ if (!groups[type2] || groups[type2].length === 0) continue;
5931
+ const label = TYPE_LABELS[type2] ?? type2;
5932
+ lines.push(`### ${label}`);
5933
+ lines.push("");
5934
+ for (const c of groups[type2]) {
5935
+ const scope = c.scope ? `**${c.scope}:** ` : "";
5936
+ lines.push(`- ${scope}${c.message} (\`${c.hash}\`)`);
5937
+ }
5938
+ lines.push("");
5939
+ }
5940
+ return lines.join("\n");
5941
+ }
5942
+ function getGitLogSince(projectRoot2, since) {
5943
+ const args = ["log", "--oneline", "--no-merges"];
5944
+ if (since) args.push(`${since}..HEAD`);
5945
+ const result = spawnSync8("git", args, { cwd: projectRoot2, encoding: "utf8" });
5946
+ if (result.status !== 0) return "";
5947
+ return result.stdout.trim();
5948
+ }
5949
+ function getLatestTag(projectRoot2) {
5950
+ const result = spawnSync8("git", ["describe", "--tags", "--abbrev=0"], {
5951
+ cwd: projectRoot2,
5952
+ encoding: "utf8"
5953
+ });
5954
+ if (result.status !== 0) return null;
5955
+ return result.stdout.trim() || null;
5956
+ }
5957
+
5958
+ // src/metrics.ts
5959
+ import { spawnSync as spawnSync9 } from "node:child_process";
5960
+ function parseCommitStats(rawLog) {
5961
+ const lines = rawLog.split("\n").map((l) => l.trim()).filter(Boolean);
5962
+ const byAuthor = {};
5963
+ const byType = {};
5964
+ const dates = [];
5965
+ for (const line of lines) {
5966
+ const parts = line.split(" ");
5967
+ if (parts.length < 3) continue;
5968
+ const date = parts[0];
5969
+ const author = parts[1];
5970
+ const message = parts.slice(2).join(" ");
5971
+ dates.push(date);
5972
+ byAuthor[author] = (byAuthor[author] || 0) + 1;
5973
+ const typeMatch = message.match(/^(\w+)(?:\([^)]+\))?:/);
5974
+ const type2 = typeMatch ? typeMatch[1] : "other";
5975
+ byType[type2] = (byType[type2] || 0) + 1;
5976
+ }
5977
+ const sortedDates = dates.sort();
5978
+ return {
5979
+ totalCommits: lines.length,
5980
+ byAuthor,
5981
+ byType,
5982
+ dateRange: {
5983
+ from: sortedDates[0] ?? "",
5984
+ to: sortedDates[sortedDates.length - 1] ?? ""
5985
+ }
5986
+ };
5987
+ }
5988
+ function formatMetrics(stats, projectName) {
5989
+ const lines = [];
5990
+ lines.push(`# Developer Metrics: ${projectName}`);
5991
+ lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
5992
+ lines.push("");
5993
+ lines.push(`**Total commits:** ${stats.totalCommits}`);
5994
+ if (stats.dateRange.from)
5995
+ lines.push(`**Date range:** ${stats.dateRange.from} \u2192 ${stats.dateRange.to}`);
5996
+ lines.push("");
5997
+ if (Object.keys(stats.byAuthor).length > 0) {
5998
+ lines.push("## Commits by Author");
5999
+ lines.push("");
6000
+ for (const [author, count] of Object.entries(stats.byAuthor).sort((a, b) => b[1] - a[1]))
6001
+ lines.push(`- **${author}:** ${count}`);
6002
+ lines.push("");
6003
+ }
6004
+ if (Object.keys(stats.byType).length > 0) {
6005
+ lines.push("## Commits by Type");
6006
+ lines.push("");
6007
+ for (const [type2, count] of Object.entries(stats.byType).sort((a, b) => b[1] - a[1]))
6008
+ lines.push(`- **${type2}:** ${count}`);
6009
+ lines.push("");
6010
+ }
6011
+ return lines.join("\n");
6012
+ }
6013
+ function getMetricsGitLog(projectRoot2, since) {
6014
+ const args = ["log", "--no-merges", "--format=%as %an %s"];
6015
+ if (since) args.push(`--since=${since}`);
6016
+ const result = spawnSync9("git", args, { cwd: projectRoot2, encoding: "utf8" });
6017
+ if (result.status !== 0) return "";
6018
+ return result.stdout.trim();
6019
+ }
6020
+
6021
+ // src/sync.ts
6022
+ import fs25 from "node:fs";
6023
+ import path26 from "node:path";
6024
+ import { spawnSync as spawnSync10 } from "node:child_process";
6025
+ 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);
6029
+ const configResult = spawnSync10(
6030
+ "git",
6031
+ ["config", "--get", "forgehive.sync-remote"],
6032
+ { cwd: projectRoot2, encoding: "utf8" }
6033
+ );
6034
+ const remote = configResult.status === 0 ? configResult.stdout.trim() : null;
6035
+ const branchResult = spawnSync10(
6036
+ "git",
6037
+ ["config", "--get", "forgehive.sync-branch"],
6038
+ { cwd: projectRoot2, encoding: "utf8" }
6039
+ );
6040
+ const branch = branchResult.status === 0 ? branchResult.stdout.trim() : null;
6041
+ return { files, hasRemote: remote !== null, remote, branch };
6042
+ }
6043
+ 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)) {
6047
+ return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
6048
+ }
6049
+ const files = fs25.readdirSync(memDir).filter((f) => f.endsWith(".md"));
6050
+ if (files.length === 0) {
6051
+ return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
6052
+ }
6053
+ const addResult = spawnSync10(
6054
+ "git",
6055
+ ["add", path26.join(".forgehive", "memory")],
6056
+ { cwd: projectRoot2, encoding: "utf8" }
6057
+ );
6058
+ if (addResult.status !== 0) {
6059
+ return { success: false, message: `git add failed: ${addResult.stderr}`, filesCommitted: 0 };
6060
+ }
6061
+ const commitResult = spawnSync10(
6062
+ "git",
6063
+ ["commit", "-m", `chore: sync forgehive memory [${(/* @__PURE__ */ new Date()).toISOString()}]`],
6064
+ { cwd: projectRoot2, encoding: "utf8" }
6065
+ );
6066
+ const commitOk = commitResult.status === 0 || (commitResult.stdout + commitResult.stderr).includes("nothing to commit");
6067
+ if (!commitOk) {
6068
+ return { success: false, message: `git commit failed: ${commitResult.stderr}`, filesCommitted: 0 };
6069
+ }
6070
+ const pushResult = spawnSync10(
6071
+ "git",
6072
+ ["push", remote, `HEAD:${branch}`],
6073
+ { cwd: projectRoot2, encoding: "utf8" }
6074
+ );
6075
+ if (pushResult.status !== 0) {
6076
+ return { success: false, message: `git push failed: ${pushResult.stderr}`, filesCommitted: 0 };
6077
+ }
6078
+ spawnSync10("git", ["config", "forgehive.sync-remote", remote], { cwd: projectRoot2 });
6079
+ spawnSync10("git", ["config", "forgehive.sync-branch", branch], { cwd: projectRoot2 });
6080
+ return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
6081
+ }
6082
+ 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 });
6086
+ const fetchResult = spawnSync10(
6087
+ "git",
6088
+ ["fetch", remote, branch],
6089
+ { cwd: projectRoot2, encoding: "utf8" }
6090
+ );
6091
+ if (fetchResult.status !== 0) {
6092
+ return { success: false, message: `git fetch failed: ${fetchResult.stderr}`, filesImported: [] };
6093
+ }
6094
+ const listResult = spawnSync10(
6095
+ "git",
6096
+ ["ls-tree", "--name-only", `${remote}/${branch}`, ".forgehive/memory/"],
6097
+ { cwd: projectRoot2, encoding: "utf8" }
6098
+ );
6099
+ if (listResult.status !== 0) {
6100
+ return { success: false, message: "Remote branch hat keine Memory-Dateien.", filesImported: [] };
6101
+ }
6102
+ const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
6103
+ const imported = [];
6104
+ for (const remotePath of remoteFiles) {
6105
+ const filename = path26.basename(remotePath);
6106
+ const localPath = path26.join(memDir, filename);
6107
+ if (!fs25.existsSync(localPath)) {
6108
+ const contentResult = spawnSync10(
6109
+ "git",
6110
+ ["show", `${remote}/${branch}:${remotePath}`],
6111
+ { cwd: projectRoot2, encoding: "utf8" }
6112
+ );
6113
+ if (contentResult.status === 0) {
6114
+ fs25.writeFileSync(localPath, contentResult.stdout, "utf8");
6115
+ imported.push(filename);
6116
+ }
6117
+ }
6118
+ }
6119
+ return {
6120
+ success: true,
6121
+ message: imported.length > 0 ? `${imported.length} neue Dateien importiert.` : "Keine neuen Dateien (bereits aktuell).",
6122
+ filesImported: imported
6123
+ };
6124
+ }
6125
+
6126
+ // src/background.ts
6127
+ import fs26 from "node:fs";
6128
+ import path27 from "node:path";
6129
+ import { spawn } from "node:child_process";
6130
+ var AGENT_ROLES = {
6131
+ kai: "Senior Engineer \u2014 implements features and fixes bugs",
6132
+ sam: "QA & Test Architect \u2014 writes tests and checks quality",
6133
+ viktor: "System Architect \u2014 designs architecture and reviews structure",
6134
+ vera: "Security Analyst \u2014 reviews for OWASP, GDPR, auth issues",
6135
+ nora: "Senior Research Analyst \u2014 researches options and summarizes findings",
6136
+ eli: "Technical Writer \u2014 writes documentation and changelogs",
6137
+ suki: "UX Designer \u2014 reviews UI/UX and user flows",
6138
+ remy: "Creative Strategist \u2014 explores creative approaches"
6139
+ };
6140
+ var LABEL_TO_AGENT = {
6141
+ security: "vera",
6142
+ research: "nora",
6143
+ docs: "eli",
6144
+ documentation: "eli",
6145
+ design: "suki",
6146
+ ux: "suki",
6147
+ architecture: "viktor",
6148
+ arch: "viktor",
6149
+ test: "sam",
6150
+ qa: "sam"
6151
+ };
6152
+ function resolveAgent(label) {
6153
+ if (!label) return "kai";
6154
+ return LABEL_TO_AGENT[label.toLowerCase()] ?? "kai";
6155
+ }
6156
+ function buildAgentPrompt(issueUrl, agentId) {
6157
+ const role = AGENT_ROLES[agentId] ?? "Senior Engineer";
6158
+ return `You are ${agentId} \u2014 ${role}.
6159
+
6160
+ Your task: Resolve the following issue autonomously.
6161
+
6162
+ Issue URL: ${issueUrl}
6163
+
6164
+ Instructions:
6165
+ 1. Read the issue at the URL above (use web fetch or gh CLI if available)
6166
+ 2. Understand what needs to be done
6167
+ 3. Implement the solution following the project's conventions (read .forgehive/ for context)
6168
+ 4. Write tests for any new code
6169
+ 5. Commit your changes with a descriptive message
6170
+ 6. Report what you did
6171
+
6172
+ Work autonomously. Do not ask for clarification \u2014 use your best judgment based on the issue description and codebase context.`;
6173
+ }
6174
+ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
6175
+ const logsDir = path27.join(forgehiveDir2, "background-runs");
6176
+ fs26.mkdirSync(logsDir, { recursive: true });
6177
+ const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6178
+ const logFile = path27.join(logsDir, `${agentId}-${timestamp2}.log`);
6179
+ const prompt = buildAgentPrompt(issueUrl, agentId);
6180
+ const logStream = fs26.openSync(logFile, "w");
6181
+ const child = spawn(
6182
+ "claude",
6183
+ ["-p", prompt, "--output-format", "text"],
6184
+ {
6185
+ cwd: path27.dirname(forgehiveDir2),
6186
+ detached: true,
6187
+ stdio: ["ignore", logStream, logStream]
6188
+ }
6189
+ );
6190
+ child.unref();
6191
+ fs26.closeSync(logStream);
6192
+ return {
6193
+ pid: child.pid,
6194
+ logFile,
6195
+ message: `Agent ${agentId} gestartet (PID ${child.pid}). Log: ${logFile}`
6196
+ };
6197
+ }
6198
+
5584
6199
  // src/cli.ts
5585
6200
  var [, , command, subcommand, ...rest] = process.argv;
5586
6201
  var projectRoot = process.cwd();
5587
- var forgehiveDir = path23.join(projectRoot, ".forgehive");
6202
+ var forgehiveDir = path28.join(projectRoot, ".forgehive");
5588
6203
  if (command === "--version" || command === "-v") {
5589
- console.log("0.6.1");
6204
+ console.log("0.7.0");
5590
6205
  process.exit(0);
5591
6206
  }
5592
6207
  function loadClaudeMdBlock() {
5593
- const templatePath = path23.join(
5594
- path23.dirname(new URL(import.meta.url).pathname),
6208
+ const templatePath = path28.join(
6209
+ path28.dirname(new URL(import.meta.url).pathname),
5595
6210
  "..",
5596
6211
  "forgehive",
5597
6212
  "templates",
5598
6213
  "claude-md.block.md"
5599
6214
  );
5600
- if (!fs23.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
5601
- return fs23.readFileSync(templatePath, "utf8");
6215
+ if (!fs27.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6216
+ return fs27.readFileSync(templatePath, "utf8");
5602
6217
  }
5603
6218
  if (command === "init") {
5604
6219
  console.log("\u{1F50D} Analysiere Projekt...\n");
@@ -5612,9 +6227,9 @@ if (command === "init") {
5612
6227
  const block = loadClaudeMdBlock();
5613
6228
  writeForgehiveDir(projectRoot, scanResult, capMap, block);
5614
6229
  const hash = computeHash(projectRoot);
5615
- fs23.writeFileSync(path23.join(forgehiveDir, ".scan-hash"), hash, "utf8");
5616
- const runtimeDir = path23.join(
5617
- path23.dirname(new URL(import.meta.url).pathname),
6230
+ fs27.writeFileSync(path28.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6231
+ const runtimeDir = path28.join(
6232
+ path28.dirname(new URL(import.meta.url).pathname),
5618
6233
  "..",
5619
6234
  "forgehive"
5620
6235
  );
@@ -5646,7 +6261,7 @@ if (command === "init") {
5646
6261
  process.exit(1);
5647
6262
  }
5648
6263
  } else if (command === "memory") {
5649
- if (!fs23.existsSync(forgehiveDir)) {
6264
+ if (!fs27.existsSync(forgehiveDir)) {
5650
6265
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
5651
6266
  process.exit(1);
5652
6267
  }
@@ -5655,7 +6270,7 @@ if (command === "init") {
5655
6270
  } else if (subcommand === "clean") {
5656
6271
  cleanMemory(forgehiveDir);
5657
6272
  } else if (subcommand === "export") {
5658
- const outputPath = rest[0] ?? path23.join(projectRoot, "forgehive-memory-export.md");
6273
+ const outputPath = rest[0] ?? path28.join(projectRoot, "forgehive-memory-export.md");
5659
6274
  try {
5660
6275
  exportMemory(forgehiveDir, outputPath);
5661
6276
  } catch (err) {
@@ -5703,7 +6318,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
5703
6318
  } else if (subcommand === "snapshot") {
5704
6319
  const snapAction = rest[0];
5705
6320
  if (snapAction === "export") {
5706
- const outPath = rest[1] ?? path23.join(
6321
+ const outPath = rest[1] ?? path28.join(
5707
6322
  projectRoot,
5708
6323
  `forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
5709
6324
  );
@@ -5743,11 +6358,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
5743
6358
  process.exit(1);
5744
6359
  }
5745
6360
  } else if (command === "scan" && subcommand === "--update") {
5746
- if (!fs23.existsSync(forgehiveDir)) {
6361
+ if (!fs27.existsSync(forgehiveDir)) {
5747
6362
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
5748
6363
  process.exit(1);
5749
6364
  }
5750
- const savedHash = fs23.existsSync(path23.join(forgehiveDir, ".scan-hash")) ? fs23.readFileSync(path23.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
6365
+ const savedHash = fs27.existsSync(path28.join(forgehiveDir, ".scan-hash")) ? fs27.readFileSync(path28.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
5751
6366
  const currentHash = computeHash(projectRoot);
5752
6367
  if (savedHash === currentHash) {
5753
6368
  console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
@@ -5755,7 +6370,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
5755
6370
  }
5756
6371
  console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
5757
6372
  const oldDoc = jsYaml.load(
5758
- fs23.readFileSync(path23.join(forgehiveDir, "capabilities.yaml"), "utf8")
6373
+ fs27.readFileSync(path28.join(forgehiveDir, "capabilities.yaml"), "utf8")
5759
6374
  );
5760
6375
  const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
5761
6376
  const scanResult = scan(projectRoot);
@@ -5775,16 +6390,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
5775
6390
  console.log();
5776
6391
  const block = loadClaudeMdBlock();
5777
6392
  writeForgehiveDir(projectRoot, scanResult, newMap, block);
5778
- fs23.writeFileSync(path23.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6393
+ fs27.writeFileSync(path28.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
5779
6394
  console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
5780
6395
  console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
5781
6396
  }
5782
6397
  } else if (command === "scan" && subcommand === "--check") {
5783
- if (!fs23.existsSync(path23.join(forgehiveDir, ".scan-hash"))) {
6398
+ if (!fs27.existsSync(path28.join(forgehiveDir, ".scan-hash"))) {
5784
6399
  console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
5785
6400
  process.exit(1);
5786
6401
  }
5787
- const saved = fs23.readFileSync(path23.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6402
+ const saved = fs27.readFileSync(path28.join(forgehiveDir, ".scan-hash"), "utf8").trim();
5788
6403
  const current = computeHash(projectRoot);
5789
6404
  if (saved !== current) {
5790
6405
  console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
@@ -5793,7 +6408,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
5793
6408
  }
5794
6409
  console.log("\u2713 capabilities.yaml ist aktuell");
5795
6410
  } else if (command === "skills") {
5796
- if (!fs23.existsSync(forgehiveDir)) {
6411
+ if (!fs27.existsSync(forgehiveDir)) {
5797
6412
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
5798
6413
  process.exit(1);
5799
6414
  }
@@ -5829,7 +6444,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
5829
6444
  process.exit(1);
5830
6445
  }
5831
6446
  } else if (command === "party") {
5832
- if (!fs23.existsSync(forgehiveDir)) {
6447
+ if (!fs27.existsSync(forgehiveDir)) {
5833
6448
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
5834
6449
  process.exit(1);
5835
6450
  }
@@ -5950,7 +6565,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
5950
6565
  }
5951
6566
  }
5952
6567
  } else if (command === "wire") {
5953
- if (!fs23.existsSync(forgehiveDir)) {
6568
+ if (!fs27.existsSync(forgehiveDir)) {
5954
6569
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
5955
6570
  process.exit(1);
5956
6571
  }
@@ -5989,7 +6604,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
5989
6604
  const limitIdx = allArgs.indexOf("--limit");
5990
6605
  const alertIdx = allArgs.indexOf("--alert");
5991
6606
  if (limitIdx !== -1) {
5992
- if (!fs23.existsSync(forgehiveDir)) {
6607
+ if (!fs27.existsSync(forgehiveDir)) {
5993
6608
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
5994
6609
  process.exit(1);
5995
6610
  }
@@ -6015,14 +6630,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6015
6630
  }
6016
6631
  const sessions = parseCostSessions(projectRoot);
6017
6632
  console.log(formatCostReport(sessions, range));
6018
- if (fs23.existsSync(forgehiveDir)) {
6633
+ if (fs27.existsSync(forgehiveDir)) {
6019
6634
  const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
6020
6635
  const status = checkSpendStatus(forgehiveDir, total);
6021
6636
  if (status.message) console.log("\n" + status.message);
6022
6637
  }
6023
6638
  }
6024
6639
  } else if (command === "watch") {
6025
- if (!fs23.existsSync(forgehiveDir)) {
6640
+ if (!fs27.existsSync(forgehiveDir)) {
6026
6641
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6027
6642
  process.exit(1);
6028
6643
  }
@@ -6038,7 +6653,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6038
6653
  process.exit(0);
6039
6654
  });
6040
6655
  } else if (command === "mcp") {
6041
- if (!fs23.existsSync(forgehiveDir)) {
6656
+ if (!fs27.existsSync(forgehiveDir)) {
6042
6657
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6043
6658
  process.exit(1);
6044
6659
  }
@@ -6149,7 +6764,7 @@ Setze diese Umgebungsvariablen:`);
6149
6764
  process.exit(1);
6150
6765
  }
6151
6766
  } else if (command === "security") {
6152
- if (!fs23.existsSync(forgehiveDir)) {
6767
+ if (!fs27.existsSync(forgehiveDir)) {
6153
6768
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6154
6769
  process.exit(1);
6155
6770
  }
@@ -6235,8 +6850,8 @@ Setze diese Umgebungsvariablen:`);
6235
6850
  `);
6236
6851
  const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
6237
6852
  const text = formatSecurityReport(report);
6238
- const reportPath = path23.join(forgehiveDir, "security-report.md");
6239
- fs23.writeFileSync(reportPath, text, "utf8");
6853
+ const reportPath = path28.join(forgehiveDir, "security-report.md");
6854
+ fs27.writeFileSync(reportPath, text, "utf8");
6240
6855
  console.log(text);
6241
6856
  console.log(`
6242
6857
  \u2713 Report gespeichert: ${reportPath}`);
@@ -6248,17 +6863,116 @@ Setze diese Umgebungsvariablen:`);
6248
6863
  } else if (subcommand === "permissions") {
6249
6864
  const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
6250
6865
  writePermissions2(forgehiveDir);
6251
- const permPath = path23.join(forgehiveDir, "harness", "permissions.yaml");
6252
- console.log(fs23.readFileSync(permPath, "utf8"));
6866
+ const permPath = path28.join(forgehiveDir, "harness", "permissions.yaml");
6867
+ console.log(fs27.readFileSync(permPath, "utf8"));
6253
6868
  } else {
6254
6869
  console.error(`Unbekannter security-Subcommand: ${subcommand}`);
6255
6870
  console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
6256
6871
  process.exit(1);
6257
6872
  }
6873
+ } else if (command === "ci") {
6874
+ const allCiArgs = [subcommand, ...rest].filter((a) => Boolean(a));
6875
+ const format = allCiArgs.includes("--format") ? allCiArgs[allCiArgs.indexOf("--format") + 1] : "markdown";
6876
+ const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
6877
+ const initFlag = allCiArgs.includes("--init");
6878
+ if (initFlag) {
6879
+ const ghDir = path28.join(projectRoot, ".github", "workflows");
6880
+ fs27.mkdirSync(ghDir, { recursive: true });
6881
+ const outPath = path28.join(ghDir, "forgehive.yml");
6882
+ fs27.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
6883
+ console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
6884
+ } else {
6885
+ const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
6886
+ const output = formatCiReport(report, format);
6887
+ console.log(output);
6888
+ if (format === "json") {
6889
+ fs27.mkdirSync(forgehiveDir, { recursive: true });
6890
+ fs27.writeFileSync(path28.join(forgehiveDir, "ci-report.json"), output, "utf8");
6891
+ }
6892
+ if (report.status === "fail") process.exit(1);
6893
+ }
6894
+ } else if (command === "map") {
6895
+ const map2 = generateMap(projectRoot);
6896
+ const md = formatMap(map2);
6897
+ const mapPath = path28.join(forgehiveDir, "map.md");
6898
+ fs27.mkdirSync(forgehiveDir, { recursive: true });
6899
+ fs27.writeFileSync(mapPath, md, "utf8");
6900
+ console.log(md);
6901
+ console.log(`
6902
+ \u2714 Codebase-Map gespeichert: ${mapPath}`);
6903
+ } else if (command === "onboard") {
6904
+ const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
6905
+ const outputPath = outputArg ?? path28.join(projectRoot, "ONBOARDING.md");
6906
+ const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
6907
+ fs27.writeFileSync(outputPath, doc, "utf8");
6908
+ console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
6909
+ } else if (command === "changelog") {
6910
+ const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
6911
+ const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
6912
+ const since = sinceArg ?? getLatestTag(projectRoot) ?? void 0;
6913
+ const rawLog = getGitLogSince(projectRoot, since);
6914
+ const commits = parseGitLog(rawLog);
6915
+ let version = "unreleased";
6916
+ try {
6917
+ const pkgPath = path28.join(projectRoot, "package.json");
6918
+ if (fs27.existsSync(pkgPath)) {
6919
+ const pkg = JSON.parse(fs27.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
6920
+ version = pkg.version ?? "unreleased";
6921
+ }
6922
+ } catch {
6923
+ }
6924
+ const md = formatChangelog(commits, version);
6925
+ const outputPath = outputArg ?? path28.join(projectRoot, "CHANGELOG.md");
6926
+ let existing = "";
6927
+ if (fs27.existsSync(outputPath)) existing = fs27.readFileSync(outputPath, "utf8");
6928
+ fs27.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
6929
+ console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
6930
+ console.log(` ${commits.length} Commits verarbeitet`);
6931
+ } else if (command === "metrics") {
6932
+ const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
6933
+ const rawLog = getMetricsGitLog(projectRoot, sinceArg);
6934
+ const stats = parseCommitStats(rawLog);
6935
+ const md = formatMetrics(stats, path28.basename(projectRoot));
6936
+ const metricsPath = path28.join(forgehiveDir, "metrics.md");
6937
+ fs27.mkdirSync(forgehiveDir, { recursive: true });
6938
+ fs27.writeFileSync(metricsPath, md, "utf8");
6939
+ console.log(md);
6940
+ console.log(`
6941
+ \u2714 Metrics gespeichert: ${metricsPath}`);
6942
+ } else if (command === "sync") {
6943
+ const remoteArg = rest.includes("--remote") ? rest[rest.indexOf("--remote") + 1] : "origin";
6944
+ const branchArg = rest.includes("--branch") ? rest[rest.indexOf("--branch") + 1] : "forgehive-memory";
6945
+ if (subcommand === "push") {
6946
+ const result = pushSync(forgehiveDir, remoteArg, branchArg);
6947
+ console.log(result.success ? `\u2714 ${result.message}` : `\u2717 ${result.message}`);
6948
+ if (!result.success) process.exit(1);
6949
+ } else if (subcommand === "pull") {
6950
+ const result = pullSync(forgehiveDir, remoteArg, branchArg);
6951
+ console.log(result.success ? `\u2714 ${result.message}` : `\u2717 ${result.message}`);
6952
+ if (result.filesImported.length > 0)
6953
+ console.log(" Importiert:", result.filesImported.join(", "));
6954
+ if (!result.success) process.exit(1);
6955
+ } else {
6956
+ const status = getSyncStatus(forgehiveDir);
6957
+ console.log(`Memory: ${status.files} Dateien`);
6958
+ console.log(status.hasRemote ? `Remote: ${status.remote}/${status.branch}` : "Kein Remote konfiguriert. Nutze: fh sync push [--remote origin --branch forgehive-memory]");
6959
+ }
6960
+ } else if (command === "run") {
6961
+ const issueUrl = subcommand;
6962
+ if (!issueUrl) {
6963
+ console.error("Usage: fh run <issue-url> [--agent <name>]");
6964
+ process.exit(1);
6965
+ }
6966
+ const agentArg = rest.includes("--agent") ? rest[rest.indexOf("--agent") + 1] : void 0;
6967
+ const labelArg = rest.includes("--label") ? rest[rest.indexOf("--label") + 1] : void 0;
6968
+ const agentId = agentArg ?? resolveAgent(labelArg);
6969
+ const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
6970
+ console.log(`\u2714 ${result.message}`);
6971
+ console.log(` fh run status \u2014 aktive Sessions anzeigen`);
6258
6972
  } else {
6259
6973
  const cmd = [command, subcommand].filter(Boolean).join(" ") || "(kein)";
6260
6974
  console.error(`Unbekannter Befehl: ${cmd}`);
6261
- console.error("Verf\xFCgbar: init | confirm | rollback | scan --update | scan --check | status | cost [today|week|all] | cost --limit N --alert N | memory [show|clean|export|prune|snapshot] | memory adr [list|<titel>] | skills [list|regen|pull <url>] | party [--set <name>|run|status|cleanup] | wire <service> | mcp [auth|search|add] | security [scan|deps|report|audit|permissions]");
6975
+ console.error("Verf\xFCgbar: init | confirm | rollback | scan --update | scan --check | status | ci [--format json|markdown] [--fail-on critical|high|any] [--init] | map | onboard [--output path] | changelog [--since tag] | metrics [--since date] | sync [push|pull] [--remote origin --branch forgehive-memory] | run <issue-url> [--agent <name>] | cost [today|week|all] | cost --limit N --alert N | memory [show|clean|export|prune|snapshot] | memory adr [list|<titel>] | skills [list|regen|pull <url>] | party [--set <name>|run|status|cleanup] | wire <service> | mcp [auth|search|add] | security [scan|deps|report|audit|permissions]");
6262
6976
  process.exit(1);
6263
6977
  }
6264
6978
  /*! Bundled license information:
@@ -0,0 +1,49 @@
1
+ You are running a deployment workflow using ForgeHive.
2
+
3
+ ## Deployment Protocol
4
+
5
+ ### Step 1: Pre-deploy checks
6
+
7
+ 1. Run the full test suite — stop if any test fails:
8
+ - Node.js: `npm test`
9
+ - Python: `pytest`
10
+ - Go: `go test ./...`
11
+ 2. Run `fh security scan` — stop if CRITICAL or HIGH findings
12
+ 3. Show `git diff main...HEAD --stat` — confirm scope is as expected
13
+ 4. Check for uncommitted changes (`git status`) — commit or stash first
14
+
15
+ ### Step 2: Build
16
+
17
+ Run the build command from `capabilities.yaml` or ask the user:
18
+ - Node.js: `npm run build`
19
+ - Python: detect from `pyproject.toml` or `setup.py`
20
+
21
+ Report bundle size and any build warnings.
22
+
23
+ ### Step 3: Deploy to staging
24
+
25
+ Check `.forgehive/capabilities.yaml` for deploy configuration. If MCP is connected for the deploy service, use it. Otherwise ask: **"Wie deploye ich in die Staging-Umgebung?"**
26
+
27
+ After deploying, run smoke tests:
28
+ - Check the health endpoint if one exists
29
+ - Verify the app responds with 200
30
+
31
+ ### Step 4: Confirm and promote to production
32
+
33
+ Ask: **"Staging sieht gut aus — soll ich in Production deployen?"**
34
+
35
+ Wait for explicit confirmation before proceeding.
36
+
37
+ ### Step 5: Post-deploy
38
+
39
+ After successful production deploy:
40
+ 1. Create a git tag: `git tag v<version> && git push origin v<version>`
41
+ 2. Run `fh changelog` to update CHANGELOG.md
42
+ 3. Report: deployment complete, version, timestamp
43
+
44
+ ### Rollback
45
+
46
+ If anything fails after production deploy:
47
+ 1. Identify the last known good version
48
+ 2. Trigger rollback via the deploy service
49
+ 3. Report what failed and why
@@ -0,0 +1,40 @@
1
+ You are Eli — Technical Writer. Your job is to write or update documentation.
2
+
3
+ ## Documentation Protocol
4
+
5
+ Ask the user: **"Was soll ich dokumentieren?"**
6
+
7
+ Options:
8
+ 1. **README update** — reflect recent features/changes
9
+ 2. **API reference** — document endpoints or exported functions
10
+ 3. **CHANGELOG** — run `fh changelog` and review the output
11
+ 4. **ADR** — document an architecture decision (`fh memory adr "<title>"`)
12
+ 5. **Inline docs** — add JSDoc/docstrings to changed functions
13
+ 6. **ONBOARDING** — run `fh onboard` and review
14
+
15
+ ### For README updates
16
+
17
+ 1. Read the current `README.md`
18
+ 2. Read `.forgehive/memory/project.md` for context
19
+ 3. Read `git log --oneline -20` for recent changes
20
+ 4. Update sections that are outdated — do NOT rewrite sections that are current
21
+ 5. Add a section for any major features added since last README update
22
+
23
+ ### For API reference
24
+
25
+ 1. Find all exported functions in `src/` (TypeScript) or `__init__.py` (Python)
26
+ 2. For each exported function: name, parameters with types, return type, one-sentence description, example
27
+ 3. Write to `docs/API.md` or update existing file
28
+
29
+ ### For inline documentation
30
+
31
+ 1. Read the changed files from `git diff HEAD --name-only`
32
+ 2. For each public function missing a JSDoc/docstring: add a single-line description
33
+ 3. Only document the WHY when it's non-obvious — never document WHAT the code does
34
+
35
+ ### Quality check
36
+
37
+ Before committing:
38
+ - All links in Markdown files resolve
39
+ - Code examples in docs are syntactically valid
40
+ - No placeholder text ("TODO", "TBD", "...")
@@ -11,17 +11,22 @@ You are running a Sprint Planning session using the ForgeHive workflow.
11
11
 
12
12
  ### Step 2: Collect backlog items
13
13
 
14
- Ask the user: **"Welche Items kommen in den Sprint? Liste sie auf — ein Item pro Zeile."**
14
+ Ask the user: **"Welche Items kommen in den Sprint? Liste sie auf — ein Item pro Zeile. Oder soll ich Linear/Jira laden?"**
15
15
 
16
- Accept input in any format:
17
- - Free text ("Login-Seite bauen")
18
- - GitHub issue references (#123)
19
- - Linear ticket IDs (ENG-456)
20
- - Rough descriptions ("Performance-Probleme in der API fixen")
16
+ **If Linear MCP is available** (check if `.mcp.json` contains `linear`):
17
+ Use the Linear MCP tool to fetch open issues:
18
+ ```
19
+ mcp__linear__list_issues({ state: "backlog", limit: 30 })
20
+ ```
21
+ Show the fetched issues and ask: **"Welche davon kommen in den Sprint?"**
22
+
23
+ **If GitHub MCP is available** (check if `.mcp.json` contains `github`):
24
+ ```bash
25
+ gh issue list --state open --label "sprint-candidate" --limit 20
26
+ ```
21
27
 
22
- If GitHub or Linear MCP is connected, offer to load open issues automatically:
23
- - GitHub: `gh issue list --state open --limit 20`
24
- - Linear: use the Linear MCP tool to fetch backlog
28
+ **If no MCP connected:**
29
+ Accept free-text input one item per line.
25
30
 
26
31
  ### Step 3: Clarify and refine
27
32
 
@@ -0,0 +1,48 @@
1
+ You are Sam — QA & Test Architect. Your job is to write tests for the current changes.
2
+
3
+ ## Test-This Protocol
4
+
5
+ ### Step 1: Identify what changed
6
+
7
+ 1. Run `git diff HEAD --stat` to see which files changed
8
+ 2. Run `git diff HEAD` to see the actual changes
9
+ 3. Note: which functions/classes were added or modified?
10
+
11
+ ### Step 2: Load testing skill
12
+
13
+ Read `.forgehive/skills/expert/testing-strategies.md` for the project's testing conventions.
14
+
15
+ Check which test framework is in use (from `capabilities.yaml`):
16
+ - `node:test` → use `describe/it` with `node:assert/strict`
17
+ - `jest` → use `describe/it` with `expect`
18
+ - `pytest` → use `def test_` functions
19
+ - `go test` → use `func TestX(t *testing.T)`
20
+
21
+ ### Step 3: Write tests
22
+
23
+ For each changed function or class, write:
24
+ 1. A **happy path test** — normal input, expected output
25
+ 2. An **edge case test** — empty input, boundary values, null/undefined
26
+ 3. An **error case test** — invalid input, should throw or return error
27
+
28
+ Place tests in the appropriate test file (same name as source file, `test/` directory or `*.test.ts` suffix).
29
+
30
+ ### Step 4: Run tests
31
+
32
+ Run the test suite and confirm all new tests pass:
33
+ ```bash
34
+ npm test # or pytest, go test, etc.
35
+ ```
36
+
37
+ If tests fail, fix the implementation or the test until all pass.
38
+
39
+ ### Step 5: Coverage check
40
+
41
+ If coverage tooling is available, run it and report the coverage percentage for the changed files.
42
+
43
+ ### Step 6: Commit
44
+
45
+ ```bash
46
+ git add <test-files>
47
+ git commit -m "test: add tests for <what you tested>"
48
+ ```
@@ -3,19 +3,41 @@ sets:
3
3
  agents: [suki, viktor]
4
4
  trigger: "/design-party"
5
5
  description: "UX + Architektur parallel"
6
+ models:
7
+ suki: claude-sonnet-4-6
8
+ viktor: claude-opus-4-7
6
9
  build:
7
10
  agents: [viktor, kai, sam]
8
11
  trigger: "/party"
9
12
  description: "Architektur + Engineering + QA"
13
+ models:
14
+ viktor: claude-opus-4-7
15
+ kai: claude-sonnet-4-6
16
+ sam: claude-haiku-4-5
10
17
  review:
11
18
  agents: [kai, sam, eli]
12
19
  trigger: "/review-party"
13
20
  description: "Code Review + QA + Doku"
21
+ models:
22
+ kai: claude-opus-4-7
23
+ sam: claude-sonnet-4-6
24
+ eli: claude-sonnet-4-6
14
25
  full:
15
26
  agents: [nora, eli, remy, suki, viktor, kai, sam]
16
27
  trigger: "/full-party"
17
28
  description: "Alle Agenten"
29
+ models:
30
+ viktor: claude-opus-4-7
31
+ kai: claude-opus-4-7
32
+ nora: claude-sonnet-4-6
33
+ eli: claude-sonnet-4-6
34
+ sam: claude-sonnet-4-6
35
+ suki: claude-sonnet-4-6
36
+ remy: claude-haiku-4-5
18
37
  security:
19
38
  agents: [vera, sam]
20
39
  trigger: "/security-party"
21
40
  description: "Security Review + QA"
41
+ models:
42
+ vera: claude-opus-4-7
43
+ sam: claude-sonnet-4-6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgehive",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Context-aware AI development environment — one binary, your stack.",
5
5
  "type": "module",
6
6
  "bin": {