forgehive 0.8.1 → 1.0.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/README.md +166 -11
- package/dist/cli.js +1500 -399
- package/forgehive/templates/claude-md.block.md +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2749,12 +2749,401 @@ var init_harness = __esm({
|
|
|
2749
2749
|
}
|
|
2750
2750
|
});
|
|
2751
2751
|
|
|
2752
|
+
// src/outcomes.ts
|
|
2753
|
+
var outcomes_exports = {};
|
|
2754
|
+
__export(outcomes_exports, {
|
|
2755
|
+
aggregateByAgent: () => aggregateByAgent,
|
|
2756
|
+
aggregateByTaskType: () => aggregateByTaskType,
|
|
2757
|
+
appendOutcome: () => appendOutcome,
|
|
2758
|
+
readOutcomes: () => readOutcomes,
|
|
2759
|
+
updateRating: () => updateRating
|
|
2760
|
+
});
|
|
2761
|
+
import fs13 from "node:fs";
|
|
2762
|
+
import path13 from "node:path";
|
|
2763
|
+
function readOutcomes(forgehiveDir2) {
|
|
2764
|
+
const filePath = path13.join(forgehiveDir2, OUTCOMES_FILE);
|
|
2765
|
+
if (!fs13.existsSync(filePath)) return [];
|
|
2766
|
+
return fs13.readFileSync(filePath, "utf8").trim().split("\n").filter(Boolean).map((line) => {
|
|
2767
|
+
try {
|
|
2768
|
+
return JSON.parse(line);
|
|
2769
|
+
} catch {
|
|
2770
|
+
return null;
|
|
2771
|
+
}
|
|
2772
|
+
}).filter((e) => e !== null);
|
|
2773
|
+
}
|
|
2774
|
+
function writeOutcomes(forgehiveDir2, entries) {
|
|
2775
|
+
const filePath = path13.join(forgehiveDir2, OUTCOMES_FILE);
|
|
2776
|
+
fs13.writeFileSync(filePath, entries.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
|
|
2777
|
+
}
|
|
2778
|
+
function appendOutcome(forgehiveDir2, entry) {
|
|
2779
|
+
const filePath = path13.join(forgehiveDir2, OUTCOMES_FILE);
|
|
2780
|
+
fs13.appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf8");
|
|
2781
|
+
const entries = readOutcomes(forgehiveDir2);
|
|
2782
|
+
if (entries.length >= MAX_ENTRIES) {
|
|
2783
|
+
const kept = entries.slice(-KEEP_AFTER_ROTATION);
|
|
2784
|
+
writeOutcomes(forgehiveDir2, kept);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
function updateRating(forgehiveDir2, runId, rating) {
|
|
2788
|
+
const entries = readOutcomes(forgehiveDir2);
|
|
2789
|
+
const updated = entries.map((e) => e.runId === runId ? { ...e, rating } : e);
|
|
2790
|
+
writeOutcomes(forgehiveDir2, updated);
|
|
2791
|
+
}
|
|
2792
|
+
function aggregateByAgent(forgehiveDir2) {
|
|
2793
|
+
const entries = readOutcomes(forgehiveDir2);
|
|
2794
|
+
const result = {};
|
|
2795
|
+
for (const entry of entries) {
|
|
2796
|
+
for (const agent of entry.agents) {
|
|
2797
|
+
if (!result[agent]) {
|
|
2798
|
+
result[agent] = { avgDurationMs: 0, taskTypes: {}, avgRating: null };
|
|
2799
|
+
}
|
|
2800
|
+
const stats = result[agent];
|
|
2801
|
+
if (!stats.taskTypes[entry.taskType]) {
|
|
2802
|
+
stats.taskTypes[entry.taskType] = { count: 0, avgDurationMs: 0 };
|
|
2803
|
+
}
|
|
2804
|
+
const ttStats = stats.taskTypes[entry.taskType];
|
|
2805
|
+
const prevCount = ttStats.count;
|
|
2806
|
+
ttStats.avgDurationMs = (ttStats.avgDurationMs * prevCount + entry.durationMs) / (prevCount + 1);
|
|
2807
|
+
ttStats.count++;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
for (const agent of Object.keys(result)) {
|
|
2811
|
+
const agentEntries = entries.filter((e) => e.agents.includes(agent));
|
|
2812
|
+
result[agent].avgDurationMs = agentEntries.reduce((sum, e) => sum + e.durationMs, 0) / agentEntries.length;
|
|
2813
|
+
const rated = agentEntries.filter((e) => e.rating !== null);
|
|
2814
|
+
result[agent].avgRating = rated.length > 0 ? rated.reduce((sum, e) => sum + e.rating, 0) / rated.length : null;
|
|
2815
|
+
}
|
|
2816
|
+
return result;
|
|
2817
|
+
}
|
|
2818
|
+
function aggregateByTaskType(forgehiveDir2) {
|
|
2819
|
+
const entries = readOutcomes(forgehiveDir2);
|
|
2820
|
+
const result = {};
|
|
2821
|
+
for (const entry of entries) {
|
|
2822
|
+
if (!result[entry.taskType]) {
|
|
2823
|
+
result[entry.taskType] = { count: 0, avgDurationMs: 0 };
|
|
2824
|
+
}
|
|
2825
|
+
const agg = result[entry.taskType];
|
|
2826
|
+
agg.avgDurationMs = (agg.avgDurationMs * agg.count + entry.durationMs) / (agg.count + 1);
|
|
2827
|
+
agg.count++;
|
|
2828
|
+
}
|
|
2829
|
+
return result;
|
|
2830
|
+
}
|
|
2831
|
+
var OUTCOMES_FILE, MAX_ENTRIES, KEEP_AFTER_ROTATION;
|
|
2832
|
+
var init_outcomes = __esm({
|
|
2833
|
+
"src/outcomes.ts"() {
|
|
2834
|
+
"use strict";
|
|
2835
|
+
OUTCOMES_FILE = "outcomes.jsonl";
|
|
2836
|
+
MAX_ENTRIES = 500;
|
|
2837
|
+
KEEP_AFTER_ROTATION = 400;
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
|
|
2841
|
+
// src/next.ts
|
|
2842
|
+
var next_exports = {};
|
|
2843
|
+
__export(next_exports, {
|
|
2844
|
+
PHASE_LABELS: () => PHASE_LABELS,
|
|
2845
|
+
buildRecommendation: () => buildRecommendation,
|
|
2846
|
+
detectPhase: () => detectPhase,
|
|
2847
|
+
detectTaskTypeFromTitle: () => detectTaskTypeFromTitle,
|
|
2848
|
+
formatNextCompact: () => formatNextCompact,
|
|
2849
|
+
formatNextFull: () => formatNextFull,
|
|
2850
|
+
loadNextState: () => loadNextState
|
|
2851
|
+
});
|
|
2852
|
+
import fs16 from "node:fs";
|
|
2853
|
+
import path16 from "node:path";
|
|
2854
|
+
function detectPhase(state) {
|
|
2855
|
+
if (state.securityScanAgeMs > SEVEN_DAYS_MS) return "health-check";
|
|
2856
|
+
if (state.storiesInSprint.length > 0 && state.activeRunIds.length > 0) return "implementing";
|
|
2857
|
+
if (state.storiesInSprint.length > 0 && state.openPRNumbers.length > 0 && state.lastOutcomeType !== "party") return "review";
|
|
2858
|
+
if (state.storiesInSprint.length > 0) return "implementing";
|
|
2859
|
+
if (state.storiesDone.length > 0 && state.lastVelocityAgeMs > FOURTEEN_DAYS_MS) {
|
|
2860
|
+
return "closing-sprint";
|
|
2861
|
+
}
|
|
2862
|
+
if (state.epicsWithoutStories.length > 0) return "solutioning";
|
|
2863
|
+
if (state.storiesBacklog.length > 0) return "planning";
|
|
2864
|
+
return "kickoff";
|
|
2865
|
+
}
|
|
2866
|
+
function detectTaskTypeFromTitle(title) {
|
|
2867
|
+
const lower = title.toLowerCase();
|
|
2868
|
+
for (const [keywords, taskType] of TASK_KEYWORDS) {
|
|
2869
|
+
if (keywords.some((kw) => lower.includes(kw))) return taskType;
|
|
2870
|
+
}
|
|
2871
|
+
return "feature";
|
|
2872
|
+
}
|
|
2873
|
+
function getAgentRefinement(forgehiveDir2, taskType) {
|
|
2874
|
+
const outcomesPath = path16.join(forgehiveDir2, "outcomes.jsonl");
|
|
2875
|
+
if (!fs16.existsSync(outcomesPath)) return null;
|
|
2876
|
+
const entries = fs16.readFileSync(outcomesPath, "utf8").trim().split("\n").filter(Boolean).map((l) => {
|
|
2877
|
+
try {
|
|
2878
|
+
return JSON.parse(l);
|
|
2879
|
+
} catch {
|
|
2880
|
+
return null;
|
|
2881
|
+
}
|
|
2882
|
+
}).filter((e) => e !== null);
|
|
2883
|
+
const agentData = {};
|
|
2884
|
+
for (const entry of entries) {
|
|
2885
|
+
if (entry.taskType !== taskType) continue;
|
|
2886
|
+
for (const agent of entry.agents) {
|
|
2887
|
+
if (!agentData[agent]) agentData[agent] = [];
|
|
2888
|
+
agentData[agent].push(entry.durationMs);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
const eligible = Object.entries(agentData).filter(([, durations]) => durations.length >= 3);
|
|
2892
|
+
if (eligible.length < 2) return null;
|
|
2893
|
+
const avgByAgent = eligible.map(([agent, durations]) => ({
|
|
2894
|
+
agent,
|
|
2895
|
+
avg: durations.reduce((a, b) => a + b, 0) / durations.length
|
|
2896
|
+
}));
|
|
2897
|
+
avgByAgent.sort((a, b) => a.avg - b.avg);
|
|
2898
|
+
const best = avgByAgent[0];
|
|
2899
|
+
const second = avgByAgent[1];
|
|
2900
|
+
const pct = Math.round((1 - best.avg / second.avg) * 100);
|
|
2901
|
+
const bestMinutes = (best.avg / 6e4).toFixed(0);
|
|
2902
|
+
return `${best.agent} empfohlen \u2014 ${pct}% schneller bei ${taskType} (\xD8 ${bestMinutes} Min)`;
|
|
2903
|
+
}
|
|
2904
|
+
function getSecondaryRecommendation(state, phase) {
|
|
2905
|
+
if (phase !== "health-check" && state.securityScanAgeMs > SECONDARY_SCAN_AGE_MS) {
|
|
2906
|
+
const days = Math.floor(state.securityScanAgeMs / (24 * 60 * 60 * 1e3));
|
|
2907
|
+
return { command: "fh security scan", reason: `letzter Scan vor ${days} Tagen` };
|
|
2908
|
+
}
|
|
2909
|
+
if (phase !== "closing-sprint" && state.storiesDone.length > 0 && state.lastVelocityAgeMs > SECONDARY_VELOCITY_AGE_MS) {
|
|
2910
|
+
return { command: "fh velocity record", reason: "Velocity nicht erfasst seit > 14 Tagen" };
|
|
2911
|
+
}
|
|
2912
|
+
return null;
|
|
2913
|
+
}
|
|
2914
|
+
function buildRecommendation(phase, state, forgehiveDir2) {
|
|
2915
|
+
const secondary = getSecondaryRecommendation(state, phase);
|
|
2916
|
+
const baseRec = {
|
|
2917
|
+
phase,
|
|
2918
|
+
secondary: secondary?.command ?? null,
|
|
2919
|
+
secondaryReason: secondary?.reason ?? null
|
|
2920
|
+
};
|
|
2921
|
+
switch (phase) {
|
|
2922
|
+
case "health-check":
|
|
2923
|
+
return { ...baseRec, primary: "fh security scan", primaryHint: null };
|
|
2924
|
+
case "implementing": {
|
|
2925
|
+
const story = state.storiesInSprint[0] ?? "US-?";
|
|
2926
|
+
const hint = getAgentRefinement(forgehiveDir2, "feature");
|
|
2927
|
+
return { ...baseRec, primary: `fh auto ${story}`, primaryHint: hint };
|
|
2928
|
+
}
|
|
2929
|
+
case "review":
|
|
2930
|
+
return { ...baseRec, primary: "/review-party", primaryHint: null };
|
|
2931
|
+
case "closing-sprint": {
|
|
2932
|
+
const sprint = state.storiesDone.length;
|
|
2933
|
+
return { ...baseRec, primary: `fh velocity record sprint-${sprint}`, primaryHint: null };
|
|
2934
|
+
}
|
|
2935
|
+
case "planning":
|
|
2936
|
+
return { ...baseRec, primary: "/fh-sprint", primaryHint: null };
|
|
2937
|
+
case "kickoff":
|
|
2938
|
+
return { ...baseRec, primary: 'fh story create "<first story>"', primaryHint: null };
|
|
2939
|
+
case "solutioning": {
|
|
2940
|
+
const epic = state.epicsWithoutStories[0] ?? "EPC-?";
|
|
2941
|
+
return { ...baseRec, primary: `fh story create --epic ${epic}`, primaryHint: null };
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
function loadNextState(forgehiveDir2, projectRoot2) {
|
|
2946
|
+
const storiesDir = path16.join(forgehiveDir2, "memory", "stories");
|
|
2947
|
+
const epicsDir = path16.join(forgehiveDir2, "memory", "epics");
|
|
2948
|
+
function readIds(dir, statusFilter) {
|
|
2949
|
+
if (!fs16.existsSync(dir)) return [];
|
|
2950
|
+
const results = [];
|
|
2951
|
+
for (const filename of fs16.readdirSync(dir)) {
|
|
2952
|
+
if (!filename.endsWith(".md")) continue;
|
|
2953
|
+
try {
|
|
2954
|
+
const content = fs16.readFileSync(path16.join(dir, filename), "utf8");
|
|
2955
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2956
|
+
if (!match) continue;
|
|
2957
|
+
const data = jsYaml.load(match[1]);
|
|
2958
|
+
if (!data?.id) continue;
|
|
2959
|
+
if (statusFilter && data.status !== statusFilter) continue;
|
|
2960
|
+
results.push(data.id);
|
|
2961
|
+
} catch {
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return results;
|
|
2965
|
+
}
|
|
2966
|
+
function getEpicsWithoutStories() {
|
|
2967
|
+
if (!fs16.existsSync(epicsDir)) return [];
|
|
2968
|
+
const allStoryEpicIds = /* @__PURE__ */ new Set();
|
|
2969
|
+
if (fs16.existsSync(storiesDir)) {
|
|
2970
|
+
for (const f of fs16.readdirSync(storiesDir)) {
|
|
2971
|
+
if (!f.endsWith(".md")) continue;
|
|
2972
|
+
try {
|
|
2973
|
+
const content = fs16.readFileSync(path16.join(storiesDir, f), "utf8");
|
|
2974
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2975
|
+
if (!match) continue;
|
|
2976
|
+
const data = jsYaml.load(match[1]);
|
|
2977
|
+
if (data?.epicId) allStoryEpicIds.add(data.epicId);
|
|
2978
|
+
} catch {
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
const result = [];
|
|
2983
|
+
for (const f of fs16.readdirSync(epicsDir)) {
|
|
2984
|
+
if (!f.endsWith(".md")) continue;
|
|
2985
|
+
try {
|
|
2986
|
+
const content = fs16.readFileSync(path16.join(epicsDir, f), "utf8");
|
|
2987
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2988
|
+
if (!match) continue;
|
|
2989
|
+
const data = jsYaml.load(match[1]);
|
|
2990
|
+
if (data?.id && !allStoryEpicIds.has(data.id)) result.push(data.id);
|
|
2991
|
+
} catch {
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return result;
|
|
2995
|
+
}
|
|
2996
|
+
let securityScanAgeMs = 0;
|
|
2997
|
+
const secPath = path16.join(forgehiveDir2, "security-last-scan.txt");
|
|
2998
|
+
if (fs16.existsSync(secPath)) {
|
|
2999
|
+
try {
|
|
3000
|
+
const ts = fs16.readFileSync(secPath, "utf8").trim();
|
|
3001
|
+
securityScanAgeMs = Date.now() - new Date(ts).getTime();
|
|
3002
|
+
} catch {
|
|
3003
|
+
securityScanAgeMs = Infinity;
|
|
3004
|
+
}
|
|
3005
|
+
} else {
|
|
3006
|
+
securityScanAgeMs = Infinity;
|
|
3007
|
+
}
|
|
3008
|
+
let lastVelocityAgeMs = Infinity;
|
|
3009
|
+
const velocityPath = path16.join(forgehiveDir2, "memory", "velocity.md");
|
|
3010
|
+
if (fs16.existsSync(velocityPath)) {
|
|
3011
|
+
try {
|
|
3012
|
+
const content = fs16.readFileSync(velocityPath, "utf8");
|
|
3013
|
+
const dateMatches = content.match(/\|\s*(\d{4}-\d{2}-\d{2})\s*\|/g);
|
|
3014
|
+
if (dateMatches && dateMatches.length > 0) {
|
|
3015
|
+
const lastDate = dateMatches[dateMatches.length - 1].replace(/[\|\s]/g, "");
|
|
3016
|
+
lastVelocityAgeMs = Date.now() - new Date(lastDate).getTime();
|
|
3017
|
+
}
|
|
3018
|
+
} catch {
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
const runsDir = path16.join(forgehiveDir2, "runs");
|
|
3022
|
+
const activeRunIds = [];
|
|
3023
|
+
if (fs16.existsSync(runsDir)) {
|
|
3024
|
+
for (const runDir of fs16.readdirSync(runsDir)) {
|
|
3025
|
+
const statePath = path16.join(runsDir, runDir, "state.yaml");
|
|
3026
|
+
if (!fs16.existsSync(statePath)) continue;
|
|
3027
|
+
try {
|
|
3028
|
+
const state = jsYaml.load(fs16.readFileSync(statePath, "utf8"));
|
|
3029
|
+
if (state?.status && !["done", "gate-stuck"].includes(state.status) && state.runId) {
|
|
3030
|
+
activeRunIds.push(state.runId);
|
|
3031
|
+
}
|
|
3032
|
+
} catch {
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
let openPRNumbers = [];
|
|
3037
|
+
const prCachePath = path16.join(forgehiveDir2, "github-pr-cache.json");
|
|
3038
|
+
if (fs16.existsSync(prCachePath)) {
|
|
3039
|
+
try {
|
|
3040
|
+
const cache = JSON.parse(fs16.readFileSync(prCachePath, "utf8"));
|
|
3041
|
+
openPRNumbers = cache.open ?? [];
|
|
3042
|
+
} catch {
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
let lastOutcomeType = null;
|
|
3046
|
+
const outcomesPath = path16.join(forgehiveDir2, "outcomes.jsonl");
|
|
3047
|
+
if (fs16.existsSync(outcomesPath)) {
|
|
3048
|
+
try {
|
|
3049
|
+
const lines = fs16.readFileSync(outcomesPath, "utf8").trim().split("\n").filter(Boolean);
|
|
3050
|
+
if (lines.length > 0) {
|
|
3051
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
3052
|
+
if (last.type === "auto" || last.type === "party") {
|
|
3053
|
+
lastOutcomeType = last.type;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
} catch {
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
return {
|
|
3060
|
+
storiesInSprint: readIds(storiesDir, "in-sprint"),
|
|
3061
|
+
storiesBacklog: readIds(storiesDir, "backlog"),
|
|
3062
|
+
storiesDone: readIds(storiesDir, "done"),
|
|
3063
|
+
epicsWithoutStories: getEpicsWithoutStories(),
|
|
3064
|
+
openPRNumbers,
|
|
3065
|
+
activeRunIds,
|
|
3066
|
+
securityScanAgeMs,
|
|
3067
|
+
lastVelocityAgeMs,
|
|
3068
|
+
lastOutcomeType
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
function formatNextFull(projectRoot2, forgehiveDir2) {
|
|
3072
|
+
const state = loadNextState(forgehiveDir2, projectRoot2);
|
|
3073
|
+
const phase = detectPhase(state);
|
|
3074
|
+
const rec = buildRecommendation(phase, state, forgehiveDir2);
|
|
3075
|
+
const phaseLabel = PHASE_LABELS[phase];
|
|
3076
|
+
const lines = [
|
|
3077
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
3078
|
+
"fh next",
|
|
3079
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
3080
|
+
`Phase: ${phaseLabel}`,
|
|
3081
|
+
""
|
|
3082
|
+
];
|
|
3083
|
+
if (state.storiesInSprint.length > 0) {
|
|
3084
|
+
lines.push(` ${state.storiesInSprint.length} Stories in Sprint:`);
|
|
3085
|
+
for (const id of state.storiesInSprint) {
|
|
3086
|
+
lines.push(` ${id}`);
|
|
3087
|
+
}
|
|
3088
|
+
lines.push("");
|
|
3089
|
+
}
|
|
3090
|
+
lines.push("Empfehlung:");
|
|
3091
|
+
lines.push(` \u2192 ${rec.primary}`);
|
|
3092
|
+
if (rec.primaryHint) {
|
|
3093
|
+
lines.push(` ${rec.primaryHint}`);
|
|
3094
|
+
}
|
|
3095
|
+
if (rec.secondary) {
|
|
3096
|
+
lines.push("");
|
|
3097
|
+
lines.push("Auch f\xE4llig:");
|
|
3098
|
+
lines.push(` \u2192 ${rec.secondary} (${rec.secondaryReason})`);
|
|
3099
|
+
}
|
|
3100
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
3101
|
+
return lines.join("\n");
|
|
3102
|
+
}
|
|
3103
|
+
function formatNextCompact(projectRoot2, forgehiveDir2) {
|
|
3104
|
+
const state = loadNextState(forgehiveDir2, projectRoot2);
|
|
3105
|
+
const phase = detectPhase(state);
|
|
3106
|
+
const rec = buildRecommendation(phase, state, forgehiveDir2);
|
|
3107
|
+
const phaseLabel = PHASE_LABELS[phase];
|
|
3108
|
+
const hint = rec.primaryHint ? ` (${rec.primaryHint})` : "";
|
|
3109
|
+
return `## Next
|
|
3110
|
+
Phase: ${phaseLabel}
|
|
3111
|
+
\u2192 ${rec.primary}${hint}`;
|
|
3112
|
+
}
|
|
3113
|
+
var SEVEN_DAYS_MS, FOURTEEN_DAYS_MS, TASK_KEYWORDS, SECONDARY_SCAN_AGE_MS, SECONDARY_VELOCITY_AGE_MS, PHASE_LABELS;
|
|
3114
|
+
var init_next = __esm({
|
|
3115
|
+
"src/next.ts"() {
|
|
3116
|
+
"use strict";
|
|
3117
|
+
init_js_yaml();
|
|
3118
|
+
SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
3119
|
+
FOURTEEN_DAYS_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
3120
|
+
TASK_KEYWORDS = [
|
|
3121
|
+
[["fix", "bug", "crash", "error", "broken"], "bug-fix"],
|
|
3122
|
+
[["test", "coverage", "spec", "assertion"], "test-gap"],
|
|
3123
|
+
[["security", "vulnerability", "auth", "cve"], "security"],
|
|
3124
|
+
[["design", "ui", "ux", "layout"], "design"],
|
|
3125
|
+
[["docs", "readme", "onboarding"], "documentation"]
|
|
3126
|
+
];
|
|
3127
|
+
SECONDARY_SCAN_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
3128
|
+
SECONDARY_VELOCITY_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
3129
|
+
PHASE_LABELS = {
|
|
3130
|
+
"health-check": "Health Check",
|
|
3131
|
+
"implementing": "Implementation",
|
|
3132
|
+
"review": "Review",
|
|
3133
|
+
"closing-sprint": "Sprint Abschluss",
|
|
3134
|
+
"planning": "Planung",
|
|
3135
|
+
"kickoff": "Kickoff",
|
|
3136
|
+
"solutioning": "Solutioning"
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
3140
|
+
|
|
2752
3141
|
// src/cli.ts
|
|
2753
3142
|
init_js_yaml();
|
|
2754
|
-
import
|
|
2755
|
-
import
|
|
2756
|
-
import { spawnSync as
|
|
2757
|
-
import { createInterface } from "node:readline";
|
|
3143
|
+
import fs39 from "node:fs";
|
|
3144
|
+
import path40 from "node:path";
|
|
3145
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
3146
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
2758
3147
|
|
|
2759
3148
|
// src/scanner.ts
|
|
2760
3149
|
import fs from "node:fs";
|
|
@@ -3837,19 +4226,24 @@ function pullRemoteSkills(forgehiveDir2, gitUrl) {
|
|
|
3837
4226
|
|
|
3838
4227
|
// src/party.ts
|
|
3839
4228
|
init_js_yaml();
|
|
3840
|
-
import
|
|
3841
|
-
import
|
|
4229
|
+
import fs14 from "node:fs";
|
|
4230
|
+
import path14 from "node:path";
|
|
3842
4231
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
4232
|
+
|
|
4233
|
+
// src/party-subagent.ts
|
|
4234
|
+
init_outcomes();
|
|
4235
|
+
|
|
4236
|
+
// src/party.ts
|
|
3843
4237
|
function loadConfig(forgehiveDir2) {
|
|
3844
|
-
const configPath =
|
|
3845
|
-
if (!
|
|
4238
|
+
const configPath = path14.join(forgehiveDir2, "party", "config.yaml");
|
|
4239
|
+
if (!fs14.existsSync(configPath)) {
|
|
3846
4240
|
throw new Error("party/config.yaml nicht gefunden \u2014 f\xFChre `fh init` aus");
|
|
3847
4241
|
}
|
|
3848
|
-
return jsYaml.load(
|
|
4242
|
+
return jsYaml.load(fs14.readFileSync(configPath, "utf8"));
|
|
3849
4243
|
}
|
|
3850
4244
|
function saveConfig(forgehiveDir2, config) {
|
|
3851
|
-
|
|
3852
|
-
|
|
4245
|
+
fs14.writeFileSync(
|
|
4246
|
+
path14.join(forgehiveDir2, "party", "config.yaml"),
|
|
3853
4247
|
jsYaml.dump(config),
|
|
3854
4248
|
"utf8"
|
|
3855
4249
|
);
|
|
@@ -3899,12 +4293,12 @@ function runParty(forgehiveDir2, projectRoot2, setName) {
|
|
|
3899
4293
|
);
|
|
3900
4294
|
}
|
|
3901
4295
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:-]/g, "");
|
|
3902
|
-
const wtBaseDir =
|
|
3903
|
-
|
|
4296
|
+
const wtBaseDir = path14.join(forgehiveDir2, "worktrees");
|
|
4297
|
+
fs14.mkdirSync(wtBaseDir, { recursive: true });
|
|
3904
4298
|
const state = { setName, sessionId, worktrees: [] };
|
|
3905
4299
|
for (const agent of config.sets[setName].agents) {
|
|
3906
4300
|
const branch = `forgehive/party/${sessionId}/${agent}`;
|
|
3907
|
-
const wtPath =
|
|
4301
|
+
const wtPath = path14.join(wtBaseDir, `${sessionId}-${agent}`);
|
|
3908
4302
|
const result = spawnSync3("git", ["worktree", "add", "-b", branch, wtPath], {
|
|
3909
4303
|
cwd: projectRoot2,
|
|
3910
4304
|
encoding: "utf8"
|
|
@@ -3916,8 +4310,8 @@ function runParty(forgehiveDir2, projectRoot2, setName) {
|
|
|
3916
4310
|
spawnSync3("git", ["worktree", "prune"], { cwd: projectRoot2, encoding: "utf8" });
|
|
3917
4311
|
throw result.error ?? new Error(`Worktree f\xFCr ${agent} fehlgeschlagen: ${result.stderr?.trim()}`);
|
|
3918
4312
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
4313
|
+
fs14.writeFileSync(
|
|
4314
|
+
path14.join(wtPath, ".forgehive-agent-dispatch.md"),
|
|
3921
4315
|
[
|
|
3922
4316
|
`# ${agent} \u2014 Party Session ${sessionId}`,
|
|
3923
4317
|
"",
|
|
@@ -3935,18 +4329,18 @@ function runParty(forgehiveDir2, projectRoot2, setName) {
|
|
|
3935
4329
|
);
|
|
3936
4330
|
state.worktrees.push({ agent, branch, path: wtPath, status: "pending" });
|
|
3937
4331
|
}
|
|
3938
|
-
|
|
3939
|
-
|
|
4332
|
+
fs14.writeFileSync(
|
|
4333
|
+
path14.join(forgehiveDir2, "party", "party-session.yaml"),
|
|
3940
4334
|
jsYaml.dump(state),
|
|
3941
4335
|
"utf8"
|
|
3942
4336
|
);
|
|
3943
4337
|
return state;
|
|
3944
4338
|
}
|
|
3945
4339
|
function getPartyStatus(forgehiveDir2) {
|
|
3946
|
-
const statePath =
|
|
3947
|
-
if (!
|
|
4340
|
+
const statePath = path14.join(forgehiveDir2, "party", "party-session.yaml");
|
|
4341
|
+
if (!fs14.existsSync(statePath)) return null;
|
|
3948
4342
|
try {
|
|
3949
|
-
return jsYaml.load(
|
|
4343
|
+
return jsYaml.load(fs14.readFileSync(statePath, "utf8"));
|
|
3950
4344
|
} catch {
|
|
3951
4345
|
return null;
|
|
3952
4346
|
}
|
|
@@ -3956,7 +4350,7 @@ function cleanupPartyWorktrees(forgehiveDir2, projectRoot2) {
|
|
|
3956
4350
|
if (!state) throw new Error("Keine aktive Party-Session gefunden");
|
|
3957
4351
|
const removed = [];
|
|
3958
4352
|
for (const wt of state.worktrees) {
|
|
3959
|
-
if (
|
|
4353
|
+
if (fs14.existsSync(wt.path)) {
|
|
3960
4354
|
const result = spawnSync3("git", ["worktree", "remove", "--force", wt.path], {
|
|
3961
4355
|
cwd: projectRoot2,
|
|
3962
4356
|
encoding: "utf8"
|
|
@@ -3965,8 +4359,8 @@ function cleanupPartyWorktrees(forgehiveDir2, projectRoot2) {
|
|
|
3965
4359
|
}
|
|
3966
4360
|
}
|
|
3967
4361
|
spawnSync3("git", ["worktree", "prune"], { cwd: projectRoot2, encoding: "utf8" });
|
|
3968
|
-
const statePath =
|
|
3969
|
-
if (
|
|
4362
|
+
const statePath = path14.join(forgehiveDir2, "party", "party-session.yaml");
|
|
4363
|
+
if (fs14.existsSync(statePath)) fs14.unlinkSync(statePath);
|
|
3970
4364
|
return removed;
|
|
3971
4365
|
}
|
|
3972
4366
|
function getPartyDefaultSet(forgehiveDir2) {
|
|
@@ -3989,8 +4383,8 @@ function setAgentModel(forgehiveDir2, setName, agentName, modelId) {
|
|
|
3989
4383
|
}
|
|
3990
4384
|
|
|
3991
4385
|
// src/wire.ts
|
|
3992
|
-
import
|
|
3993
|
-
import
|
|
4386
|
+
import fs15 from "node:fs";
|
|
4387
|
+
import path15 from "node:path";
|
|
3994
4388
|
var SERVICES = {
|
|
3995
4389
|
linear: {
|
|
3996
4390
|
mcp: {
|
|
@@ -4827,19 +5221,19 @@ function wireService(projectRoot2, forgehiveDir2, service) {
|
|
|
4827
5221
|
`Unbekannter Service: "${service}". Verf\xFCgbar: ${Object.keys(SERVICES).join(", ")}`
|
|
4828
5222
|
);
|
|
4829
5223
|
}
|
|
4830
|
-
const mcpJsonPath =
|
|
5224
|
+
const mcpJsonPath = path15.join(projectRoot2, ".mcp.json");
|
|
4831
5225
|
let mcpConfig = { mcpServers: {} };
|
|
4832
|
-
if (
|
|
4833
|
-
mcpConfig = JSON.parse(
|
|
5226
|
+
if (fs15.existsSync(mcpJsonPath)) {
|
|
5227
|
+
mcpConfig = JSON.parse(fs15.readFileSync(mcpJsonPath, "utf8"));
|
|
4834
5228
|
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
4835
5229
|
}
|
|
4836
5230
|
if (!mcpConfig.mcpServers[service]) {
|
|
4837
5231
|
mcpConfig.mcpServers[service] = def.mcp;
|
|
4838
|
-
|
|
5232
|
+
fs15.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf8");
|
|
4839
5233
|
}
|
|
4840
|
-
const skillPath =
|
|
4841
|
-
|
|
4842
|
-
|
|
5234
|
+
const skillPath = path15.join(forgehiveDir2, "skills", "workflows", `${service}.md`);
|
|
5235
|
+
fs15.mkdirSync(path15.dirname(skillPath), { recursive: true });
|
|
5236
|
+
fs15.writeFileSync(skillPath, def.skillContent, "utf8");
|
|
4843
5237
|
}
|
|
4844
5238
|
function validateWireService(service) {
|
|
4845
5239
|
if (!SERVICES[service]) {
|
|
@@ -4853,21 +5247,146 @@ function validateWireService(service) {
|
|
|
4853
5247
|
|
|
4854
5248
|
// src/status.ts
|
|
4855
5249
|
init_js_yaml();
|
|
4856
|
-
|
|
4857
|
-
import
|
|
5250
|
+
init_next();
|
|
5251
|
+
import fs17 from "node:fs";
|
|
5252
|
+
import path17 from "node:path";
|
|
4858
5253
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
4859
5254
|
function countMdFiles(dir) {
|
|
4860
|
-
if (!
|
|
4861
|
-
return
|
|
5255
|
+
if (!fs17.existsSync(dir)) return 0;
|
|
5256
|
+
return fs17.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
|
|
5257
|
+
}
|
|
5258
|
+
function readYamlFrontmatterFiles(dir) {
|
|
5259
|
+
if (!fs17.existsSync(dir)) return [];
|
|
5260
|
+
const results = [];
|
|
5261
|
+
for (const filename of fs17.readdirSync(dir)) {
|
|
5262
|
+
if (!filename.endsWith(".md")) continue;
|
|
5263
|
+
try {
|
|
5264
|
+
const content = fs17.readFileSync(path17.join(dir, filename), "utf8");
|
|
5265
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
5266
|
+
if (!match) continue;
|
|
5267
|
+
const parsed = jsYaml.load(match[1]);
|
|
5268
|
+
if (parsed && typeof parsed === "object") results.push(parsed);
|
|
5269
|
+
} catch {
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
return results;
|
|
5273
|
+
}
|
|
5274
|
+
function formatSprintSection(forgehiveDir2) {
|
|
5275
|
+
const storiesDir = path17.join(forgehiveDir2, "memory", "stories");
|
|
5276
|
+
if (!fs17.existsSync(storiesDir)) return "";
|
|
5277
|
+
const stories = readYamlFrontmatterFiles(storiesDir);
|
|
5278
|
+
const open = stories.filter((s) => s.status !== "done");
|
|
5279
|
+
const inSprint = stories.filter((s) => s.status === "in-sprint");
|
|
5280
|
+
const next = inSprint[0] ?? open[0] ?? null;
|
|
5281
|
+
const epicsDir = path17.join(forgehiveDir2, "memory", "epics");
|
|
5282
|
+
const activeEpics = readYamlFrontmatterFiles(epicsDir).filter((e) => e.status === "active" || e.status === "in-progress");
|
|
5283
|
+
const lines = ["SPRINT"];
|
|
5284
|
+
lines.push(` \u25CF ${open.length} offen \u25CF ${inSprint.length} in-sprint`);
|
|
5285
|
+
if (activeEpics.length > 0) {
|
|
5286
|
+
lines.push(` Epic: ${activeEpics[0].id} ${activeEpics[0].title}`);
|
|
5287
|
+
}
|
|
5288
|
+
if (next) {
|
|
5289
|
+
const pts = next.points ? ` [${next.points} pts]` : "";
|
|
5290
|
+
lines.push(` \u2192 n\xE4chstes: ${next.id} "${next.title}"${pts}`);
|
|
5291
|
+
}
|
|
5292
|
+
return lines.join("\n");
|
|
5293
|
+
}
|
|
5294
|
+
function formatCodeHealthSection(projectRoot2, forgehiveDir2) {
|
|
5295
|
+
const lines = ["CODE HEALTH"];
|
|
5296
|
+
const gitResult = spawnSync4("git", ["status", "--porcelain"], {
|
|
5297
|
+
cwd: projectRoot2,
|
|
5298
|
+
encoding: "utf8"
|
|
5299
|
+
});
|
|
5300
|
+
if (gitResult.status === 0) {
|
|
5301
|
+
const changed = gitResult.stdout.trim().split("\n").filter(Boolean).length;
|
|
5302
|
+
lines.push(
|
|
5303
|
+
changed === 0 ? " \u2713 Arbeitsbaum sauber" : ` \u26A0 ${changed} ge\xE4nderte Datei(en) \u2014 nicht committed`
|
|
5304
|
+
);
|
|
5305
|
+
}
|
|
5306
|
+
const secPath = path17.join(forgehiveDir2, "security-last-scan.txt");
|
|
5307
|
+
if (fs17.existsSync(secPath)) {
|
|
5308
|
+
try {
|
|
5309
|
+
const ts = fs17.readFileSync(secPath, "utf8").trim();
|
|
5310
|
+
const days = Math.floor((Date.now() - new Date(ts).getTime()) / 864e5);
|
|
5311
|
+
lines.push(
|
|
5312
|
+
days > 7 ? ` \u26A0 Security-Scan: ${days} Tage alt \u2014 fh security scan` : ` \u2713 Security-Scan: ${days} Tage alt`
|
|
5313
|
+
);
|
|
5314
|
+
} catch {
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
const prCachePath = path17.join(forgehiveDir2, "github-pr-cache.json");
|
|
5318
|
+
if (fs17.existsSync(prCachePath)) {
|
|
5319
|
+
try {
|
|
5320
|
+
const cache = JSON.parse(fs17.readFileSync(prCachePath, "utf8"));
|
|
5321
|
+
if (cache.open.length > 0) {
|
|
5322
|
+
lines.push(` \u25CF ${cache.open.length} offene PR(s): #${cache.open.join(", #")}`);
|
|
5323
|
+
}
|
|
5324
|
+
} catch {
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
const outcomesPath = path17.join(forgehiveDir2, "outcomes.jsonl");
|
|
5328
|
+
if (fs17.existsSync(outcomesPath)) {
|
|
5329
|
+
try {
|
|
5330
|
+
const rawLines = fs17.readFileSync(outcomesPath, "utf8").trim().split("\n").filter(Boolean);
|
|
5331
|
+
const lastParty = rawLines.map((l) => {
|
|
5332
|
+
try {
|
|
5333
|
+
return JSON.parse(l);
|
|
5334
|
+
} catch {
|
|
5335
|
+
return null;
|
|
5336
|
+
}
|
|
5337
|
+
}).filter((e) => e !== null && e.type === "party").pop();
|
|
5338
|
+
if (lastParty) {
|
|
5339
|
+
const days = Math.floor((Date.now() - new Date(lastParty.ts).getTime()) / 864e5);
|
|
5340
|
+
const set2 = lastParty.partySet ?? "party";
|
|
5341
|
+
const findings = lastParty.signals?.findingsCount ?? 0;
|
|
5342
|
+
lines.push(` \u25CF Letzter Review (${set2}): vor ${days} Tag(en) \u2014 ${findings} Findings`);
|
|
5343
|
+
}
|
|
5344
|
+
} catch {
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
return lines.join("\n");
|
|
5348
|
+
}
|
|
5349
|
+
function formatWatchSection(forgehiveDir2) {
|
|
5350
|
+
const logPath = path17.join(forgehiveDir2, "watch-events.jsonl");
|
|
5351
|
+
if (!fs17.existsSync(logPath)) return "";
|
|
5352
|
+
const events = fs17.readFileSync(logPath, "utf8").trim().split("\n").filter(Boolean).map((line) => {
|
|
5353
|
+
try {
|
|
5354
|
+
return JSON.parse(line);
|
|
5355
|
+
} catch {
|
|
5356
|
+
return null;
|
|
5357
|
+
}
|
|
5358
|
+
}).filter((e) => e !== null).slice(-5).reverse();
|
|
5359
|
+
if (events.length === 0) return "";
|
|
5360
|
+
const lines = ["WATCH (letzte Ereignisse)"];
|
|
5361
|
+
for (const e of events) {
|
|
5362
|
+
const time = new Date(e.ts).toLocaleTimeString("de", { hour: "2-digit", minute: "2-digit" });
|
|
5363
|
+
if (e.type === "stack-update") {
|
|
5364
|
+
lines.push(` [${time}] \u2713 Stack-Update (${e.file ?? "?"}) \u2014 capabilities.yaml aktualisiert`);
|
|
5365
|
+
} else {
|
|
5366
|
+
const verdict = e.accepted === true ? "\u2192 gestartet" : e.accepted === false ? "\u2192 abgelehnt" : "\u2192 vorgeschlagen";
|
|
5367
|
+
lines.push(` [${time}] \u26A0 ${e.file ?? e.type} ${verdict}: ${e.agent ?? "?"}`);
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
return lines.join("\n");
|
|
5371
|
+
}
|
|
5372
|
+
function formatDashboard(projectRoot2, forgehiveDir2) {
|
|
5373
|
+
const sections = [
|
|
5374
|
+
formatSprintSection(forgehiveDir2),
|
|
5375
|
+
formatCodeHealthSection(projectRoot2, forgehiveDir2),
|
|
5376
|
+
formatWatchSection(forgehiveDir2),
|
|
5377
|
+
formatNextCompact(projectRoot2, forgehiveDir2)
|
|
5378
|
+
].filter(Boolean);
|
|
5379
|
+
if (sections.length === 0) return "";
|
|
5380
|
+
return "\n" + sections.join("\n\n");
|
|
4862
5381
|
}
|
|
4863
5382
|
function checkDrift(projectRoot2, forgehiveDir2) {
|
|
4864
|
-
const scanPath =
|
|
4865
|
-
if (!
|
|
5383
|
+
const scanPath = path17.join(forgehiveDir2, "scan-result.yaml");
|
|
5384
|
+
if (!fs17.existsSync(scanPath)) {
|
|
4866
5385
|
return { daysSinceScan: null, recentCommits: 0, isDrifted: false };
|
|
4867
5386
|
}
|
|
4868
5387
|
let scannedAt = null;
|
|
4869
5388
|
try {
|
|
4870
|
-
const raw = jsYaml.load(
|
|
5389
|
+
const raw = jsYaml.load(fs17.readFileSync(scanPath, "utf8"));
|
|
4871
5390
|
scannedAt = raw?.scanned_at ?? null;
|
|
4872
5391
|
} catch {
|
|
4873
5392
|
}
|
|
@@ -4890,14 +5409,14 @@ function checkDrift(projectRoot2, forgehiveDir2) {
|
|
|
4890
5409
|
}
|
|
4891
5410
|
function projectStatus(projectRoot2, forgehiveDir2) {
|
|
4892
5411
|
const lines = ["ForgeHive Status", ""];
|
|
4893
|
-
if (!
|
|
5412
|
+
if (!fs17.existsSync(forgehiveDir2)) {
|
|
4894
5413
|
lines.push("\u2717 Not initialized \u2014 run: fh init");
|
|
4895
5414
|
return lines.join("\n");
|
|
4896
5415
|
}
|
|
4897
|
-
const capsPath =
|
|
4898
|
-
if (
|
|
5416
|
+
const capsPath = path17.join(forgehiveDir2, "capabilities.yaml");
|
|
5417
|
+
if (fs17.existsSync(capsPath)) {
|
|
4899
5418
|
try {
|
|
4900
|
-
const raw = jsYaml.load(
|
|
5419
|
+
const raw = jsYaml.load(fs17.readFileSync(capsPath, "utf8"));
|
|
4901
5420
|
const caps = raw && typeof raw === "object" ? raw : {};
|
|
4902
5421
|
const confirmed = caps.capabilities?.confirmed?.length ?? 0;
|
|
4903
5422
|
const inferred = caps.capabilities?.inferred?.length ?? 0;
|
|
@@ -4908,10 +5427,10 @@ function projectStatus(projectRoot2, forgehiveDir2) {
|
|
|
4908
5427
|
lines.push("\u26A0 Capabilities: capabilities.yaml unlesbar");
|
|
4909
5428
|
}
|
|
4910
5429
|
}
|
|
4911
|
-
const claudeMdPath =
|
|
4912
|
-
if (
|
|
5430
|
+
const claudeMdPath = path17.join(projectRoot2, "CLAUDE.md");
|
|
5431
|
+
if (fs17.existsSync(claudeMdPath)) {
|
|
4913
5432
|
try {
|
|
4914
|
-
const content =
|
|
5433
|
+
const content = fs17.readFileSync(claudeMdPath, "utf8");
|
|
4915
5434
|
const hasBlock = content.includes("<!-- forgehive:start -->");
|
|
4916
5435
|
lines.push(hasBlock ? "\u2713 CLAUDE.md block present" : "\u26A0 CLAUDE.md block missing \u2014 run: fh init");
|
|
4917
5436
|
} catch {
|
|
@@ -4920,15 +5439,15 @@ function projectStatus(projectRoot2, forgehiveDir2) {
|
|
|
4920
5439
|
} else {
|
|
4921
5440
|
lines.push("\u26A0 CLAUDE.md not found");
|
|
4922
5441
|
}
|
|
4923
|
-
const skillsDir =
|
|
4924
|
-
const generated = countMdFiles(
|
|
4925
|
-
const expert = countMdFiles(
|
|
4926
|
-
const workflows = countMdFiles(
|
|
5442
|
+
const skillsDir = path17.join(forgehiveDir2, "skills");
|
|
5443
|
+
const generated = countMdFiles(path17.join(skillsDir, "generated"));
|
|
5444
|
+
const expert = countMdFiles(path17.join(skillsDir, "expert"));
|
|
5445
|
+
const workflows = countMdFiles(path17.join(skillsDir, "workflows"));
|
|
4927
5446
|
lines.push(`\u2713 Skills: ${generated} generated, ${expert} expert, ${workflows} workflows`);
|
|
4928
|
-
const mcpPath =
|
|
4929
|
-
if (
|
|
5447
|
+
const mcpPath = path17.join(projectRoot2, ".mcp.json");
|
|
5448
|
+
if (fs17.existsSync(mcpPath)) {
|
|
4930
5449
|
try {
|
|
4931
|
-
const mcp = JSON.parse(
|
|
5450
|
+
const mcp = JSON.parse(fs17.readFileSync(mcpPath, "utf8"));
|
|
4932
5451
|
const services = Object.keys(mcp.mcpServers ?? {});
|
|
4933
5452
|
lines.push(
|
|
4934
5453
|
services.length > 0 ? `\u2713 MCP: ${services.join(", ")}` : " MCP: none configured"
|
|
@@ -4939,17 +5458,17 @@ function projectStatus(projectRoot2, forgehiveDir2) {
|
|
|
4939
5458
|
} else {
|
|
4940
5459
|
lines.push(" MCP: none configured (fh wire <service>)");
|
|
4941
5460
|
}
|
|
4942
|
-
const memoryDir =
|
|
4943
|
-
if (
|
|
4944
|
-
const files =
|
|
5461
|
+
const memoryDir = path17.join(forgehiveDir2, "memory");
|
|
5462
|
+
if (fs17.existsSync(memoryDir)) {
|
|
5463
|
+
const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md"));
|
|
4945
5464
|
lines.push(`\u2713 Memory: ${files.length} file(s)`);
|
|
4946
5465
|
} else {
|
|
4947
5466
|
lines.push(" Memory: not initialized");
|
|
4948
5467
|
}
|
|
4949
|
-
const scanPath =
|
|
4950
|
-
if (
|
|
5468
|
+
const scanPath = path17.join(forgehiveDir2, "scan-result.yaml");
|
|
5469
|
+
if (fs17.existsSync(scanPath)) {
|
|
4951
5470
|
try {
|
|
4952
|
-
const raw = jsYaml.load(
|
|
5471
|
+
const raw = jsYaml.load(fs17.readFileSync(scanPath, "utf8"));
|
|
4953
5472
|
const scan2 = raw && typeof raw === "object" ? raw : {};
|
|
4954
5473
|
lines.push(`\u2713 Last scan: ${scan2.scanned_at ?? "unknown"}`);
|
|
4955
5474
|
} catch {
|
|
@@ -4963,23 +5482,24 @@ function projectStatus(projectRoot2, forgehiveDir2) {
|
|
|
4963
5482
|
} else if (drift.daysSinceScan !== null) {
|
|
4964
5483
|
lines.push(`\u2713 Kontext aktuell (${drift.daysSinceScan} Tage, ${drift.recentCommits} Commits seit Scan)`);
|
|
4965
5484
|
}
|
|
4966
|
-
const agentMemDir =
|
|
4967
|
-
if (
|
|
4968
|
-
const agents =
|
|
5485
|
+
const agentMemDir = path17.join(forgehiveDir2, "agents", "memory");
|
|
5486
|
+
if (fs17.existsSync(agentMemDir)) {
|
|
5487
|
+
const agents = fs17.readdirSync(agentMemDir).filter((f) => f.endsWith(".md"));
|
|
4969
5488
|
lines.push(`\u2713 Agent memory: ${agents.length} agent(s)`);
|
|
4970
5489
|
}
|
|
4971
5490
|
return lines.join("\n");
|
|
4972
5491
|
}
|
|
4973
5492
|
|
|
4974
5493
|
// src/watch.ts
|
|
4975
|
-
import
|
|
4976
|
-
import
|
|
5494
|
+
import fs18 from "node:fs";
|
|
5495
|
+
import path18 from "node:path";
|
|
5496
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
4977
5497
|
function checkProjectHash(projectRoot2, forgehiveDir2, claudeMdBlock) {
|
|
4978
|
-
const hashPath =
|
|
4979
|
-
const savedHash =
|
|
5498
|
+
const hashPath = path18.join(forgehiveDir2, ".scan-hash");
|
|
5499
|
+
const savedHash = fs18.existsSync(hashPath) ? fs18.readFileSync(hashPath, "utf8").trim() : null;
|
|
4980
5500
|
const currentHash = computeHash(projectRoot2);
|
|
4981
5501
|
if (currentHash === savedHash) return false;
|
|
4982
|
-
|
|
5502
|
+
fs18.writeFileSync(hashPath, currentHash, "utf8");
|
|
4983
5503
|
const scanResult = scan(projectRoot2);
|
|
4984
5504
|
const capMap = mapSignalsToCapabilities(scanResult);
|
|
4985
5505
|
writeForgehiveDir(projectRoot2, scanResult, capMap, claudeMdBlock);
|
|
@@ -4996,7 +5516,7 @@ function watchProject(projectRoot2, forgehiveDir2, claudeMdBlock, onUpdate = def
|
|
|
4996
5516
|
"go.mod",
|
|
4997
5517
|
"pyproject.toml",
|
|
4998
5518
|
"composer.json"
|
|
4999
|
-
].map((f) =>
|
|
5519
|
+
].map((f) => path18.join(projectRoot2, f)).filter((f) => fs18.existsSync(f));
|
|
5000
5520
|
let debounce = null;
|
|
5001
5521
|
const watchers = [];
|
|
5002
5522
|
function schedule() {
|
|
@@ -5008,12 +5528,12 @@ function watchProject(projectRoot2, forgehiveDir2, claudeMdBlock, onUpdate = def
|
|
|
5008
5528
|
}
|
|
5009
5529
|
for (const f of candidates) {
|
|
5010
5530
|
try {
|
|
5011
|
-
watchers.push(
|
|
5531
|
+
watchers.push(fs18.watch(f, schedule));
|
|
5012
5532
|
} catch {
|
|
5013
5533
|
}
|
|
5014
5534
|
}
|
|
5015
5535
|
try {
|
|
5016
|
-
watchers.push(
|
|
5536
|
+
watchers.push(fs18.watch(projectRoot2, schedule));
|
|
5017
5537
|
} catch {
|
|
5018
5538
|
}
|
|
5019
5539
|
return () => {
|
|
@@ -5031,10 +5551,95 @@ function defaultOnUpdate(changed) {
|
|
|
5031
5551
|
console.log("[forgehive] \u2713 Codebase changed \u2014 capabilities.yaml aktualisiert");
|
|
5032
5552
|
}
|
|
5033
5553
|
}
|
|
5554
|
+
function appendWatchEvent(forgehiveDir2, event) {
|
|
5555
|
+
const logPath = path18.join(forgehiveDir2, "watch-events.jsonl");
|
|
5556
|
+
fs18.appendFileSync(logPath, JSON.stringify(event) + "\n", "utf8");
|
|
5557
|
+
try {
|
|
5558
|
+
const stat = fs18.statSync(logPath);
|
|
5559
|
+
if (stat.size > 25e3) {
|
|
5560
|
+
const lines = fs18.readFileSync(logPath, "utf8").trim().split("\n").filter(Boolean);
|
|
5561
|
+
if (lines.length >= 500) {
|
|
5562
|
+
fs18.writeFileSync(logPath, lines.slice(-500).join("\n") + "\n", "utf8");
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
} catch {
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
function detectErrorType(output) {
|
|
5569
|
+
const lower = output.toLowerCase();
|
|
5570
|
+
if (lower.includes("failing") || lower.includes("assertionerror") || lower.includes("not ok")) {
|
|
5571
|
+
return { agent: "sam", reason: "Test-Fehler erkannt" };
|
|
5572
|
+
}
|
|
5573
|
+
if (lower.includes("error ts") || lower.includes("type error")) {
|
|
5574
|
+
return { agent: "kai", reason: "TypeScript-Fehler erkannt" };
|
|
5575
|
+
}
|
|
5576
|
+
if (lower.includes("secret") || lower.includes("vulnerability") || lower.includes("cve")) {
|
|
5577
|
+
return { agent: "vera", reason: "Sicherheitsproblem erkannt" };
|
|
5578
|
+
}
|
|
5579
|
+
if (lower.includes("error") || lower.includes("failed to compile")) {
|
|
5580
|
+
return { agent: "kai", reason: "Build-Fehler erkannt" };
|
|
5581
|
+
}
|
|
5582
|
+
return null;
|
|
5583
|
+
}
|
|
5584
|
+
function smartWatch(projectRoot2, forgehiveDir2, testCmd, onSuggest) {
|
|
5585
|
+
const srcDir = path18.join(projectRoot2, "src");
|
|
5586
|
+
const testDir = path18.join(projectRoot2, "test");
|
|
5587
|
+
const watchDirs = [srcDir, testDir].filter((d) => fs18.existsSync(d));
|
|
5588
|
+
let debounce = null;
|
|
5589
|
+
const watchers = [];
|
|
5590
|
+
function runAndAnalyze(changedFile) {
|
|
5591
|
+
const result = spawnSync5(testCmd, {
|
|
5592
|
+
cwd: projectRoot2,
|
|
5593
|
+
encoding: "utf8",
|
|
5594
|
+
shell: true
|
|
5595
|
+
});
|
|
5596
|
+
if (result.status === 0) return;
|
|
5597
|
+
const output = (result.stdout ?? "") + (result.stderr ?? "");
|
|
5598
|
+
const detection = detectErrorType(output);
|
|
5599
|
+
if (!detection) return;
|
|
5600
|
+
const relFile = path18.relative(projectRoot2, changedFile);
|
|
5601
|
+
console.log(`
|
|
5602
|
+
\u26A0 ${relFile} \u2014 ${detection.reason}`);
|
|
5603
|
+
console.log(` \u2192 ${detection.agent} ist der richtige Agent.`);
|
|
5604
|
+
void onSuggest(detection.agent, detection.reason, relFile).then((accepted) => {
|
|
5605
|
+
appendWatchEvent(forgehiveDir2, {
|
|
5606
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5607
|
+
type: "test-failure",
|
|
5608
|
+
file: relFile,
|
|
5609
|
+
agent: detection.agent,
|
|
5610
|
+
accepted
|
|
5611
|
+
});
|
|
5612
|
+
});
|
|
5613
|
+
}
|
|
5614
|
+
function schedule(changedFile) {
|
|
5615
|
+
if (debounce) clearTimeout(debounce);
|
|
5616
|
+
debounce = setTimeout(() => runAndAnalyze(changedFile), 1500);
|
|
5617
|
+
}
|
|
5618
|
+
for (const dir of watchDirs) {
|
|
5619
|
+
try {
|
|
5620
|
+
const watcher = fs18.watch(dir, { recursive: true }, (_evt, filename) => {
|
|
5621
|
+
if (filename && (filename.endsWith(".ts") || filename.endsWith(".js"))) {
|
|
5622
|
+
schedule(path18.join(dir, filename));
|
|
5623
|
+
}
|
|
5624
|
+
});
|
|
5625
|
+
watchers.push(watcher);
|
|
5626
|
+
} catch {
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
return () => {
|
|
5630
|
+
if (debounce) clearTimeout(debounce);
|
|
5631
|
+
for (const w of watchers) {
|
|
5632
|
+
try {
|
|
5633
|
+
w.close();
|
|
5634
|
+
} catch {
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
};
|
|
5638
|
+
}
|
|
5034
5639
|
|
|
5035
5640
|
// src/cost.ts
|
|
5036
|
-
import
|
|
5037
|
-
import
|
|
5641
|
+
import fs19 from "node:fs";
|
|
5642
|
+
import path19 from "node:path";
|
|
5038
5643
|
import os2 from "node:os";
|
|
5039
5644
|
var PRICING = {
|
|
5040
5645
|
input: 3,
|
|
@@ -5045,31 +5650,31 @@ var PRICING = {
|
|
|
5045
5650
|
function calcCost(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
|
|
5046
5651
|
return inputTokens / 1e6 * PRICING.input + outputTokens / 1e6 * PRICING.output + cacheCreationTokens / 1e6 * PRICING.cacheCreation + cacheReadTokens / 1e6 * PRICING.cacheRead;
|
|
5047
5652
|
}
|
|
5048
|
-
function parseCostSessions(_projectRoot, claudeHome =
|
|
5049
|
-
const projectsDir =
|
|
5050
|
-
if (!
|
|
5051
|
-
const matchDirs =
|
|
5653
|
+
function parseCostSessions(_projectRoot, claudeHome = path19.join(os2.homedir(), ".claude")) {
|
|
5654
|
+
const projectsDir = path19.join(claudeHome, "projects");
|
|
5655
|
+
if (!fs19.existsSync(projectsDir)) return [];
|
|
5656
|
+
const matchDirs = fs19.readdirSync(projectsDir);
|
|
5052
5657
|
const sessions = [];
|
|
5053
5658
|
for (const dir of matchDirs) {
|
|
5054
|
-
const dirPath =
|
|
5659
|
+
const dirPath = path19.join(projectsDir, dir);
|
|
5055
5660
|
let stat;
|
|
5056
5661
|
try {
|
|
5057
|
-
stat =
|
|
5662
|
+
stat = fs19.statSync(dirPath);
|
|
5058
5663
|
} catch {
|
|
5059
5664
|
continue;
|
|
5060
5665
|
}
|
|
5061
5666
|
if (!stat.isDirectory()) continue;
|
|
5062
5667
|
let files;
|
|
5063
5668
|
try {
|
|
5064
|
-
files =
|
|
5669
|
+
files = fs19.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
5065
5670
|
} catch {
|
|
5066
5671
|
continue;
|
|
5067
5672
|
}
|
|
5068
5673
|
for (const jsonlFile of files) {
|
|
5069
|
-
const filePath =
|
|
5674
|
+
const filePath = path19.join(dirPath, jsonlFile);
|
|
5070
5675
|
let inputTokens = 0, outputTokens = 0, cacheCreationTokens = 0, cacheReadTokens = 0;
|
|
5071
5676
|
try {
|
|
5072
|
-
const lines =
|
|
5677
|
+
const lines = fs19.readFileSync(filePath, "utf8").split("\n").filter(Boolean);
|
|
5073
5678
|
for (const line of lines) {
|
|
5074
5679
|
try {
|
|
5075
5680
|
const entry = JSON.parse(line);
|
|
@@ -5082,7 +5687,7 @@ function parseCostSessions(_projectRoot, claudeHome = path17.join(os2.homedir(),
|
|
|
5082
5687
|
} catch {
|
|
5083
5688
|
}
|
|
5084
5689
|
}
|
|
5085
|
-
const mtime =
|
|
5690
|
+
const mtime = fs19.statSync(filePath).mtime;
|
|
5086
5691
|
sessions.push({
|
|
5087
5692
|
sessionFile: jsonlFile,
|
|
5088
5693
|
date: mtime.toISOString().slice(0, 10),
|
|
@@ -5130,28 +5735,28 @@ function formatCostReport(sessions, range) {
|
|
|
5130
5735
|
}
|
|
5131
5736
|
|
|
5132
5737
|
// src/mcp-auth.ts
|
|
5133
|
-
import
|
|
5134
|
-
import
|
|
5738
|
+
import fs20 from "node:fs";
|
|
5739
|
+
import path20 from "node:path";
|
|
5135
5740
|
import os3 from "node:os";
|
|
5136
5741
|
function getCredsPath() {
|
|
5137
|
-
const dir =
|
|
5138
|
-
|
|
5139
|
-
return
|
|
5742
|
+
const dir = path20.join(os3.homedir(), ".forgehive");
|
|
5743
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
5744
|
+
return path20.join(dir, "credentials.json");
|
|
5140
5745
|
}
|
|
5141
5746
|
function loadStore() {
|
|
5142
5747
|
const p = getCredsPath();
|
|
5143
|
-
if (!
|
|
5748
|
+
if (!fs20.existsSync(p)) return {};
|
|
5144
5749
|
try {
|
|
5145
|
-
return JSON.parse(
|
|
5750
|
+
return JSON.parse(fs20.readFileSync(p, "utf8"));
|
|
5146
5751
|
} catch {
|
|
5147
5752
|
return {};
|
|
5148
5753
|
}
|
|
5149
5754
|
}
|
|
5150
5755
|
function saveStore(store) {
|
|
5151
5756
|
const p = getCredsPath();
|
|
5152
|
-
|
|
5757
|
+
fs20.writeFileSync(p, JSON.stringify(store, null, 2), "utf8");
|
|
5153
5758
|
try {
|
|
5154
|
-
|
|
5759
|
+
fs20.chmodSync(p, 384);
|
|
5155
5760
|
} catch {
|
|
5156
5761
|
}
|
|
5157
5762
|
}
|
|
@@ -5198,9 +5803,9 @@ function checkMcpTrust(packageName) {
|
|
|
5198
5803
|
}
|
|
5199
5804
|
|
|
5200
5805
|
// src/mcp-registry.ts
|
|
5201
|
-
import
|
|
5202
|
-
import
|
|
5203
|
-
import { spawnSync as
|
|
5806
|
+
import fs21 from "node:fs";
|
|
5807
|
+
import path21 from "node:path";
|
|
5808
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5204
5809
|
function parseRegistryResponse(raw) {
|
|
5205
5810
|
if (!raw || typeof raw !== "object") return [];
|
|
5206
5811
|
const data = raw;
|
|
@@ -5214,7 +5819,7 @@ function parseRegistryResponse(raw) {
|
|
|
5214
5819
|
}
|
|
5215
5820
|
function searchRegistry(query) {
|
|
5216
5821
|
const url = `https://registry.smithery.ai/servers?q=${encodeURIComponent(query)}&pageSize=10`;
|
|
5217
|
-
const result =
|
|
5822
|
+
const result = spawnSync6("curl", [
|
|
5218
5823
|
"-s",
|
|
5219
5824
|
"--max-time",
|
|
5220
5825
|
"10",
|
|
@@ -5236,11 +5841,11 @@ function searchRegistry(query) {
|
|
|
5236
5841
|
}
|
|
5237
5842
|
function addMcpFromRegistry(projectRoot2, forgehiveDir2, packageName, envKeys) {
|
|
5238
5843
|
const serverId = packageName.replace(/^@[^/]+\//, "") || packageName;
|
|
5239
|
-
const mcpPath =
|
|
5844
|
+
const mcpPath = path21.join(projectRoot2, ".mcp.json");
|
|
5240
5845
|
let mcpConfig = { mcpServers: {} };
|
|
5241
|
-
if (
|
|
5846
|
+
if (fs21.existsSync(mcpPath)) {
|
|
5242
5847
|
try {
|
|
5243
|
-
mcpConfig = JSON.parse(
|
|
5848
|
+
mcpConfig = JSON.parse(fs21.readFileSync(mcpPath, "utf8"));
|
|
5244
5849
|
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
5245
5850
|
} catch {
|
|
5246
5851
|
mcpConfig = { mcpServers: {} };
|
|
@@ -5255,12 +5860,12 @@ function addMcpFromRegistry(projectRoot2, forgehiveDir2, packageName, envKeys) {
|
|
|
5255
5860
|
args: ["-y", packageName],
|
|
5256
5861
|
...envKeys.length > 0 ? { env: envObj } : {}
|
|
5257
5862
|
};
|
|
5258
|
-
|
|
5259
|
-
const skillDir =
|
|
5260
|
-
|
|
5261
|
-
const skillPath =
|
|
5262
|
-
if (!
|
|
5263
|
-
|
|
5863
|
+
fs21.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
5864
|
+
const skillDir = path21.join(forgehiveDir2, "skills", "workflows");
|
|
5865
|
+
fs21.mkdirSync(skillDir, { recursive: true });
|
|
5866
|
+
const skillPath = path21.join(skillDir, `${serverId}.md`);
|
|
5867
|
+
if (!fs21.existsSync(skillPath)) {
|
|
5868
|
+
fs21.writeFileSync(skillPath, [
|
|
5264
5869
|
`# ${serverId} Workflow Skill`,
|
|
5265
5870
|
"",
|
|
5266
5871
|
`## Setup`,
|
|
@@ -5275,9 +5880,9 @@ function addMcpFromRegistry(projectRoot2, forgehiveDir2, packageName, envKeys) {
|
|
|
5275
5880
|
}
|
|
5276
5881
|
|
|
5277
5882
|
// src/security-scan.ts
|
|
5278
|
-
import
|
|
5279
|
-
import
|
|
5280
|
-
import { spawnSync as
|
|
5883
|
+
import fs22 from "node:fs";
|
|
5884
|
+
import path22 from "node:path";
|
|
5885
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5281
5886
|
var SECRET_PATTERNS = [
|
|
5282
5887
|
{ name: "Anthropic Key", pattern: /sk-ant-[a-zA-Z0-9-]{20,}/g },
|
|
5283
5888
|
{ name: "OpenAI Key", pattern: /sk-(?!ant-)[a-zA-Z0-9]{20,}/g },
|
|
@@ -5295,16 +5900,16 @@ function collectScanFiles(dir, exts = SCAN_EXTS) {
|
|
|
5295
5900
|
function walk(current) {
|
|
5296
5901
|
let entries;
|
|
5297
5902
|
try {
|
|
5298
|
-
entries =
|
|
5903
|
+
entries = fs22.readdirSync(current, { withFileTypes: true });
|
|
5299
5904
|
} catch {
|
|
5300
5905
|
return;
|
|
5301
5906
|
}
|
|
5302
5907
|
for (const entry of entries) {
|
|
5303
5908
|
if (IGNORE_DIRS.includes(entry.name)) continue;
|
|
5304
|
-
const full =
|
|
5909
|
+
const full = path22.join(current, entry.name);
|
|
5305
5910
|
if (entry.isDirectory()) {
|
|
5306
5911
|
walk(full);
|
|
5307
|
-
} else if (entry.isFile() && exts.includes(
|
|
5912
|
+
} else if (entry.isFile() && exts.includes(path22.extname(entry.name))) {
|
|
5308
5913
|
results.push(full);
|
|
5309
5914
|
}
|
|
5310
5915
|
}
|
|
@@ -5318,7 +5923,7 @@ function scanSecrets(projectRoot2) {
|
|
|
5318
5923
|
for (const file of files) {
|
|
5319
5924
|
let content;
|
|
5320
5925
|
try {
|
|
5321
|
-
content =
|
|
5926
|
+
content = fs22.readFileSync(file, "utf8");
|
|
5322
5927
|
} catch {
|
|
5323
5928
|
continue;
|
|
5324
5929
|
}
|
|
@@ -5385,7 +5990,7 @@ function scanSast(projectRoot2) {
|
|
|
5385
5990
|
for (const file of files) {
|
|
5386
5991
|
let content;
|
|
5387
5992
|
try {
|
|
5388
|
-
content =
|
|
5993
|
+
content = fs22.readFileSync(file, "utf8");
|
|
5389
5994
|
} catch {
|
|
5390
5995
|
continue;
|
|
5391
5996
|
}
|
|
@@ -5410,9 +6015,9 @@ function scanSast(projectRoot2) {
|
|
|
5410
6015
|
return findings;
|
|
5411
6016
|
}
|
|
5412
6017
|
function scanDeps(projectRoot2) {
|
|
5413
|
-
const pkgJsonPath =
|
|
5414
|
-
if (!
|
|
5415
|
-
const result =
|
|
6018
|
+
const pkgJsonPath = path22.join(projectRoot2, "package.json");
|
|
6019
|
+
if (!fs22.existsSync(pkgJsonPath)) return [];
|
|
6020
|
+
const result = spawnSync7("npm", ["audit", "--json"], {
|
|
5416
6021
|
cwd: projectRoot2,
|
|
5417
6022
|
encoding: "utf8",
|
|
5418
6023
|
timeout: 3e4
|
|
@@ -5440,19 +6045,19 @@ function scanDeps(projectRoot2) {
|
|
|
5440
6045
|
}
|
|
5441
6046
|
|
|
5442
6047
|
// src/audit.ts
|
|
5443
|
-
import
|
|
5444
|
-
import
|
|
6048
|
+
import fs23 from "node:fs";
|
|
6049
|
+
import path23 from "node:path";
|
|
5445
6050
|
function getAuditPath(forgehiveDir2) {
|
|
5446
|
-
return
|
|
6051
|
+
return path23.join(forgehiveDir2, "audit.log");
|
|
5447
6052
|
}
|
|
5448
6053
|
function logAuditEvent(forgehiveDir2, event) {
|
|
5449
6054
|
const line = JSON.stringify(event) + "\n";
|
|
5450
|
-
|
|
6055
|
+
fs23.appendFileSync(getAuditPath(forgehiveDir2), line, "utf8");
|
|
5451
6056
|
}
|
|
5452
6057
|
function getAuditLog(forgehiveDir2, limit = 50) {
|
|
5453
6058
|
const p = getAuditPath(forgehiveDir2);
|
|
5454
|
-
if (!
|
|
5455
|
-
const lines =
|
|
6059
|
+
if (!fs23.existsSync(p)) return [];
|
|
6060
|
+
const lines = fs23.readFileSync(p, "utf8").trim().split("\n").filter(Boolean);
|
|
5456
6061
|
const recent = lines.slice(-limit);
|
|
5457
6062
|
return recent.map((l) => {
|
|
5458
6063
|
try {
|
|
@@ -5477,19 +6082,19 @@ function formatAuditReport(events) {
|
|
|
5477
6082
|
|
|
5478
6083
|
// src/spend-limits.ts
|
|
5479
6084
|
init_js_yaml();
|
|
5480
|
-
import
|
|
5481
|
-
import
|
|
6085
|
+
import fs24 from "node:fs";
|
|
6086
|
+
import path24 from "node:path";
|
|
5482
6087
|
function getConfigPath(forgehiveDir2) {
|
|
5483
|
-
return
|
|
6088
|
+
return path24.join(forgehiveDir2, "cost-config.yaml");
|
|
5484
6089
|
}
|
|
5485
6090
|
function setSpendLimit(forgehiveDir2, config) {
|
|
5486
|
-
|
|
6091
|
+
fs24.writeFileSync(getConfigPath(forgehiveDir2), jsYaml.dump(config), "utf8");
|
|
5487
6092
|
}
|
|
5488
6093
|
function getSpendConfig(forgehiveDir2) {
|
|
5489
6094
|
const p = getConfigPath(forgehiveDir2);
|
|
5490
|
-
if (!
|
|
6095
|
+
if (!fs24.existsSync(p)) return {};
|
|
5491
6096
|
try {
|
|
5492
|
-
return jsYaml.load(
|
|
6097
|
+
return jsYaml.load(fs24.readFileSync(p, "utf8"));
|
|
5493
6098
|
} catch {
|
|
5494
6099
|
return {};
|
|
5495
6100
|
}
|
|
@@ -5584,7 +6189,7 @@ function formatSecurityReport(report) {
|
|
|
5584
6189
|
}
|
|
5585
6190
|
|
|
5586
6191
|
// src/ci.ts
|
|
5587
|
-
import
|
|
6192
|
+
import path25 from "node:path";
|
|
5588
6193
|
function generateCiReport(projectRoot2, forgehiveDir2, failOn = "high") {
|
|
5589
6194
|
const secrets = scanSecrets(projectRoot2);
|
|
5590
6195
|
const sast = scanSast(projectRoot2);
|
|
@@ -5601,7 +6206,7 @@ function generateCiReport(projectRoot2, forgehiveDir2, failOn = "high") {
|
|
|
5601
6206
|
}
|
|
5602
6207
|
return {
|
|
5603
6208
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5604
|
-
project:
|
|
6209
|
+
project: path25.basename(projectRoot2),
|
|
5605
6210
|
status: failedOn ? "fail" : "pass",
|
|
5606
6211
|
secrets,
|
|
5607
6212
|
sast,
|
|
@@ -5675,8 +6280,8 @@ jobs:
|
|
|
5675
6280
|
}
|
|
5676
6281
|
|
|
5677
6282
|
// src/map.ts
|
|
5678
|
-
import
|
|
5679
|
-
import
|
|
6283
|
+
import fs25 from "node:fs";
|
|
6284
|
+
import path26 from "node:path";
|
|
5680
6285
|
var MAP_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"];
|
|
5681
6286
|
var IGNORE_DIRS2 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
|
|
5682
6287
|
var IMPORT_PATTERNS = [
|
|
@@ -5698,11 +6303,11 @@ function extractImports(content) {
|
|
|
5698
6303
|
function walkFiles(dir) {
|
|
5699
6304
|
const results = [];
|
|
5700
6305
|
function walk(current) {
|
|
5701
|
-
for (const entry of
|
|
6306
|
+
for (const entry of fs25.readdirSync(current, { withFileTypes: true })) {
|
|
5702
6307
|
if (IGNORE_DIRS2.includes(entry.name)) continue;
|
|
5703
|
-
const full =
|
|
6308
|
+
const full = path26.join(current, entry.name);
|
|
5704
6309
|
if (entry.isDirectory()) walk(full);
|
|
5705
|
-
else if (entry.isFile() && MAP_EXTS.includes(
|
|
6310
|
+
else if (entry.isFile() && MAP_EXTS.includes(path26.extname(entry.name)))
|
|
5706
6311
|
results.push(full);
|
|
5707
6312
|
}
|
|
5708
6313
|
}
|
|
@@ -5714,7 +6319,7 @@ function generateMap(projectRoot2) {
|
|
|
5714
6319
|
const files = filePaths.map((filePath) => {
|
|
5715
6320
|
let content = "";
|
|
5716
6321
|
try {
|
|
5717
|
-
content =
|
|
6322
|
+
content = fs25.readFileSync(filePath, "utf8");
|
|
5718
6323
|
} catch {
|
|
5719
6324
|
}
|
|
5720
6325
|
const rawLines = content.split("\n");
|
|
@@ -5722,7 +6327,7 @@ function generateMap(projectRoot2) {
|
|
|
5722
6327
|
const imports = extractImports(content);
|
|
5723
6328
|
return {
|
|
5724
6329
|
path: filePath,
|
|
5725
|
-
relativePath:
|
|
6330
|
+
relativePath: path26.relative(projectRoot2, filePath),
|
|
5726
6331
|
lines,
|
|
5727
6332
|
imports
|
|
5728
6333
|
};
|
|
@@ -5811,8 +6416,8 @@ function formatMap(map2, semantic) {
|
|
|
5811
6416
|
}
|
|
5812
6417
|
|
|
5813
6418
|
// src/semantic-map.ts
|
|
5814
|
-
import
|
|
5815
|
-
import
|
|
6419
|
+
import fs26 from "node:fs";
|
|
6420
|
+
import path27 from "node:path";
|
|
5816
6421
|
var IGNORE_DIRS3 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
|
|
5817
6422
|
var MAP_EXTS2 = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
|
|
5818
6423
|
var IMPORT_PATTERNS2 = {
|
|
@@ -5863,15 +6468,15 @@ function walkFiles2(dir) {
|
|
|
5863
6468
|
function walk(current) {
|
|
5864
6469
|
let entries;
|
|
5865
6470
|
try {
|
|
5866
|
-
entries =
|
|
6471
|
+
entries = fs26.readdirSync(current, { withFileTypes: true });
|
|
5867
6472
|
} catch {
|
|
5868
6473
|
return;
|
|
5869
6474
|
}
|
|
5870
6475
|
for (const entry of entries) {
|
|
5871
6476
|
if (IGNORE_DIRS3.includes(entry.name)) continue;
|
|
5872
|
-
const full =
|
|
6477
|
+
const full = path27.join(current, entry.name);
|
|
5873
6478
|
if (entry.isDirectory()) walk(full);
|
|
5874
|
-
else if (entry.isFile() && MAP_EXTS2.includes(
|
|
6479
|
+
else if (entry.isFile() && MAP_EXTS2.includes(path27.extname(entry.name))) {
|
|
5875
6480
|
results.push(full);
|
|
5876
6481
|
}
|
|
5877
6482
|
}
|
|
@@ -5917,15 +6522,15 @@ function _extractExports(content, lang) {
|
|
|
5917
6522
|
}
|
|
5918
6523
|
function _resolveImport(fromFile, importPath, allFiles) {
|
|
5919
6524
|
if (!importPath.startsWith(".")) return null;
|
|
5920
|
-
const fromDir =
|
|
5921
|
-
const base =
|
|
6525
|
+
const fromDir = path27.dirname(fromFile);
|
|
6526
|
+
const base = path27.resolve(fromDir, importPath);
|
|
5922
6527
|
if (allFiles.includes(base)) return base;
|
|
5923
6528
|
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".py", ".go"]) {
|
|
5924
6529
|
const candidate = base + ext;
|
|
5925
6530
|
if (allFiles.includes(candidate)) return candidate;
|
|
5926
6531
|
}
|
|
5927
6532
|
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
5928
|
-
const candidate =
|
|
6533
|
+
const candidate = path27.join(base, "index" + ext);
|
|
5929
6534
|
if (allFiles.includes(candidate)) return candidate;
|
|
5930
6535
|
}
|
|
5931
6536
|
return null;
|
|
@@ -5973,10 +6578,10 @@ function buildSemanticMap(projectRoot2) {
|
|
|
5973
6578
|
for (const filePath of allFiles) {
|
|
5974
6579
|
let content = "";
|
|
5975
6580
|
try {
|
|
5976
|
-
content =
|
|
6581
|
+
content = fs26.readFileSync(filePath, "utf8");
|
|
5977
6582
|
} catch {
|
|
5978
6583
|
}
|
|
5979
|
-
const ext =
|
|
6584
|
+
const ext = path27.extname(filePath);
|
|
5980
6585
|
const lang = langOf(ext);
|
|
5981
6586
|
const rawImps = _extractImports(content, lang);
|
|
5982
6587
|
const resolvedImps = [];
|
|
@@ -5994,11 +6599,11 @@ function buildSemanticMap(projectRoot2) {
|
|
|
5994
6599
|
|
|
5995
6600
|
// src/onboard.ts
|
|
5996
6601
|
init_js_yaml();
|
|
5997
|
-
import
|
|
5998
|
-
import
|
|
5999
|
-
import { spawnSync as
|
|
6602
|
+
import fs27 from "node:fs";
|
|
6603
|
+
import path28 from "node:path";
|
|
6604
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
6000
6605
|
function getRecentCommits(projectRoot2, n = 20) {
|
|
6001
|
-
const result =
|
|
6606
|
+
const result = spawnSync8("git", ["log", `--oneline`, `-${n}`], {
|
|
6002
6607
|
cwd: projectRoot2,
|
|
6003
6608
|
encoding: "utf8"
|
|
6004
6609
|
});
|
|
@@ -6006,30 +6611,30 @@ function getRecentCommits(projectRoot2, n = 20) {
|
|
|
6006
6611
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
6007
6612
|
}
|
|
6008
6613
|
function readCapabilities(forgehiveDir2) {
|
|
6009
|
-
const capPath =
|
|
6010
|
-
if (!
|
|
6614
|
+
const capPath = path28.join(forgehiveDir2, "capabilities.yaml");
|
|
6615
|
+
if (!fs27.existsSync(capPath)) return {};
|
|
6011
6616
|
try {
|
|
6012
|
-
return jsYaml.load(
|
|
6617
|
+
return jsYaml.load(fs27.readFileSync(capPath, "utf8")) ?? {};
|
|
6013
6618
|
} catch {
|
|
6014
6619
|
return {};
|
|
6015
6620
|
}
|
|
6016
6621
|
}
|
|
6017
6622
|
function readMemoryFiles(forgehiveDir2) {
|
|
6018
|
-
const memDir =
|
|
6019
|
-
if (!
|
|
6623
|
+
const memDir = path28.join(forgehiveDir2, "memory");
|
|
6624
|
+
if (!fs27.existsSync(memDir)) return {};
|
|
6020
6625
|
const result = {};
|
|
6021
|
-
for (const f of
|
|
6022
|
-
result[f] =
|
|
6626
|
+
for (const f of fs27.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
|
|
6627
|
+
result[f] = fs27.readFileSync(path28.join(memDir, f), "utf8");
|
|
6023
6628
|
}
|
|
6024
6629
|
return result;
|
|
6025
6630
|
}
|
|
6026
6631
|
function listAdrs2(forgehiveDir2) {
|
|
6027
|
-
const adrsDir =
|
|
6028
|
-
if (!
|
|
6029
|
-
return
|
|
6632
|
+
const adrsDir = path28.join(forgehiveDir2, "memory", "adrs");
|
|
6633
|
+
if (!fs27.existsSync(adrsDir)) return [];
|
|
6634
|
+
return fs27.readdirSync(adrsDir).filter((f) => f.endsWith(".md"));
|
|
6030
6635
|
}
|
|
6031
6636
|
function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
|
|
6032
|
-
const projectName =
|
|
6637
|
+
const projectName = path28.basename(projectRoot2);
|
|
6033
6638
|
const caps = readCapabilities(forgehiveDir2);
|
|
6034
6639
|
const memFiles = readMemoryFiles(forgehiveDir2);
|
|
6035
6640
|
const commits = getRecentCommits(projectRoot2);
|
|
@@ -6103,7 +6708,7 @@ function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
|
|
|
6103
6708
|
}
|
|
6104
6709
|
|
|
6105
6710
|
// src/changelog.ts
|
|
6106
|
-
import { spawnSync as
|
|
6711
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
6107
6712
|
var TYPE_LABELS = {
|
|
6108
6713
|
feat: "Added",
|
|
6109
6714
|
fix: "Fixed",
|
|
@@ -6167,12 +6772,12 @@ function formatChangelog(commits, version2) {
|
|
|
6167
6772
|
function getGitLogSince(projectRoot2, since) {
|
|
6168
6773
|
const args = ["log", "--oneline", "--no-merges"];
|
|
6169
6774
|
if (since) args.push(`${since}..HEAD`);
|
|
6170
|
-
const result =
|
|
6775
|
+
const result = spawnSync9("git", args, { cwd: projectRoot2, encoding: "utf8" });
|
|
6171
6776
|
if (result.status !== 0) return "";
|
|
6172
6777
|
return result.stdout.trim();
|
|
6173
6778
|
}
|
|
6174
6779
|
function getLatestTag(projectRoot2) {
|
|
6175
|
-
const result =
|
|
6780
|
+
const result = spawnSync9("git", ["describe", "--tags", "--abbrev=0"], {
|
|
6176
6781
|
cwd: projectRoot2,
|
|
6177
6782
|
encoding: "utf8"
|
|
6178
6783
|
});
|
|
@@ -6181,7 +6786,7 @@ function getLatestTag(projectRoot2) {
|
|
|
6181
6786
|
}
|
|
6182
6787
|
|
|
6183
6788
|
// src/metrics.ts
|
|
6184
|
-
import { spawnSync as
|
|
6789
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
6185
6790
|
function parseCommitStats(rawLog) {
|
|
6186
6791
|
const lines = rawLog.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
6187
6792
|
const byAuthor = {};
|
|
@@ -6238,26 +6843,26 @@ function formatMetrics(stats, projectName) {
|
|
|
6238
6843
|
function getMetricsGitLog(projectRoot2, since) {
|
|
6239
6844
|
const args = ["log", "--no-merges", "--format=%as %an %s"];
|
|
6240
6845
|
if (since) args.push(`--since=${since}`);
|
|
6241
|
-
const result =
|
|
6846
|
+
const result = spawnSync10("git", args, { cwd: projectRoot2, encoding: "utf8" });
|
|
6242
6847
|
if (result.status !== 0) return "";
|
|
6243
6848
|
return result.stdout.trim();
|
|
6244
6849
|
}
|
|
6245
6850
|
|
|
6246
6851
|
// src/sync.ts
|
|
6247
|
-
import
|
|
6248
|
-
import
|
|
6249
|
-
import { spawnSync as
|
|
6852
|
+
import fs28 from "node:fs";
|
|
6853
|
+
import path29 from "node:path";
|
|
6854
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
6250
6855
|
function getSyncStatus(forgehiveDir2) {
|
|
6251
|
-
const memDir =
|
|
6252
|
-
const files =
|
|
6253
|
-
const projectRoot2 =
|
|
6254
|
-
const configResult =
|
|
6856
|
+
const memDir = path29.join(forgehiveDir2, "memory");
|
|
6857
|
+
const files = fs28.existsSync(memDir) ? fs28.readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
6858
|
+
const projectRoot2 = path29.dirname(forgehiveDir2);
|
|
6859
|
+
const configResult = spawnSync11(
|
|
6255
6860
|
"git",
|
|
6256
6861
|
["config", "--get", "forgehive.sync-remote"],
|
|
6257
6862
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6258
6863
|
);
|
|
6259
6864
|
const remote = configResult.status === 0 ? configResult.stdout.trim() : null;
|
|
6260
|
-
const branchResult =
|
|
6865
|
+
const branchResult = spawnSync11(
|
|
6261
6866
|
"git",
|
|
6262
6867
|
["config", "--get", "forgehive.sync-branch"],
|
|
6263
6868
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
@@ -6266,24 +6871,24 @@ function getSyncStatus(forgehiveDir2) {
|
|
|
6266
6871
|
return { files, hasRemote: remote !== null, remote, branch };
|
|
6267
6872
|
}
|
|
6268
6873
|
function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6269
|
-
const projectRoot2 =
|
|
6270
|
-
const memDir =
|
|
6271
|
-
if (!
|
|
6874
|
+
const projectRoot2 = path29.dirname(forgehiveDir2);
|
|
6875
|
+
const memDir = path29.join(forgehiveDir2, "memory");
|
|
6876
|
+
if (!fs28.existsSync(memDir)) {
|
|
6272
6877
|
return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
|
|
6273
6878
|
}
|
|
6274
|
-
const files =
|
|
6879
|
+
const files = fs28.readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
6275
6880
|
if (files.length === 0) {
|
|
6276
6881
|
return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
|
|
6277
6882
|
}
|
|
6278
|
-
const addResult =
|
|
6883
|
+
const addResult = spawnSync11(
|
|
6279
6884
|
"git",
|
|
6280
|
-
["add",
|
|
6885
|
+
["add", path29.join(".forgehive", "memory")],
|
|
6281
6886
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6282
6887
|
);
|
|
6283
6888
|
if (addResult.status !== 0) {
|
|
6284
6889
|
return { success: false, message: `git add failed: ${addResult.stderr}`, filesCommitted: 0 };
|
|
6285
6890
|
}
|
|
6286
|
-
const commitResult =
|
|
6891
|
+
const commitResult = spawnSync11(
|
|
6287
6892
|
"git",
|
|
6288
6893
|
["commit", "-m", `chore: sync forgehive memory [${(/* @__PURE__ */ new Date()).toISOString()}]`],
|
|
6289
6894
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
@@ -6292,7 +6897,7 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6292
6897
|
if (!commitOk) {
|
|
6293
6898
|
return { success: false, message: `git commit failed: ${commitResult.stderr}`, filesCommitted: 0 };
|
|
6294
6899
|
}
|
|
6295
|
-
const pushResult =
|
|
6900
|
+
const pushResult = spawnSync11(
|
|
6296
6901
|
"git",
|
|
6297
6902
|
["push", remote, `HEAD:${branch}`],
|
|
6298
6903
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
@@ -6300,15 +6905,15 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6300
6905
|
if (pushResult.status !== 0) {
|
|
6301
6906
|
return { success: false, message: `git push failed: ${pushResult.stderr}`, filesCommitted: 0 };
|
|
6302
6907
|
}
|
|
6303
|
-
|
|
6304
|
-
|
|
6908
|
+
spawnSync11("git", ["config", "forgehive.sync-remote", remote], { cwd: projectRoot2 });
|
|
6909
|
+
spawnSync11("git", ["config", "forgehive.sync-branch", branch], { cwd: projectRoot2 });
|
|
6305
6910
|
return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
|
|
6306
6911
|
}
|
|
6307
6912
|
function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6308
|
-
const projectRoot2 =
|
|
6309
|
-
const memDir =
|
|
6310
|
-
|
|
6311
|
-
const fetchResult =
|
|
6913
|
+
const projectRoot2 = path29.dirname(forgehiveDir2);
|
|
6914
|
+
const memDir = path29.join(forgehiveDir2, "memory");
|
|
6915
|
+
fs28.mkdirSync(memDir, { recursive: true });
|
|
6916
|
+
const fetchResult = spawnSync11(
|
|
6312
6917
|
"git",
|
|
6313
6918
|
["fetch", remote, branch],
|
|
6314
6919
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
@@ -6316,7 +6921,7 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6316
6921
|
if (fetchResult.status !== 0) {
|
|
6317
6922
|
return { success: false, message: `git fetch failed: ${fetchResult.stderr}`, filesImported: [] };
|
|
6318
6923
|
}
|
|
6319
|
-
const listResult =
|
|
6924
|
+
const listResult = spawnSync11(
|
|
6320
6925
|
"git",
|
|
6321
6926
|
["ls-tree", "--name-only", `${remote}/${branch}`, ".forgehive/memory/"],
|
|
6322
6927
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
@@ -6327,16 +6932,16 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6327
6932
|
const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
|
|
6328
6933
|
const imported = [];
|
|
6329
6934
|
for (const remotePath of remoteFiles) {
|
|
6330
|
-
const filename =
|
|
6331
|
-
const localPath =
|
|
6332
|
-
if (!
|
|
6333
|
-
const contentResult =
|
|
6935
|
+
const filename = path29.basename(remotePath);
|
|
6936
|
+
const localPath = path29.join(memDir, filename);
|
|
6937
|
+
if (!fs28.existsSync(localPath)) {
|
|
6938
|
+
const contentResult = spawnSync11(
|
|
6334
6939
|
"git",
|
|
6335
6940
|
["show", `${remote}/${branch}:${remotePath}`],
|
|
6336
6941
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6337
6942
|
);
|
|
6338
6943
|
if (contentResult.status === 0) {
|
|
6339
|
-
|
|
6944
|
+
fs28.writeFileSync(localPath, contentResult.stdout, "utf8");
|
|
6340
6945
|
imported.push(filename);
|
|
6341
6946
|
}
|
|
6342
6947
|
}
|
|
@@ -6349,8 +6954,8 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6349
6954
|
}
|
|
6350
6955
|
|
|
6351
6956
|
// src/background.ts
|
|
6352
|
-
import
|
|
6353
|
-
import
|
|
6957
|
+
import fs29 from "node:fs";
|
|
6958
|
+
import path30 from "node:path";
|
|
6354
6959
|
import { spawn } from "node:child_process";
|
|
6355
6960
|
var AGENT_ROLES = {
|
|
6356
6961
|
kai: "Senior Engineer \u2014 implements features and fixes bugs",
|
|
@@ -6397,23 +7002,23 @@ Instructions:
|
|
|
6397
7002
|
Work autonomously. Do not ask for clarification \u2014 use your best judgment based on the issue description and codebase context.`;
|
|
6398
7003
|
}
|
|
6399
7004
|
function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
6400
|
-
const logsDir =
|
|
6401
|
-
|
|
7005
|
+
const logsDir = path30.join(forgehiveDir2, "background-runs");
|
|
7006
|
+
fs29.mkdirSync(logsDir, { recursive: true });
|
|
6402
7007
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6403
|
-
const logFile =
|
|
7008
|
+
const logFile = path30.join(logsDir, `${agentId}-${timestamp2}.log`);
|
|
6404
7009
|
const prompt = buildAgentPrompt(issueUrl, agentId);
|
|
6405
|
-
const logStream =
|
|
7010
|
+
const logStream = fs29.openSync(logFile, "w");
|
|
6406
7011
|
const child = spawn(
|
|
6407
7012
|
"claude",
|
|
6408
7013
|
["-p", prompt, "--output-format", "text"],
|
|
6409
7014
|
{
|
|
6410
|
-
cwd:
|
|
7015
|
+
cwd: path30.dirname(forgehiveDir2),
|
|
6411
7016
|
detached: true,
|
|
6412
7017
|
stdio: ["ignore", logStream, logStream]
|
|
6413
7018
|
}
|
|
6414
7019
|
);
|
|
6415
7020
|
child.unref();
|
|
6416
|
-
|
|
7021
|
+
fs29.closeSync(logStream);
|
|
6417
7022
|
return {
|
|
6418
7023
|
pid: child.pid,
|
|
6419
7024
|
logFile,
|
|
@@ -6423,11 +7028,11 @@ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
|
6423
7028
|
|
|
6424
7029
|
// src/stories.ts
|
|
6425
7030
|
init_js_yaml();
|
|
6426
|
-
import
|
|
6427
|
-
import
|
|
7031
|
+
import fs30 from "node:fs";
|
|
7032
|
+
import path31 from "node:path";
|
|
6428
7033
|
function nextStoryId(storiesDir) {
|
|
6429
|
-
if (!
|
|
6430
|
-
const existing =
|
|
7034
|
+
if (!fs30.existsSync(storiesDir)) return "US-1";
|
|
7035
|
+
const existing = fs30.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseInt(f.replace("US-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
|
|
6431
7036
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6432
7037
|
return `US-${max + 1}`;
|
|
6433
7038
|
}
|
|
@@ -6459,7 +7064,7 @@ ${acLines || "- [ ] (noch nicht definiert)"}
|
|
|
6459
7064
|
}
|
|
6460
7065
|
function parseStoryFile(filePath) {
|
|
6461
7066
|
try {
|
|
6462
|
-
const content =
|
|
7067
|
+
const content = fs30.readFileSync(filePath, "utf8");
|
|
6463
7068
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6464
7069
|
if (!match) return null;
|
|
6465
7070
|
const data = jsYaml.load(match[1]);
|
|
@@ -6479,7 +7084,7 @@ function parseStoryFile(filePath) {
|
|
|
6479
7084
|
}
|
|
6480
7085
|
}
|
|
6481
7086
|
function createStory(storiesDir, title, epicId) {
|
|
6482
|
-
|
|
7087
|
+
fs30.mkdirSync(storiesDir, { recursive: true });
|
|
6483
7088
|
const id = nextStoryId(storiesDir);
|
|
6484
7089
|
const story = {
|
|
6485
7090
|
id,
|
|
@@ -6492,33 +7097,33 @@ function createStory(storiesDir, title, epicId) {
|
|
|
6492
7097
|
epicId: epicId ?? null,
|
|
6493
7098
|
status: "backlog"
|
|
6494
7099
|
};
|
|
6495
|
-
|
|
7100
|
+
fs30.writeFileSync(path31.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6496
7101
|
return story;
|
|
6497
7102
|
}
|
|
6498
7103
|
function listStories(storiesDir) {
|
|
6499
|
-
if (!
|
|
6500
|
-
return
|
|
7104
|
+
if (!fs30.existsSync(storiesDir)) return [];
|
|
7105
|
+
return fs30.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseStoryFile(path31.join(storiesDir, f))).filter((s) => s !== null).sort((a, b) => {
|
|
6501
7106
|
const na = parseInt(a.id.replace("US-", ""), 10);
|
|
6502
7107
|
const nb = parseInt(b.id.replace("US-", ""), 10);
|
|
6503
7108
|
return na - nb;
|
|
6504
7109
|
});
|
|
6505
7110
|
}
|
|
6506
7111
|
function getStory(storiesDir, id) {
|
|
6507
|
-
const filePath =
|
|
6508
|
-
if (!
|
|
7112
|
+
const filePath = path31.join(storiesDir, `${id}.md`);
|
|
7113
|
+
if (!fs30.existsSync(filePath)) return null;
|
|
6509
7114
|
return parseStoryFile(filePath);
|
|
6510
7115
|
}
|
|
6511
7116
|
function updateStoryPoints(storiesDir, id, points) {
|
|
6512
7117
|
const story = getStory(storiesDir, id);
|
|
6513
7118
|
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6514
7119
|
story.points = points;
|
|
6515
|
-
|
|
7120
|
+
fs30.writeFileSync(path31.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6516
7121
|
}
|
|
6517
7122
|
function updateStoryStatus(storiesDir, id, status) {
|
|
6518
7123
|
const story = getStory(storiesDir, id);
|
|
6519
7124
|
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6520
7125
|
story.status = status;
|
|
6521
|
-
|
|
7126
|
+
fs30.writeFileSync(path31.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6522
7127
|
}
|
|
6523
7128
|
function formatStoryCard(story) {
|
|
6524
7129
|
const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
|
|
@@ -6537,11 +7142,11 @@ function formatStoryCard(story) {
|
|
|
6537
7142
|
|
|
6538
7143
|
// src/epics.ts
|
|
6539
7144
|
init_js_yaml();
|
|
6540
|
-
import
|
|
6541
|
-
import
|
|
7145
|
+
import fs31 from "node:fs";
|
|
7146
|
+
import path32 from "node:path";
|
|
6542
7147
|
function nextEpicId(epicsDir) {
|
|
6543
|
-
if (!
|
|
6544
|
-
const existing =
|
|
7148
|
+
if (!fs31.existsSync(epicsDir)) return "EPC-1";
|
|
7149
|
+
const existing = fs31.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseInt(f.replace("EPC-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
|
|
6545
7150
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6546
7151
|
return `EPC-${max + 1}`;
|
|
6547
7152
|
}
|
|
@@ -6569,7 +7174,7 @@ ${storyLines || "(noch keine Stories)"}
|
|
|
6569
7174
|
}
|
|
6570
7175
|
function parseEpicFile(filePath) {
|
|
6571
7176
|
try {
|
|
6572
|
-
const content =
|
|
7177
|
+
const content = fs31.readFileSync(filePath, "utf8");
|
|
6573
7178
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6574
7179
|
if (!match) return null;
|
|
6575
7180
|
const data = jsYaml.load(match[1]);
|
|
@@ -6586,23 +7191,23 @@ function parseEpicFile(filePath) {
|
|
|
6586
7191
|
}
|
|
6587
7192
|
}
|
|
6588
7193
|
function createEpic(epicsDir, title, goal, prdId) {
|
|
6589
|
-
|
|
7194
|
+
fs31.mkdirSync(epicsDir, { recursive: true });
|
|
6590
7195
|
const id = nextEpicId(epicsDir);
|
|
6591
7196
|
const epic = { id, title, goal: goal ?? "", stories: [], status: "active", prdId: prdId ?? null };
|
|
6592
|
-
|
|
7197
|
+
fs31.writeFileSync(path32.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
|
|
6593
7198
|
return epic;
|
|
6594
7199
|
}
|
|
6595
7200
|
function listEpics(epicsDir) {
|
|
6596
|
-
if (!
|
|
6597
|
-
return
|
|
7201
|
+
if (!fs31.existsSync(epicsDir)) return [];
|
|
7202
|
+
return fs31.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseEpicFile(path32.join(epicsDir, f))).filter((e) => e !== null).sort((a, b) => {
|
|
6598
7203
|
const na = parseInt(a.id.replace("EPC-", ""), 10);
|
|
6599
7204
|
const nb = parseInt(b.id.replace("EPC-", ""), 10);
|
|
6600
7205
|
return na - nb;
|
|
6601
7206
|
});
|
|
6602
7207
|
}
|
|
6603
7208
|
function getEpic(epicsDir, id) {
|
|
6604
|
-
const filePath =
|
|
6605
|
-
if (!
|
|
7209
|
+
const filePath = path32.join(epicsDir, `${id}.md`);
|
|
7210
|
+
if (!fs31.existsSync(filePath)) return null;
|
|
6606
7211
|
return parseEpicFile(filePath);
|
|
6607
7212
|
}
|
|
6608
7213
|
function formatEpicCard(epic, stories) {
|
|
@@ -6626,14 +7231,14 @@ function formatEpicCard(epic, stories) {
|
|
|
6626
7231
|
|
|
6627
7232
|
// src/product.ts
|
|
6628
7233
|
init_js_yaml();
|
|
6629
|
-
import
|
|
6630
|
-
import
|
|
7234
|
+
import fs32 from "node:fs";
|
|
7235
|
+
import path33 from "node:path";
|
|
6631
7236
|
function slugify2(title) {
|
|
6632
7237
|
return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-");
|
|
6633
7238
|
}
|
|
6634
7239
|
function nextPrdId(prdsDir) {
|
|
6635
|
-
if (!
|
|
6636
|
-
const existing =
|
|
7240
|
+
if (!fs32.existsSync(prdsDir)) return "PRD-1";
|
|
7241
|
+
const existing = fs32.readdirSync(prdsDir).filter((f) => f.endsWith(".md")).map((f) => parsePrdFile(path33.join(prdsDir, f))).filter((p) => p !== null).map((p) => parseInt(p.id.replace("PRD-", ""), 10)).filter((n) => !isNaN(n));
|
|
6637
7242
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6638
7243
|
return `PRD-${max + 1}`;
|
|
6639
7244
|
}
|
|
@@ -6671,7 +7276,7 @@ ${frontmatter}---
|
|
|
6671
7276
|
}
|
|
6672
7277
|
function parsePrdFile(filePath) {
|
|
6673
7278
|
try {
|
|
6674
|
-
const content =
|
|
7279
|
+
const content = fs32.readFileSync(filePath, "utf8");
|
|
6675
7280
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6676
7281
|
if (!match) return null;
|
|
6677
7282
|
const data = jsYaml.load(match[1]);
|
|
@@ -6688,37 +7293,37 @@ function parsePrdFile(filePath) {
|
|
|
6688
7293
|
}
|
|
6689
7294
|
}
|
|
6690
7295
|
function createPrd(prdsDir, title) {
|
|
6691
|
-
|
|
7296
|
+
fs32.mkdirSync(prdsDir, { recursive: true });
|
|
6692
7297
|
const id = nextPrdId(prdsDir);
|
|
6693
7298
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6694
7299
|
const slug = slugify2(title);
|
|
6695
7300
|
const filename = `${today}-${slug}.md`;
|
|
6696
|
-
const filepath =
|
|
7301
|
+
const filepath = path33.join(prdsDir, filename);
|
|
6697
7302
|
const prd = { id, title, date: today, status: "draft", filepath };
|
|
6698
|
-
|
|
7303
|
+
fs32.writeFileSync(filepath, prdToMarkdown(prd), "utf8");
|
|
6699
7304
|
return prd;
|
|
6700
7305
|
}
|
|
6701
7306
|
function listPrds(prdsDir) {
|
|
6702
|
-
if (!
|
|
6703
|
-
return
|
|
7307
|
+
if (!fs32.existsSync(prdsDir)) return [];
|
|
7308
|
+
return fs32.readdirSync(prdsDir).filter((f) => f.endsWith(".md")).map((f) => parsePrdFile(path33.join(prdsDir, f))).filter((p) => p !== null && /^PRD-\d+$/.test(p.id)).sort((a, b) => {
|
|
6704
7309
|
const na = parseInt(a.id.replace("PRD-", ""), 10);
|
|
6705
7310
|
const nb = parseInt(b.id.replace("PRD-", ""), 10);
|
|
6706
7311
|
return na - nb;
|
|
6707
7312
|
});
|
|
6708
7313
|
}
|
|
6709
7314
|
function getPrd(prdsDir, id) {
|
|
6710
|
-
if (!
|
|
6711
|
-
const files =
|
|
7315
|
+
if (!fs32.existsSync(prdsDir)) return null;
|
|
7316
|
+
const files = fs32.readdirSync(prdsDir).filter((f) => f.endsWith(".md"));
|
|
6712
7317
|
for (const f of files) {
|
|
6713
|
-
const prd = parsePrdFile(
|
|
7318
|
+
const prd = parsePrdFile(path33.join(prdsDir, f));
|
|
6714
7319
|
if (prd && prd.id === id) return prd;
|
|
6715
7320
|
}
|
|
6716
7321
|
return null;
|
|
6717
7322
|
}
|
|
6718
7323
|
function generateRoadmap(forgehiveDir2) {
|
|
6719
|
-
const prdsDir =
|
|
6720
|
-
const epicsDir =
|
|
6721
|
-
const storiesDir =
|
|
7324
|
+
const prdsDir = path33.join(forgehiveDir2, "memory", "prds");
|
|
7325
|
+
const epicsDir = path33.join(forgehiveDir2, "memory", "epics");
|
|
7326
|
+
const storiesDir = path33.join(forgehiveDir2, "memory", "stories");
|
|
6722
7327
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6723
7328
|
const prds = listPrds(prdsDir);
|
|
6724
7329
|
const epics = listEpics(epicsDir);
|
|
@@ -6788,24 +7393,24 @@ function generateRoadmap(forgehiveDir2) {
|
|
|
6788
7393
|
}
|
|
6789
7394
|
|
|
6790
7395
|
// src/velocity.ts
|
|
6791
|
-
import
|
|
6792
|
-
import
|
|
7396
|
+
import fs33 from "node:fs";
|
|
7397
|
+
import path34 from "node:path";
|
|
6793
7398
|
var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
|
|
6794
7399
|
function recordVelocity(velocityFile, sprint, committed, delivered) {
|
|
6795
7400
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6796
7401
|
const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
|
|
6797
7402
|
const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
|
|
6798
7403
|
`;
|
|
6799
|
-
if (!
|
|
6800
|
-
|
|
6801
|
-
|
|
7404
|
+
if (!fs33.existsSync(velocityFile)) {
|
|
7405
|
+
fs33.mkdirSync(path34.dirname(velocityFile), { recursive: true });
|
|
7406
|
+
fs33.writeFileSync(velocityFile, HEADER + row, "utf8");
|
|
6802
7407
|
} else {
|
|
6803
|
-
|
|
7408
|
+
fs33.appendFileSync(velocityFile, row, "utf8");
|
|
6804
7409
|
}
|
|
6805
7410
|
}
|
|
6806
7411
|
function getVelocityHistory(velocityFile) {
|
|
6807
|
-
if (!
|
|
6808
|
-
const content =
|
|
7412
|
+
if (!fs33.existsSync(velocityFile)) return [];
|
|
7413
|
+
const content = fs33.readFileSync(velocityFile, "utf8");
|
|
6809
7414
|
const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
|
|
6810
7415
|
return rows.map((row) => {
|
|
6811
7416
|
const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
|
|
@@ -6846,9 +7451,9 @@ function formatVelocityReport(history) {
|
|
|
6846
7451
|
|
|
6847
7452
|
// src/docs.ts
|
|
6848
7453
|
init_js_yaml();
|
|
6849
|
-
import
|
|
6850
|
-
import
|
|
6851
|
-
import { spawnSync as
|
|
7454
|
+
import fs34 from "node:fs";
|
|
7455
|
+
import path35 from "node:path";
|
|
7456
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
6852
7457
|
var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
|
|
6853
7458
|
var IGNORE_DIRS4 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
|
|
6854
7459
|
var EXPORT_PATTERNS2 = [
|
|
@@ -6858,10 +7463,10 @@ var EXPORT_PATTERNS2 = [
|
|
|
6858
7463
|
/^export\s+default\s+(?:function\s+)?(\w+)?/gm
|
|
6859
7464
|
];
|
|
6860
7465
|
function readCapabilities2(forgehiveDir2) {
|
|
6861
|
-
const capPath =
|
|
6862
|
-
if (!
|
|
7466
|
+
const capPath = path35.join(forgehiveDir2, "capabilities.yaml");
|
|
7467
|
+
if (!fs34.existsSync(capPath)) return {};
|
|
6863
7468
|
try {
|
|
6864
|
-
return jsYaml.load(
|
|
7469
|
+
return jsYaml.load(fs34.readFileSync(capPath, "utf8")) ?? {};
|
|
6865
7470
|
} catch {
|
|
6866
7471
|
return {};
|
|
6867
7472
|
}
|
|
@@ -6888,16 +7493,16 @@ function extractCapabilityInfo(caps) {
|
|
|
6888
7493
|
return { language, packageManager };
|
|
6889
7494
|
}
|
|
6890
7495
|
function readMemoryFiles2(forgehiveDir2) {
|
|
6891
|
-
const memDir =
|
|
6892
|
-
if (!
|
|
7496
|
+
const memDir = path35.join(forgehiveDir2, "memory");
|
|
7497
|
+
if (!fs34.existsSync(memDir)) return {};
|
|
6893
7498
|
const result = {};
|
|
6894
|
-
for (const f of
|
|
6895
|
-
result[f] =
|
|
7499
|
+
for (const f of fs34.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
|
|
7500
|
+
result[f] = fs34.readFileSync(path35.join(memDir, f), "utf8");
|
|
6896
7501
|
}
|
|
6897
7502
|
return result;
|
|
6898
7503
|
}
|
|
6899
7504
|
function getRecentCommits2(projectRoot2, n = 10) {
|
|
6900
|
-
const result =
|
|
7505
|
+
const result = spawnSync12("git", ["log", "--oneline", `-${n}`], { cwd: projectRoot2, encoding: "utf8" });
|
|
6901
7506
|
if (result.status !== 0) return [];
|
|
6902
7507
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
6903
7508
|
}
|
|
@@ -6915,19 +7520,19 @@ function extractExports(content) {
|
|
|
6915
7520
|
function walkSourceFiles(dir) {
|
|
6916
7521
|
const results = [];
|
|
6917
7522
|
function walk(current) {
|
|
6918
|
-
if (!
|
|
6919
|
-
for (const entry of
|
|
7523
|
+
if (!fs34.existsSync(current)) return;
|
|
7524
|
+
for (const entry of fs34.readdirSync(current, { withFileTypes: true })) {
|
|
6920
7525
|
if (IGNORE_DIRS4.includes(entry.name)) continue;
|
|
6921
|
-
const full =
|
|
7526
|
+
const full = path35.join(current, entry.name);
|
|
6922
7527
|
if (entry.isDirectory()) walk(full);
|
|
6923
|
-
else if (entry.isFile() && SOURCE_EXTS.includes(
|
|
7528
|
+
else if (entry.isFile() && SOURCE_EXTS.includes(path35.extname(entry.name))) results.push(full);
|
|
6924
7529
|
}
|
|
6925
7530
|
}
|
|
6926
7531
|
walk(dir);
|
|
6927
7532
|
return results;
|
|
6928
7533
|
}
|
|
6929
7534
|
function generateUserGuide(projectRoot2, forgehiveDir2) {
|
|
6930
|
-
const projectName =
|
|
7535
|
+
const projectName = path35.basename(projectRoot2);
|
|
6931
7536
|
const caps = readCapabilities2(forgehiveDir2);
|
|
6932
7537
|
const { language: lang, packageManager, entryPoints } = extractCapabilityInfo(caps);
|
|
6933
7538
|
const memFiles = readMemoryFiles2(forgehiveDir2);
|
|
@@ -7006,8 +7611,8 @@ function generateUserGuide(projectRoot2, forgehiveDir2) {
|
|
|
7006
7611
|
return lines.join("\n");
|
|
7007
7612
|
}
|
|
7008
7613
|
function generateApiReference(projectRoot2) {
|
|
7009
|
-
const srcDir =
|
|
7010
|
-
const searchDir =
|
|
7614
|
+
const srcDir = path35.join(projectRoot2, "src");
|
|
7615
|
+
const searchDir = fs34.existsSync(srcDir) ? srcDir : projectRoot2;
|
|
7011
7616
|
const files = walkSourceFiles(searchDir);
|
|
7012
7617
|
const lines = [];
|
|
7013
7618
|
lines.push("# API Reference");
|
|
@@ -7021,13 +7626,13 @@ function generateApiReference(projectRoot2) {
|
|
|
7021
7626
|
for (const filePath of files) {
|
|
7022
7627
|
let content = "";
|
|
7023
7628
|
try {
|
|
7024
|
-
content =
|
|
7629
|
+
content = fs34.readFileSync(filePath, "utf8");
|
|
7025
7630
|
} catch {
|
|
7026
7631
|
continue;
|
|
7027
7632
|
}
|
|
7028
7633
|
const exports = extractExports(content);
|
|
7029
7634
|
if (exports.length === 0) continue;
|
|
7030
|
-
const relPath =
|
|
7635
|
+
const relPath = path35.relative(projectRoot2, filePath);
|
|
7031
7636
|
lines.push(`## \`${relPath}\``);
|
|
7032
7637
|
lines.push("");
|
|
7033
7638
|
lines.push("**Exports:**");
|
|
@@ -7039,23 +7644,23 @@ function generateApiReference(projectRoot2) {
|
|
|
7039
7644
|
}
|
|
7040
7645
|
function listExistingDocs(projectRoot2) {
|
|
7041
7646
|
const docs = [];
|
|
7042
|
-
const docsDir =
|
|
7043
|
-
if (
|
|
7044
|
-
for (const f of
|
|
7045
|
-
if (f.endsWith(".md")) docs.push(
|
|
7647
|
+
const docsDir = path35.join(projectRoot2, "docs");
|
|
7648
|
+
if (fs34.existsSync(docsDir)) {
|
|
7649
|
+
for (const f of fs34.readdirSync(docsDir)) {
|
|
7650
|
+
if (f.endsWith(".md")) docs.push(path35.join(docsDir, f));
|
|
7046
7651
|
}
|
|
7047
7652
|
}
|
|
7048
7653
|
const rootDocs = ["README.md", "CHANGELOG.md", "ONBOARDING.md", "CONTRIBUTING.md"];
|
|
7049
7654
|
for (const f of rootDocs) {
|
|
7050
|
-
const full =
|
|
7051
|
-
if (
|
|
7655
|
+
const full = path35.join(projectRoot2, f);
|
|
7656
|
+
if (fs34.existsSync(full)) docs.push(full);
|
|
7052
7657
|
}
|
|
7053
7658
|
return docs;
|
|
7054
7659
|
}
|
|
7055
7660
|
|
|
7056
7661
|
// src/router.ts
|
|
7057
|
-
import
|
|
7058
|
-
import
|
|
7662
|
+
import fs35 from "node:fs";
|
|
7663
|
+
import path36 from "node:path";
|
|
7059
7664
|
var AGENT_KEYWORDS = {
|
|
7060
7665
|
vera: ["security", "auth", "vulnerability", "owasp", "cve", "gdpr", "compliance", "penetration", "exploit"],
|
|
7061
7666
|
sam: ["test", "coverage", "qa", "quality", "spec", "regression", "fixture", "assertion", "e2e"],
|
|
@@ -7164,9 +7769,9 @@ function routeTask(task) {
|
|
|
7164
7769
|
}
|
|
7165
7770
|
function resolveWorktreePath(forgehiveDir2, agent) {
|
|
7166
7771
|
if (!agent) return void 0;
|
|
7167
|
-
const worktreeFile =
|
|
7168
|
-
if (
|
|
7169
|
-
return
|
|
7772
|
+
const worktreeFile = path36.join(forgehiveDir2, "agents", agent, "worktree");
|
|
7773
|
+
if (fs35.existsSync(worktreeFile)) {
|
|
7774
|
+
return fs35.readFileSync(worktreeFile, "utf8").trim();
|
|
7170
7775
|
}
|
|
7171
7776
|
return void 0;
|
|
7172
7777
|
}
|
|
@@ -7195,17 +7800,18 @@ function formatRoutingResult(result, worktreePath) {
|
|
|
7195
7800
|
|
|
7196
7801
|
// src/github.ts
|
|
7197
7802
|
init_js_yaml();
|
|
7198
|
-
import
|
|
7199
|
-
import
|
|
7803
|
+
import fs36 from "node:fs";
|
|
7804
|
+
import path37 from "node:path";
|
|
7200
7805
|
import https from "node:https";
|
|
7806
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
7201
7807
|
function loadGitHubConfig(forgehiveDir2) {
|
|
7202
|
-
const configPath =
|
|
7203
|
-
if (!
|
|
7808
|
+
const configPath = path37.join(forgehiveDir2, "github.yaml");
|
|
7809
|
+
if (!fs36.existsSync(configPath)) {
|
|
7204
7810
|
throw new Error("Nicht konfiguriert. F\xFChre zuerst aus: fh github setup");
|
|
7205
7811
|
}
|
|
7206
7812
|
let parsed;
|
|
7207
7813
|
try {
|
|
7208
|
-
parsed = jsYaml.load(
|
|
7814
|
+
parsed = jsYaml.load(fs36.readFileSync(configPath, "utf8"));
|
|
7209
7815
|
} catch {
|
|
7210
7816
|
throw new Error("Ung\xFCltige Konfigurationsdatei: .forgehive/github.yaml ist kein g\xFCltiges YAML.");
|
|
7211
7817
|
}
|
|
@@ -7219,9 +7825,9 @@ function loadGitHubConfig(forgehiveDir2) {
|
|
|
7219
7825
|
return { repo: data.repo };
|
|
7220
7826
|
}
|
|
7221
7827
|
function saveGitHubConfig(forgehiveDir2, config) {
|
|
7222
|
-
|
|
7223
|
-
const configPath =
|
|
7224
|
-
|
|
7828
|
+
fs36.mkdirSync(forgehiveDir2, { recursive: true });
|
|
7829
|
+
const configPath = path37.join(forgehiveDir2, "github.yaml");
|
|
7830
|
+
fs36.writeFileSync(configPath, jsYaml.dump({ repo: config.repo }), "utf8");
|
|
7225
7831
|
}
|
|
7226
7832
|
function githubGet(apiPath, token) {
|
|
7227
7833
|
return new Promise((resolve, reject) => {
|
|
@@ -7303,22 +7909,22 @@ async function fetchPRFiles(owner, repo, number, token) {
|
|
|
7303
7909
|
return files;
|
|
7304
7910
|
}
|
|
7305
7911
|
function issueAlreadySynced(storiesDir, issueNumber) {
|
|
7306
|
-
if (!
|
|
7912
|
+
if (!fs36.existsSync(storiesDir)) return false;
|
|
7307
7913
|
const pattern = new RegExp(`GitHub: #${issueNumber}(?![\\d\\w])`);
|
|
7308
|
-
return
|
|
7914
|
+
return fs36.readdirSync(storiesDir).filter((f) => f.endsWith(".md")).some((f) => pattern.test(fs36.readFileSync(path37.join(storiesDir, f), "utf8")));
|
|
7309
7915
|
}
|
|
7310
7916
|
function appendGitHubRef(storiesDir, storyId, issue, repoFullName) {
|
|
7311
|
-
const filePath =
|
|
7312
|
-
if (!
|
|
7917
|
+
const filePath = path37.join(storiesDir, `${storyId}.md`);
|
|
7918
|
+
if (!fs36.existsSync(filePath)) {
|
|
7313
7919
|
throw new Error(`Story-Datei nicht gefunden: ${storyId}.md`);
|
|
7314
7920
|
}
|
|
7315
|
-
const existing =
|
|
7921
|
+
const existing = fs36.readFileSync(filePath, "utf8");
|
|
7316
7922
|
const ref = `
|
|
7317
7923
|
## GitHub
|
|
7318
7924
|
|
|
7319
7925
|
GitHub: #${issue.number} \u2014 ${issue.html_url}
|
|
7320
7926
|
`;
|
|
7321
|
-
|
|
7927
|
+
fs36.writeFileSync(filePath, existing.trimEnd() + "\n" + ref, "utf8");
|
|
7322
7928
|
}
|
|
7323
7929
|
function formatPRContext(pr, files, repoFullName) {
|
|
7324
7930
|
const body = pr.body?.trim() || "(keine Beschreibung)";
|
|
@@ -7339,6 +7945,381 @@ function formatPRContext(pr, files, repoFullName) {
|
|
|
7339
7945
|
fileLines || "(keine Dateien)"
|
|
7340
7946
|
].join("\n");
|
|
7341
7947
|
}
|
|
7948
|
+
async function fetchOpenPRs(owner, repo, token) {
|
|
7949
|
+
const prs = [];
|
|
7950
|
+
let page = 1;
|
|
7951
|
+
while (true) {
|
|
7952
|
+
const batch = await githubGet(
|
|
7953
|
+
`/repos/${owner}/${repo}/pulls?state=open&per_page=100&page=${page}`,
|
|
7954
|
+
token
|
|
7955
|
+
);
|
|
7956
|
+
if (batch.length === 0) break;
|
|
7957
|
+
prs.push(...batch);
|
|
7958
|
+
if (batch.length < 100) break;
|
|
7959
|
+
page++;
|
|
7960
|
+
}
|
|
7961
|
+
return prs.map((pr) => pr.number);
|
|
7962
|
+
}
|
|
7963
|
+
function createPR(branch, title, body) {
|
|
7964
|
+
const result = spawnSync13(
|
|
7965
|
+
"gh",
|
|
7966
|
+
["pr", "create", "--title", title, "--body", body, "--head", branch],
|
|
7967
|
+
{ encoding: "utf8" }
|
|
7968
|
+
);
|
|
7969
|
+
if (result.status !== 0) {
|
|
7970
|
+
throw new Error(`gh pr create fehlgeschlagen: ${result.stderr?.trim() ?? "unbekannter Fehler"}`);
|
|
7971
|
+
}
|
|
7972
|
+
return result.stdout.trim();
|
|
7973
|
+
}
|
|
7974
|
+
|
|
7975
|
+
// src/autonomous.ts
|
|
7976
|
+
init_js_yaml();
|
|
7977
|
+
import fs38 from "node:fs";
|
|
7978
|
+
import path39 from "node:path";
|
|
7979
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
7980
|
+
import { createInterface } from "node:readline";
|
|
7981
|
+
|
|
7982
|
+
// src/prompt-builder.ts
|
|
7983
|
+
import fs37 from "node:fs";
|
|
7984
|
+
import path38 from "node:path";
|
|
7985
|
+
function buildPlanningPrompt(storyContent, capabilitiesPath, forgehiveDir2) {
|
|
7986
|
+
const capabilities = fs37.existsSync(capabilitiesPath) ? fs37.readFileSync(capabilitiesPath, "utf8") : "";
|
|
7987
|
+
const feedbackPath = path38.join(forgehiveDir2, "memory", "feedback.md");
|
|
7988
|
+
const projectPath = path38.join(forgehiveDir2, "memory", "project.md");
|
|
7989
|
+
const memory = [feedbackPath, projectPath].filter((p) => fs37.existsSync(p)).map((p) => fs37.readFileSync(p, "utf8")).join("\n\n");
|
|
7990
|
+
return [
|
|
7991
|
+
"Du bist ein Planungs-Agent. Produziere einen Implementierungsplan als Markdown.",
|
|
7992
|
+
"",
|
|
7993
|
+
"## Story",
|
|
7994
|
+
storyContent,
|
|
7995
|
+
"",
|
|
7996
|
+
"## Stack",
|
|
7997
|
+
capabilities,
|
|
7998
|
+
...memory ? ["", "## Memory", memory] : [],
|
|
7999
|
+
"",
|
|
8000
|
+
"## Ausgabeformat",
|
|
8001
|
+
"### Files",
|
|
8002
|
+
"(Liste der zu \xE4ndernden/erstellenden Dateipfade, einer pro Zeile)",
|
|
8003
|
+
"### Ansatz",
|
|
8004
|
+
"(2-3 S\xE4tze Beschreibung des Vorgehens)",
|
|
8005
|
+
"### Tests",
|
|
8006
|
+
"(Liste der erwarteten Tests die danach gr\xFCn sein m\xFCssen)"
|
|
8007
|
+
].join("\n");
|
|
8008
|
+
}
|
|
8009
|
+
function buildImplementationPrompt(storyContent, planContent, capabilitiesPath, forgehiveDir2) {
|
|
8010
|
+
const capabilities = fs37.existsSync(capabilitiesPath) ? fs37.readFileSync(capabilitiesPath, "utf8") : "";
|
|
8011
|
+
const skillsDir = path38.join(forgehiveDir2, "skills", "expert");
|
|
8012
|
+
const skills = fs37.existsSync(skillsDir) ? fs37.readdirSync(skillsDir).filter((f) => f.endsWith(".md")).slice(0, 3).map((f) => fs37.readFileSync(path38.join(skillsDir, f), "utf8")).join("\n\n---\n\n") : "";
|
|
8013
|
+
return [
|
|
8014
|
+
"Du bist Kai, Senior Software Engineer. Implementiere den folgenden Plan.",
|
|
8015
|
+
"Erstelle einen git-Commit wenn du fertig bist.",
|
|
8016
|
+
"\xC4ndere NUR die Dateien die im Plan aufgelistet sind.",
|
|
8017
|
+
"",
|
|
8018
|
+
"## Story",
|
|
8019
|
+
storyContent,
|
|
8020
|
+
"",
|
|
8021
|
+
"## Plan",
|
|
8022
|
+
planContent,
|
|
8023
|
+
"",
|
|
8024
|
+
"## Stack",
|
|
8025
|
+
capabilities,
|
|
8026
|
+
...skills ? ["", "## Skills", skills] : []
|
|
8027
|
+
].join("\n");
|
|
8028
|
+
}
|
|
8029
|
+
function buildEscalationPrompt(planContent, diff, testError) {
|
|
8030
|
+
return [
|
|
8031
|
+
"Du bist Sam, QA Architect. Kai hat versucht den Plan umzusetzen aber die Tests schlagen fehl.",
|
|
8032
|
+
"Analysiere was schiefgelaufen ist und produziere einen minimalen Fix.",
|
|
8033
|
+
"Kein neuer Plan \u2014 nur der Fix. Committe danach.",
|
|
8034
|
+
"",
|
|
8035
|
+
"## Originaler Plan",
|
|
8036
|
+
planContent,
|
|
8037
|
+
"",
|
|
8038
|
+
"## Was produziert wurde (git diff)",
|
|
8039
|
+
diff,
|
|
8040
|
+
"",
|
|
8041
|
+
"## Test-Fehler",
|
|
8042
|
+
testError
|
|
8043
|
+
].join("\n");
|
|
8044
|
+
}
|
|
8045
|
+
|
|
8046
|
+
// src/autonomous.ts
|
|
8047
|
+
function generateRunId() {
|
|
8048
|
+
const now = /* @__PURE__ */ new Date();
|
|
8049
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8050
|
+
return `run-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
8051
|
+
}
|
|
8052
|
+
function getRunDir(forgehiveDir2, runId) {
|
|
8053
|
+
return path39.join(forgehiveDir2, "runs", runId);
|
|
8054
|
+
}
|
|
8055
|
+
function saveRunState(forgehiveDir2, state) {
|
|
8056
|
+
const runDir = getRunDir(forgehiveDir2, state.runId);
|
|
8057
|
+
fs38.mkdirSync(runDir, { recursive: true });
|
|
8058
|
+
fs38.writeFileSync(path39.join(runDir, "state.yaml"), jsYaml.dump(state), "utf8");
|
|
8059
|
+
}
|
|
8060
|
+
function loadRunState(forgehiveDir2, runId) {
|
|
8061
|
+
const statePath = path39.join(getRunDir(forgehiveDir2, runId), "state.yaml");
|
|
8062
|
+
if (!fs38.existsSync(statePath)) {
|
|
8063
|
+
throw new Error(`Run ${runId} nicht gefunden`);
|
|
8064
|
+
}
|
|
8065
|
+
return jsYaml.load(fs38.readFileSync(statePath, "utf8"));
|
|
8066
|
+
}
|
|
8067
|
+
function listRuns(forgehiveDir2) {
|
|
8068
|
+
const runsDir = path39.join(forgehiveDir2, "runs");
|
|
8069
|
+
if (!fs38.existsSync(runsDir)) return [];
|
|
8070
|
+
return fs38.readdirSync(runsDir).filter((d) => d.startsWith("run-")).sort().reverse().map((d) => {
|
|
8071
|
+
try {
|
|
8072
|
+
return loadRunState(forgehiveDir2, d);
|
|
8073
|
+
} catch {
|
|
8074
|
+
return null;
|
|
8075
|
+
}
|
|
8076
|
+
}).filter((s) => s !== null);
|
|
8077
|
+
}
|
|
8078
|
+
function nextPhase(current, ctx) {
|
|
8079
|
+
switch (current) {
|
|
8080
|
+
case "analyzing":
|
|
8081
|
+
return "planning";
|
|
8082
|
+
case "planning":
|
|
8083
|
+
return "gate-1";
|
|
8084
|
+
case "gate-1":
|
|
8085
|
+
return "implementing";
|
|
8086
|
+
case "implementing":
|
|
8087
|
+
return "testing";
|
|
8088
|
+
case "testing":
|
|
8089
|
+
if (ctx.testsPassed) return "gate-2";
|
|
8090
|
+
return ctx.attempts >= 2 ? "gate-stuck" : "escalating";
|
|
8091
|
+
case "escalating":
|
|
8092
|
+
return "testing";
|
|
8093
|
+
case "gate-2":
|
|
8094
|
+
return "pr-creating";
|
|
8095
|
+
case "pr-creating":
|
|
8096
|
+
return "done";
|
|
8097
|
+
default:
|
|
8098
|
+
return current;
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
function generateBranchName(entryPoint) {
|
|
8102
|
+
if (/^US-\d+$/.test(entryPoint)) return `feat/${entryPoint}`;
|
|
8103
|
+
const slug = entryPoint.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 40).replace(/-+$/, "");
|
|
8104
|
+
return `feat/${slug}`;
|
|
8105
|
+
}
|
|
8106
|
+
function shouldSkipGate(phase, auto) {
|
|
8107
|
+
if (phase === "gate-stuck") return false;
|
|
8108
|
+
return auto && (phase === "gate-1" || phase === "gate-2");
|
|
8109
|
+
}
|
|
8110
|
+
function formatRunStatus(state) {
|
|
8111
|
+
const lines = [
|
|
8112
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
8113
|
+
`fh auto \u203A ${state.entryPoint} \u203A ${state.status}`,
|
|
8114
|
+
`Run-ID: ${state.runId}`,
|
|
8115
|
+
`Branch: ${state.branch}`,
|
|
8116
|
+
`Agent: ${state.agent}`,
|
|
8117
|
+
`Start: ${state.startedAt}`,
|
|
8118
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
|
|
8119
|
+
];
|
|
8120
|
+
return lines.join("\n");
|
|
8121
|
+
}
|
|
8122
|
+
async function promptUser(question) {
|
|
8123
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8124
|
+
return new Promise((resolve) => {
|
|
8125
|
+
rl.question(question, (answer) => {
|
|
8126
|
+
rl.close();
|
|
8127
|
+
resolve(answer.trim());
|
|
8128
|
+
});
|
|
8129
|
+
});
|
|
8130
|
+
}
|
|
8131
|
+
function runClaudeP(prompt) {
|
|
8132
|
+
const result = spawnSync14("claude", ["-p", prompt], { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
|
|
8133
|
+
return {
|
|
8134
|
+
stdout: result.stdout ?? "",
|
|
8135
|
+
success: result.status === 0
|
|
8136
|
+
};
|
|
8137
|
+
}
|
|
8138
|
+
function getTestCmd(forgehiveDir2) {
|
|
8139
|
+
const capPath = path39.join(forgehiveDir2, "capabilities.yaml");
|
|
8140
|
+
if (!fs38.existsSync(capPath)) return "npm test";
|
|
8141
|
+
try {
|
|
8142
|
+
const caps = jsYaml.load(fs38.readFileSync(capPath, "utf8"));
|
|
8143
|
+
return caps?.testCmd ?? "npm test";
|
|
8144
|
+
} catch {
|
|
8145
|
+
return "npm test";
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
function getDiff(projectRoot2) {
|
|
8149
|
+
const result = spawnSync14("git", ["diff", "HEAD"], { cwd: projectRoot2, encoding: "utf8" });
|
|
8150
|
+
return result.stdout ?? "";
|
|
8151
|
+
}
|
|
8152
|
+
async function startRun(entryPoint, forgehiveDir2, projectRoot2, auto) {
|
|
8153
|
+
const runId = generateRunId();
|
|
8154
|
+
const capPath = path39.join(forgehiveDir2, "capabilities.yaml");
|
|
8155
|
+
let storyContent;
|
|
8156
|
+
if (/^US-\d+$/.test(entryPoint)) {
|
|
8157
|
+
const storiesDir = path39.join(forgehiveDir2, "memory", "stories");
|
|
8158
|
+
const story = getStory(storiesDir, entryPoint);
|
|
8159
|
+
storyContent = story ? `${story.id}: ${story.title}` : entryPoint;
|
|
8160
|
+
} else {
|
|
8161
|
+
const storiesDir = path39.join(forgehiveDir2, "memory", "stories");
|
|
8162
|
+
fs38.mkdirSync(storiesDir, { recursive: true });
|
|
8163
|
+
const story = createStory(storiesDir, entryPoint);
|
|
8164
|
+
storyContent = `${story.id}: ${story.title}`;
|
|
8165
|
+
entryPoint = story.id;
|
|
8166
|
+
}
|
|
8167
|
+
const routing = routeTask(storyContent);
|
|
8168
|
+
const agent = routing.type === "agent" ? routing.agent ?? "kai" : "kai";
|
|
8169
|
+
const branch = generateBranchName(entryPoint);
|
|
8170
|
+
const state = {
|
|
8171
|
+
runId,
|
|
8172
|
+
entryPoint,
|
|
8173
|
+
status: "planning",
|
|
8174
|
+
agent,
|
|
8175
|
+
branch,
|
|
8176
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8177
|
+
attempts: 0,
|
|
8178
|
+
auto
|
|
8179
|
+
};
|
|
8180
|
+
saveRunState(forgehiveDir2, state);
|
|
8181
|
+
await executeRun(state, storyContent, forgehiveDir2, projectRoot2, capPath);
|
|
8182
|
+
}
|
|
8183
|
+
async function resumeRun(runId, forgehiveDir2, projectRoot2) {
|
|
8184
|
+
const state = loadRunState(forgehiveDir2, runId);
|
|
8185
|
+
const capPath = path39.join(forgehiveDir2, "capabilities.yaml");
|
|
8186
|
+
const storyContent = state.entryPoint;
|
|
8187
|
+
await executeRun(state, storyContent, forgehiveDir2, projectRoot2, capPath);
|
|
8188
|
+
}
|
|
8189
|
+
async function executeRun(state, storyContent, forgehiveDir2, projectRoot2, capPath) {
|
|
8190
|
+
const runDir = path39.join(forgehiveDir2, "runs", state.runId);
|
|
8191
|
+
if (state.status === "planning") {
|
|
8192
|
+
console.log(`
|
|
8193
|
+
[fh auto] Planning...`);
|
|
8194
|
+
const planPrompt = buildPlanningPrompt(storyContent, capPath, forgehiveDir2);
|
|
8195
|
+
const { stdout: planMd } = runClaudeP(planPrompt);
|
|
8196
|
+
fs38.writeFileSync(path39.join(runDir, "plan.md"), planMd, "utf8");
|
|
8197
|
+
state.status = "gate-1";
|
|
8198
|
+
saveRunState(forgehiveDir2, state);
|
|
8199
|
+
}
|
|
8200
|
+
if (state.status === "gate-1") {
|
|
8201
|
+
const planMd = fs38.readFileSync(path39.join(runDir, "plan.md"), "utf8");
|
|
8202
|
+
if (!shouldSkipGate("gate-1", state.auto)) {
|
|
8203
|
+
console.log(`
|
|
8204
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
8205
|
+
console.log(`fh auto \u203A ${state.entryPoint} \u203A Plan bereit`);
|
|
8206
|
+
console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
8207
|
+
console.log(planMd);
|
|
8208
|
+
const answer = await promptUser("Fortfahren? [j / n / anpassen: <freitext>]\n> ");
|
|
8209
|
+
if (answer.toLowerCase() === "n") {
|
|
8210
|
+
console.log("Abgebrochen.");
|
|
8211
|
+
return;
|
|
8212
|
+
}
|
|
8213
|
+
if (answer.toLowerCase() !== "j" && answer.toLowerCase() !== "ja") {
|
|
8214
|
+
const revisedPrompt = buildPlanningPrompt(
|
|
8215
|
+
`${storyContent}
|
|
8216
|
+
|
|
8217
|
+
Feedback: ${answer}`,
|
|
8218
|
+
capPath,
|
|
8219
|
+
forgehiveDir2
|
|
8220
|
+
);
|
|
8221
|
+
const { stdout: revisedPlan } = runClaudeP(revisedPrompt);
|
|
8222
|
+
fs38.writeFileSync(path39.join(runDir, "plan.md"), revisedPlan, "utf8");
|
|
8223
|
+
}
|
|
8224
|
+
}
|
|
8225
|
+
state.status = "implementing";
|
|
8226
|
+
saveRunState(forgehiveDir2, state);
|
|
8227
|
+
}
|
|
8228
|
+
if (state.status === "implementing") {
|
|
8229
|
+
console.log(`
|
|
8230
|
+
[fh auto] Implementing (${state.agent})...`);
|
|
8231
|
+
const planMd = fs38.readFileSync(path39.join(runDir, "plan.md"), "utf8");
|
|
8232
|
+
const implPrompt = buildImplementationPrompt(storyContent, planMd, capPath, forgehiveDir2);
|
|
8233
|
+
const branchResult = spawnSync14("git", ["checkout", "-b", state.branch], { cwd: projectRoot2, encoding: "utf8" });
|
|
8234
|
+
if (branchResult.status !== 0) {
|
|
8235
|
+
spawnSync14("git", ["checkout", state.branch], { cwd: projectRoot2, encoding: "utf8" });
|
|
8236
|
+
}
|
|
8237
|
+
runClaudeP(implPrompt);
|
|
8238
|
+
state.status = "testing";
|
|
8239
|
+
state.attempts += 1;
|
|
8240
|
+
saveRunState(forgehiveDir2, state);
|
|
8241
|
+
}
|
|
8242
|
+
if (state.status === "testing") {
|
|
8243
|
+
console.log(`
|
|
8244
|
+
[fh auto] Running tests...`);
|
|
8245
|
+
const testCmd = getTestCmd(forgehiveDir2);
|
|
8246
|
+
const [cmd, ...args] = testCmd.split(" ");
|
|
8247
|
+
const testResult = spawnSync14(cmd, args, { cwd: projectRoot2, encoding: "utf8" });
|
|
8248
|
+
const testsPassed = testResult.status === 0;
|
|
8249
|
+
const ctx = { testsPassed, attempts: state.attempts };
|
|
8250
|
+
state.status = nextPhase("testing", ctx);
|
|
8251
|
+
saveRunState(forgehiveDir2, state);
|
|
8252
|
+
if (!testsPassed && state.status === "escalating") {
|
|
8253
|
+
console.log(`
|
|
8254
|
+
[fh auto] Tests failed. Escalating to Sam...`);
|
|
8255
|
+
const planMd = fs38.readFileSync(path39.join(runDir, "plan.md"), "utf8");
|
|
8256
|
+
const diff = getDiff(projectRoot2);
|
|
8257
|
+
const escalationPrompt = buildEscalationPrompt(planMd, diff, testResult.stdout + testResult.stderr);
|
|
8258
|
+
runClaudeP(escalationPrompt);
|
|
8259
|
+
state.status = "testing";
|
|
8260
|
+
state.attempts += 1;
|
|
8261
|
+
saveRunState(forgehiveDir2, state);
|
|
8262
|
+
const retestResult = spawnSync14(cmd, args, { cwd: projectRoot2, encoding: "utf8" });
|
|
8263
|
+
const retestPassed = retestResult.status === 0;
|
|
8264
|
+
state.status = nextPhase("testing", { testsPassed: retestPassed, attempts: state.attempts });
|
|
8265
|
+
saveRunState(forgehiveDir2, state);
|
|
8266
|
+
if (!retestPassed && state.status === "gate-stuck") {
|
|
8267
|
+
const diff2 = getDiff(projectRoot2);
|
|
8268
|
+
fs38.writeFileSync(path39.join(runDir, "diff.patch"), diff2, "utf8");
|
|
8269
|
+
console.log(`
|
|
8270
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
8271
|
+
console.log(`fh auto \u203A ${state.entryPoint} \u203A Stuck nach 2 Versuchen`);
|
|
8272
|
+
console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
8273
|
+
console.log(`\u2717 ${retestResult.stderr?.trim().split("\n")[0] ?? "Tests fehlgeschlagen"}`);
|
|
8274
|
+
console.log(`
|
|
8275
|
+
Diff: ${path39.join(forgehiveDir2, "runs", state.runId, "diff.patch")}`);
|
|
8276
|
+
console.log(`Run pausiert. Wenn du bereit bist:
|
|
8277
|
+
fh auto resume ${state.runId}`);
|
|
8278
|
+
return;
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
}
|
|
8282
|
+
if (state.status === "gate-2") {
|
|
8283
|
+
const diff = getDiff(projectRoot2);
|
|
8284
|
+
fs38.writeFileSync(path39.join(runDir, "diff.patch"), diff, "utf8");
|
|
8285
|
+
const additions = (diff.match(/^\+[^+]/mg) ?? []).length;
|
|
8286
|
+
const deletions = (diff.match(/^-[^-]/mg) ?? []).length;
|
|
8287
|
+
if (!shouldSkipGate("gate-2", state.auto)) {
|
|
8288
|
+
console.log(`
|
|
8289
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
8290
|
+
console.log(`fh auto \u203A ${state.entryPoint} \u203A Tests gr\xFCn`);
|
|
8291
|
+
console.log(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
8292
|
+
console.log(`\u2713 Tests passing`);
|
|
8293
|
+
console.log(`Branch: ${state.branch}`);
|
|
8294
|
+
console.log(`Diff: +${additions} / -${deletions} Zeilen`);
|
|
8295
|
+
const answer = await promptUser("\nPR erstellen? [j / n]\n> ");
|
|
8296
|
+
if (answer.toLowerCase() === "n") {
|
|
8297
|
+
console.log("Branch behalten. Kein PR erstellt.");
|
|
8298
|
+
return;
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
state.status = "pr-creating";
|
|
8302
|
+
saveRunState(forgehiveDir2, state);
|
|
8303
|
+
}
|
|
8304
|
+
if (state.status === "pr-creating") {
|
|
8305
|
+
console.log(`
|
|
8306
|
+
[fh auto] Creating PR...`);
|
|
8307
|
+
const planMd = fs38.readFileSync(path39.join(runDir, "plan.md"), "utf8");
|
|
8308
|
+
try {
|
|
8309
|
+
spawnSync14("git", ["push", "-u", "origin", state.branch], { cwd: projectRoot2, encoding: "utf8" });
|
|
8310
|
+
const prUrl = createPR(state.branch, `${state.entryPoint}: ${storyContent}`, planMd);
|
|
8311
|
+
state.status = "done";
|
|
8312
|
+
saveRunState(forgehiveDir2, state);
|
|
8313
|
+
console.log(`
|
|
8314
|
+
\u2713 PR erstellt: ${prUrl}`);
|
|
8315
|
+
} catch (e) {
|
|
8316
|
+
console.warn(`PR konnte nicht erstellt werden: ${e.message}`);
|
|
8317
|
+
console.warn("Branch ist lokal verf\xFCgbar. Erstelle den PR manuell.");
|
|
8318
|
+
state.status = "done";
|
|
8319
|
+
saveRunState(forgehiveDir2, state);
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
8322
|
+
}
|
|
7342
8323
|
|
|
7343
8324
|
// src/cli.ts
|
|
7344
8325
|
import { createRequire } from "node:module";
|
|
@@ -7346,7 +8327,7 @@ var require2 = createRequire(import.meta.url);
|
|
|
7346
8327
|
var { version } = require2("../package.json");
|
|
7347
8328
|
var [, , command, subcommand, ...rest] = process.argv;
|
|
7348
8329
|
var projectRoot = process.cwd();
|
|
7349
|
-
var forgehiveDir =
|
|
8330
|
+
var forgehiveDir = path40.join(projectRoot, ".forgehive");
|
|
7350
8331
|
if (command === "--version" || command === "-v") {
|
|
7351
8332
|
console.log(version);
|
|
7352
8333
|
process.exit(0);
|
|
@@ -7362,7 +8343,15 @@ SETUP
|
|
|
7362
8343
|
fh init [--yes] Set up forgehive in the current project
|
|
7363
8344
|
fh confirm Activate capabilities (draft \u2192 confirmed)
|
|
7364
8345
|
fh rollback Remove forgehive from the project
|
|
8346
|
+
fh auto <US-N|text> Autonomer Pipeline-Run (plan \u2192 implement \u2192 test \u2192 PR)
|
|
8347
|
+
fh auto --auto <US-N|text> Ohne Gates (CI/overnight)
|
|
8348
|
+
fh auto resume <run-id> Unterbrochenen Run fortsetzen
|
|
8349
|
+
fh auto status Aktive und letzte Runs
|
|
8350
|
+
fh auto list Alle Runs auflisten
|
|
8351
|
+
|
|
7365
8352
|
fh status Show current project state
|
|
8353
|
+
fh next Zeigt n\xE4chste empfohlene Aktion (Phase + Befehl)
|
|
8354
|
+
fh watch [--smart] Observe project; --smart runs tests on change and suggests agents
|
|
7366
8355
|
fh scan --update Re-scan project after changes
|
|
7367
8356
|
fh scan --check Check if scan is still current
|
|
7368
8357
|
|
|
@@ -7418,6 +8407,7 @@ TEAM
|
|
|
7418
8407
|
|
|
7419
8408
|
AGENTS & MCP
|
|
7420
8409
|
fh party [--set name|run|status|cleanup]
|
|
8410
|
+
fh outcomes Show party/auto run statistics and last 5 runs
|
|
7421
8411
|
fh wire <service> Configure MCP server
|
|
7422
8412
|
fh mcp auth add|list|remove Manage credentials
|
|
7423
8413
|
fh mcp search <query> Search MCP registry
|
|
@@ -7433,24 +8423,24 @@ COST
|
|
|
7433
8423
|
process.exit(0);
|
|
7434
8424
|
}
|
|
7435
8425
|
function loadClaudeMdBlock() {
|
|
7436
|
-
const templatePath =
|
|
7437
|
-
|
|
8426
|
+
const templatePath = path40.join(
|
|
8427
|
+
path40.dirname(new URL(import.meta.url).pathname),
|
|
7438
8428
|
"..",
|
|
7439
8429
|
"forgehive",
|
|
7440
8430
|
"templates",
|
|
7441
8431
|
"claude-md.block.md"
|
|
7442
8432
|
);
|
|
7443
|
-
if (!
|
|
7444
|
-
return
|
|
8433
|
+
if (!fs39.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
|
|
8434
|
+
return fs39.readFileSync(templatePath, "utf8");
|
|
7445
8435
|
}
|
|
7446
8436
|
async function promptConfirm(question) {
|
|
7447
8437
|
if (!process.stdin.isTTY) return false;
|
|
7448
|
-
const rl =
|
|
8438
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
7449
8439
|
return new Promise((resolve) => {
|
|
7450
8440
|
rl.question(question, (answer) => {
|
|
7451
8441
|
rl.close();
|
|
7452
8442
|
const a = answer.trim().toLowerCase();
|
|
7453
|
-
resolve(a === "y" || a === "yes" || a === "");
|
|
8443
|
+
resolve(a === "y" || a === "yes" || a === "j" || a === "ja" || a === "");
|
|
7454
8444
|
});
|
|
7455
8445
|
});
|
|
7456
8446
|
}
|
|
@@ -7464,13 +8454,13 @@ function buildCapabilitySummary(ids) {
|
|
|
7464
8454
|
}
|
|
7465
8455
|
if (command === "init") {
|
|
7466
8456
|
(async () => {
|
|
7467
|
-
const gitCheck =
|
|
8457
|
+
const gitCheck = spawnSync15("git", ["--version"], { stdio: "ignore" });
|
|
7468
8458
|
if (gitCheck.error || gitCheck.status !== 0) {
|
|
7469
8459
|
console.error("Fehler: git nicht gefunden.");
|
|
7470
8460
|
console.error(" Installation: https://git-scm.com");
|
|
7471
8461
|
process.exit(1);
|
|
7472
8462
|
}
|
|
7473
|
-
const forgehiveDirExists =
|
|
8463
|
+
const forgehiveDirExists = fs39.existsSync(forgehiveDir);
|
|
7474
8464
|
if (forgehiveDirExists && subcommand !== "--force" && !rest.includes("--force")) {
|
|
7475
8465
|
console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
|
|
7476
8466
|
console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
|
|
@@ -7488,9 +8478,9 @@ if (command === "init") {
|
|
|
7488
8478
|
const block = loadClaudeMdBlock();
|
|
7489
8479
|
writeForgehiveDir(projectRoot, scanResult, capMap, block);
|
|
7490
8480
|
const hash = computeHash(projectRoot);
|
|
7491
|
-
|
|
7492
|
-
const runtimeDir =
|
|
7493
|
-
|
|
8481
|
+
fs39.writeFileSync(path40.join(forgehiveDir, ".scan-hash"), hash, "utf8");
|
|
8482
|
+
const runtimeDir = path40.join(
|
|
8483
|
+
path40.dirname(new URL(import.meta.url).pathname),
|
|
7494
8484
|
"..",
|
|
7495
8485
|
"forgehive"
|
|
7496
8486
|
);
|
|
@@ -7532,7 +8522,7 @@ if (command === "init") {
|
|
|
7532
8522
|
process.exit(1);
|
|
7533
8523
|
}
|
|
7534
8524
|
} else if (command === "memory") {
|
|
7535
|
-
if (!
|
|
8525
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7536
8526
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7537
8527
|
process.exit(1);
|
|
7538
8528
|
}
|
|
@@ -7541,7 +8531,7 @@ if (command === "init") {
|
|
|
7541
8531
|
} else if (subcommand === "clean") {
|
|
7542
8532
|
cleanMemory(forgehiveDir);
|
|
7543
8533
|
} else if (subcommand === "export") {
|
|
7544
|
-
const outputPath = rest[0] ??
|
|
8534
|
+
const outputPath = rest[0] ?? path40.join(projectRoot, "forgehive-memory-export.md");
|
|
7545
8535
|
try {
|
|
7546
8536
|
exportMemory(forgehiveDir, outputPath);
|
|
7547
8537
|
} catch (err) {
|
|
@@ -7589,7 +8579,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7589
8579
|
} else if (subcommand === "snapshot") {
|
|
7590
8580
|
const snapAction = rest[0];
|
|
7591
8581
|
if (snapAction === "export") {
|
|
7592
|
-
const outPath = rest[1] ??
|
|
8582
|
+
const outPath = rest[1] ?? path40.join(
|
|
7593
8583
|
projectRoot,
|
|
7594
8584
|
`forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
|
|
7595
8585
|
);
|
|
@@ -7629,11 +8619,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7629
8619
|
process.exit(1);
|
|
7630
8620
|
}
|
|
7631
8621
|
} else if (command === "scan" && subcommand === "--update") {
|
|
7632
|
-
if (!
|
|
8622
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7633
8623
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7634
8624
|
process.exit(1);
|
|
7635
8625
|
}
|
|
7636
|
-
const savedHash =
|
|
8626
|
+
const savedHash = fs39.existsSync(path40.join(forgehiveDir, ".scan-hash")) ? fs39.readFileSync(path40.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
|
|
7637
8627
|
const currentHash = computeHash(projectRoot);
|
|
7638
8628
|
if (savedHash === currentHash) {
|
|
7639
8629
|
console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
|
|
@@ -7641,7 +8631,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7641
8631
|
}
|
|
7642
8632
|
console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
|
|
7643
8633
|
const oldDoc = jsYaml.load(
|
|
7644
|
-
|
|
8634
|
+
fs39.readFileSync(path40.join(forgehiveDir, "capabilities.yaml"), "utf8")
|
|
7645
8635
|
);
|
|
7646
8636
|
const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
|
|
7647
8637
|
const scanResult = scan(projectRoot);
|
|
@@ -7661,16 +8651,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7661
8651
|
console.log();
|
|
7662
8652
|
const block = loadClaudeMdBlock();
|
|
7663
8653
|
writeForgehiveDir(projectRoot, scanResult, newMap, block);
|
|
7664
|
-
|
|
8654
|
+
fs39.writeFileSync(path40.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
|
|
7665
8655
|
console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
|
|
7666
8656
|
console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
|
|
7667
8657
|
}
|
|
7668
8658
|
} else if (command === "scan" && subcommand === "--check") {
|
|
7669
|
-
if (!
|
|
8659
|
+
if (!fs39.existsSync(path40.join(forgehiveDir, ".scan-hash"))) {
|
|
7670
8660
|
console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
|
|
7671
8661
|
process.exit(1);
|
|
7672
8662
|
}
|
|
7673
|
-
const saved =
|
|
8663
|
+
const saved = fs39.readFileSync(path40.join(forgehiveDir, ".scan-hash"), "utf8").trim();
|
|
7674
8664
|
const current = computeHash(projectRoot);
|
|
7675
8665
|
if (saved !== current) {
|
|
7676
8666
|
console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
|
|
@@ -7679,7 +8669,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7679
8669
|
}
|
|
7680
8670
|
console.log("\u2713 capabilities.yaml ist aktuell");
|
|
7681
8671
|
} else if (command === "skills") {
|
|
7682
|
-
if (!
|
|
8672
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7683
8673
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7684
8674
|
process.exit(1);
|
|
7685
8675
|
}
|
|
@@ -7715,7 +8705,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7715
8705
|
process.exit(1);
|
|
7716
8706
|
}
|
|
7717
8707
|
} else if (command === "party") {
|
|
7718
|
-
if (!
|
|
8708
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7719
8709
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7720
8710
|
process.exit(1);
|
|
7721
8711
|
}
|
|
@@ -7836,7 +8826,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7836
8826
|
}
|
|
7837
8827
|
}
|
|
7838
8828
|
} else if (command === "wire") {
|
|
7839
|
-
if (!
|
|
8829
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7840
8830
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7841
8831
|
process.exit(1);
|
|
7842
8832
|
}
|
|
@@ -7868,14 +8858,47 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7868
8858
|
console.error(`Fehler: ${err.message}`);
|
|
7869
8859
|
process.exit(1);
|
|
7870
8860
|
}
|
|
8861
|
+
} else if (command === "auto") {
|
|
8862
|
+
if (subcommand === "status" || subcommand === "list") {
|
|
8863
|
+
const runs = listRuns(forgehiveDir);
|
|
8864
|
+
if (runs.length === 0) {
|
|
8865
|
+
console.log("Keine Runs gefunden.");
|
|
8866
|
+
} else {
|
|
8867
|
+
runs.slice(0, 10).forEach((r) => console.log(formatRunStatus(r)));
|
|
8868
|
+
}
|
|
8869
|
+
process.exit(0);
|
|
8870
|
+
}
|
|
8871
|
+
if (subcommand === "resume") {
|
|
8872
|
+
const runId = rest[0];
|
|
8873
|
+
if (!runId) {
|
|
8874
|
+
console.error("Usage: fh auto resume <run-id>");
|
|
8875
|
+
process.exit(1);
|
|
8876
|
+
}
|
|
8877
|
+
await resumeRun(runId, forgehiveDir, projectRoot);
|
|
8878
|
+
process.exit(0);
|
|
8879
|
+
}
|
|
8880
|
+
const autoFlag = subcommand === "--auto";
|
|
8881
|
+
const entryPoint = autoFlag ? rest[0] : subcommand;
|
|
8882
|
+
if (!entryPoint) {
|
|
8883
|
+
console.error("Usage: fh auto <US-N|text>");
|
|
8884
|
+
process.exit(1);
|
|
8885
|
+
}
|
|
8886
|
+
await startRun(entryPoint, forgehiveDir, projectRoot, autoFlag);
|
|
8887
|
+
process.exit(0);
|
|
7871
8888
|
} else if (command === "status") {
|
|
7872
8889
|
console.log(projectStatus(projectRoot, forgehiveDir));
|
|
8890
|
+
const dashboard = formatDashboard(projectRoot, forgehiveDir);
|
|
8891
|
+
if (dashboard) console.log(dashboard);
|
|
8892
|
+
} else if (command === "next") {
|
|
8893
|
+
const { formatNextFull: formatNextFull2 } = await Promise.resolve().then(() => (init_next(), next_exports));
|
|
8894
|
+
console.log(formatNextFull2(projectRoot, forgehiveDir));
|
|
8895
|
+
process.exit(0);
|
|
7873
8896
|
} else if (command === "cost") {
|
|
7874
8897
|
const allArgs = [subcommand, ...rest].filter((a) => Boolean(a));
|
|
7875
8898
|
const limitIdx = allArgs.indexOf("--limit");
|
|
7876
8899
|
const alertIdx = allArgs.indexOf("--alert");
|
|
7877
8900
|
if (limitIdx !== -1) {
|
|
7878
|
-
if (!
|
|
8901
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7879
8902
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7880
8903
|
process.exit(1);
|
|
7881
8904
|
}
|
|
@@ -7901,30 +8924,57 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7901
8924
|
}
|
|
7902
8925
|
const sessions = parseCostSessions(projectRoot);
|
|
7903
8926
|
console.log(formatCostReport(sessions, range));
|
|
7904
|
-
if (
|
|
8927
|
+
if (fs39.existsSync(forgehiveDir)) {
|
|
7905
8928
|
const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
|
|
7906
8929
|
const status = checkSpendStatus(forgehiveDir, total);
|
|
7907
8930
|
if (status.message) console.log("\n" + status.message);
|
|
7908
8931
|
}
|
|
7909
8932
|
}
|
|
7910
8933
|
} else if (command === "watch") {
|
|
7911
|
-
if (!
|
|
8934
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7912
8935
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7913
8936
|
process.exit(1);
|
|
7914
8937
|
}
|
|
7915
|
-
const
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
8938
|
+
const isSmartMode = subcommand === "--smart" || rest.includes("--smart");
|
|
8939
|
+
if (isSmartMode) {
|
|
8940
|
+
let testCmd = "npm test";
|
|
8941
|
+
const capsPath = path40.join(forgehiveDir, "capabilities.yaml");
|
|
8942
|
+
if (fs39.existsSync(capsPath)) {
|
|
8943
|
+
try {
|
|
8944
|
+
const caps = jsYaml.load(fs39.readFileSync(capsPath, "utf8"));
|
|
8945
|
+
if (caps?.test_command) testCmd = caps.test_command;
|
|
8946
|
+
} catch {
|
|
8947
|
+
}
|
|
8948
|
+
}
|
|
8949
|
+
console.log(`\u{1F441} ForgeHive Smart Watch \u2014 Testbefehl: ${testCmd}`);
|
|
8950
|
+
console.log(" \xDCberwache src/ und test/ auf \xC4nderungen (Ctrl+C zum Beenden)\n");
|
|
8951
|
+
const stop = smartWatch(projectRoot, forgehiveDir, testCmd, async (agent, _reason, file) => {
|
|
8952
|
+
console.log(` Soll ich ${agent} starten f\xFCr ${file}?`);
|
|
8953
|
+
return promptConfirm(" [j/n] ");
|
|
8954
|
+
});
|
|
8955
|
+
process.on("SIGINT", () => {
|
|
8956
|
+
stop();
|
|
8957
|
+
process.exit(0);
|
|
8958
|
+
});
|
|
8959
|
+
process.on("SIGTERM", () => {
|
|
8960
|
+
stop();
|
|
8961
|
+
process.exit(0);
|
|
8962
|
+
});
|
|
8963
|
+
} else {
|
|
8964
|
+
const block = loadClaudeMdBlock();
|
|
8965
|
+
console.log("\u{1F441} ForgeHive watch gestartet \u2014 beobachte Projekt-Dateien (Ctrl+C zum Beenden)\n");
|
|
8966
|
+
const stop = watchProject(projectRoot, forgehiveDir, block);
|
|
8967
|
+
process.on("SIGINT", () => {
|
|
8968
|
+
stop();
|
|
8969
|
+
process.exit(0);
|
|
8970
|
+
});
|
|
8971
|
+
process.on("SIGTERM", () => {
|
|
8972
|
+
stop();
|
|
8973
|
+
process.exit(0);
|
|
8974
|
+
});
|
|
8975
|
+
}
|
|
7926
8976
|
} else if (command === "mcp") {
|
|
7927
|
-
if (!
|
|
8977
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7928
8978
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7929
8979
|
process.exit(1);
|
|
7930
8980
|
}
|
|
@@ -8035,7 +9085,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8035
9085
|
process.exit(1);
|
|
8036
9086
|
}
|
|
8037
9087
|
} else if (command === "security") {
|
|
8038
|
-
if (!
|
|
9088
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8039
9089
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8040
9090
|
process.exit(1);
|
|
8041
9091
|
}
|
|
@@ -8088,6 +9138,11 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8088
9138
|
high: high.length
|
|
8089
9139
|
}
|
|
8090
9140
|
});
|
|
9141
|
+
fs39.writeFileSync(
|
|
9142
|
+
path40.join(forgehiveDir, "security-last-scan.txt"),
|
|
9143
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
9144
|
+
"utf8"
|
|
9145
|
+
);
|
|
8091
9146
|
if (secrets.length > 0 || critical.length > 0 || high.length > 0) {
|
|
8092
9147
|
process.exit(1);
|
|
8093
9148
|
}
|
|
@@ -8121,8 +9176,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8121
9176
|
`);
|
|
8122
9177
|
const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
|
|
8123
9178
|
const text = formatSecurityReport(report);
|
|
8124
|
-
const reportPath =
|
|
8125
|
-
|
|
9179
|
+
const reportPath = path40.join(forgehiveDir, "security-report.md");
|
|
9180
|
+
fs39.writeFileSync(reportPath, text, "utf8");
|
|
8126
9181
|
console.log(text);
|
|
8127
9182
|
console.log(`
|
|
8128
9183
|
\u2713 Report gespeichert: ${reportPath}`);
|
|
@@ -8134,8 +9189,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8134
9189
|
} else if (subcommand === "permissions") {
|
|
8135
9190
|
const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
8136
9191
|
writePermissions2(forgehiveDir);
|
|
8137
|
-
const permPath =
|
|
8138
|
-
console.log(
|
|
9192
|
+
const permPath = path40.join(forgehiveDir, "harness", "permissions.yaml");
|
|
9193
|
+
console.log(fs39.readFileSync(permPath, "utf8"));
|
|
8139
9194
|
} else {
|
|
8140
9195
|
console.error(`Unbekannter security-Subcommand: ${subcommand}`);
|
|
8141
9196
|
console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
|
|
@@ -8147,18 +9202,18 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8147
9202
|
const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
|
|
8148
9203
|
const initFlag = allCiArgs.includes("--init");
|
|
8149
9204
|
if (initFlag) {
|
|
8150
|
-
const ghDir =
|
|
8151
|
-
|
|
8152
|
-
const outPath =
|
|
8153
|
-
|
|
9205
|
+
const ghDir = path40.join(projectRoot, ".github", "workflows");
|
|
9206
|
+
fs39.mkdirSync(ghDir, { recursive: true });
|
|
9207
|
+
const outPath = path40.join(ghDir, "forgehive.yml");
|
|
9208
|
+
fs39.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
|
|
8154
9209
|
console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
|
|
8155
9210
|
} else {
|
|
8156
9211
|
const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
|
|
8157
9212
|
const output = formatCiReport(report, format);
|
|
8158
9213
|
console.log(output);
|
|
8159
9214
|
if (format === "json") {
|
|
8160
|
-
|
|
8161
|
-
|
|
9215
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
9216
|
+
fs39.writeFileSync(path40.join(forgehiveDir, "ci-report.json"), output, "utf8");
|
|
8162
9217
|
}
|
|
8163
9218
|
if (report.status === "fail") process.exit(1);
|
|
8164
9219
|
}
|
|
@@ -8166,29 +9221,29 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8166
9221
|
const semanticFlag = rest.includes("--semantic");
|
|
8167
9222
|
const map2 = generateMap(projectRoot);
|
|
8168
9223
|
const semantic = buildSemanticMap(projectRoot);
|
|
8169
|
-
const mapPath =
|
|
8170
|
-
|
|
9224
|
+
const mapPath = path40.join(forgehiveDir, "map.md");
|
|
9225
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
8171
9226
|
if (semanticFlag) {
|
|
8172
9227
|
const fullMd = formatMap(map2, semantic);
|
|
8173
9228
|
const marker = "\n## Semantic Graph\n";
|
|
8174
9229
|
const markerIdx = fullMd.indexOf(marker);
|
|
8175
9230
|
const semanticMd = markerIdx >= 0 ? "## Semantic Graph\n" + fullMd.slice(markerIdx + marker.length) : fullMd;
|
|
8176
|
-
|
|
9231
|
+
fs39.writeFileSync(mapPath, fullMd, "utf8");
|
|
8177
9232
|
console.log(semanticMd);
|
|
8178
9233
|
console.log(`
|
|
8179
9234
|
\u2714 Codebase-Map gespeichert (Semantic Graph hervorgehoben): ${mapPath}`);
|
|
8180
9235
|
} else {
|
|
8181
9236
|
const md = formatMap(map2, semantic);
|
|
8182
|
-
|
|
9237
|
+
fs39.writeFileSync(mapPath, md, "utf8");
|
|
8183
9238
|
console.log(md);
|
|
8184
9239
|
console.log(`
|
|
8185
9240
|
\u2714 Codebase-Map gespeichert: ${mapPath}`);
|
|
8186
9241
|
}
|
|
8187
9242
|
} else if (command === "onboard") {
|
|
8188
9243
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8189
|
-
const outputPath = outputArg ??
|
|
9244
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "ONBOARDING.md");
|
|
8190
9245
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
8191
|
-
|
|
9246
|
+
fs39.writeFileSync(outputPath, doc, "utf8");
|
|
8192
9247
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
8193
9248
|
} else if (command === "ask") {
|
|
8194
9249
|
const taskArg = subcommand ? [subcommand, ...rest.filter((r) => !r.startsWith("--"))].join(" ") : rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
@@ -8211,28 +9266,28 @@ Task: "${taskArg}"
|
|
|
8211
9266
|
const commits = parseGitLog(rawLog);
|
|
8212
9267
|
let version2 = "unreleased";
|
|
8213
9268
|
try {
|
|
8214
|
-
const pkgPath =
|
|
8215
|
-
if (
|
|
8216
|
-
const pkg = JSON.parse(
|
|
9269
|
+
const pkgPath = path40.join(projectRoot, "package.json");
|
|
9270
|
+
if (fs39.existsSync(pkgPath)) {
|
|
9271
|
+
const pkg = JSON.parse(fs39.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
|
|
8217
9272
|
version2 = pkg.version ?? "unreleased";
|
|
8218
9273
|
}
|
|
8219
9274
|
} catch {
|
|
8220
9275
|
}
|
|
8221
9276
|
const md = formatChangelog(commits, version2);
|
|
8222
|
-
const outputPath = outputArg ??
|
|
9277
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "CHANGELOG.md");
|
|
8223
9278
|
let existing = "";
|
|
8224
|
-
if (
|
|
8225
|
-
|
|
9279
|
+
if (fs39.existsSync(outputPath)) existing = fs39.readFileSync(outputPath, "utf8");
|
|
9280
|
+
fs39.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
8226
9281
|
console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
|
|
8227
9282
|
console.log(` ${commits.length} Commits verarbeitet`);
|
|
8228
9283
|
} else if (command === "metrics") {
|
|
8229
9284
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
|
|
8230
9285
|
const rawLog = getMetricsGitLog(projectRoot, sinceArg);
|
|
8231
9286
|
const stats = parseCommitStats(rawLog);
|
|
8232
|
-
const md = formatMetrics(stats,
|
|
8233
|
-
const metricsPath =
|
|
8234
|
-
|
|
8235
|
-
|
|
9287
|
+
const md = formatMetrics(stats, path40.basename(projectRoot));
|
|
9288
|
+
const metricsPath = path40.join(forgehiveDir, "metrics.md");
|
|
9289
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
9290
|
+
fs39.writeFileSync(metricsPath, md, "utf8");
|
|
8236
9291
|
console.log(md);
|
|
8237
9292
|
console.log(`
|
|
8238
9293
|
\u2714 Metrics gespeichert: ${metricsPath}`);
|
|
@@ -8279,7 +9334,7 @@ Task: "${taskDescription}"
|
|
|
8279
9334
|
console.log(` fh run status \u2014 aktive Sessions anzeigen`);
|
|
8280
9335
|
}
|
|
8281
9336
|
} else if (command === "story") {
|
|
8282
|
-
const storiesDir =
|
|
9337
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8283
9338
|
if (subcommand === "create") {
|
|
8284
9339
|
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
8285
9340
|
const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
|
|
@@ -8290,7 +9345,7 @@ Task: "${taskDescription}"
|
|
|
8290
9345
|
}
|
|
8291
9346
|
const story = createStory(storiesDir, title, epicArg);
|
|
8292
9347
|
if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
|
|
8293
|
-
console.log(`\u2714 ${story.id} erstellt: ${
|
|
9348
|
+
console.log(`\u2714 ${story.id} erstellt: ${path40.join(storiesDir, story.id + ".md")}`);
|
|
8294
9349
|
console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
|
|
8295
9350
|
} else if (subcommand === "list") {
|
|
8296
9351
|
const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
|
|
@@ -8339,8 +9394,8 @@ Task: "${taskDescription}"
|
|
|
8339
9394
|
console.error("Verf\xFCgbar: fh story create | list | show | done");
|
|
8340
9395
|
}
|
|
8341
9396
|
} else if (command === "epic") {
|
|
8342
|
-
const epicsDir =
|
|
8343
|
-
const storiesDir =
|
|
9397
|
+
const epicsDir = path40.join(forgehiveDir, "memory", "epics");
|
|
9398
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8344
9399
|
if (subcommand === "create") {
|
|
8345
9400
|
const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
|
|
8346
9401
|
const prdArg = rest.includes("--prd") ? rest[rest.indexOf("--prd") + 1] : void 0;
|
|
@@ -8358,12 +9413,12 @@ Task: "${taskDescription}"
|
|
|
8358
9413
|
process.exit(1);
|
|
8359
9414
|
}
|
|
8360
9415
|
if (prdArg) {
|
|
8361
|
-
const prdsDir =
|
|
9416
|
+
const prdsDir = path40.join(forgehiveDir, "memory", "prds");
|
|
8362
9417
|
const prd = getPrd(prdsDir, prdArg);
|
|
8363
9418
|
if (!prd) console.warn(`\u26A0 PRD ${prdArg} nicht gefunden \u2014 Epic wird trotzdem erstellt`);
|
|
8364
9419
|
}
|
|
8365
9420
|
const epic = createEpic(epicsDir, title, goalArg, prdArg);
|
|
8366
|
-
console.log(`\u2714 ${epic.id} erstellt: ${
|
|
9421
|
+
console.log(`\u2714 ${epic.id} erstellt: ${path40.join(epicsDir, epic.id + ".md")}`);
|
|
8367
9422
|
if (prdArg) console.log(` Verkn\xFCpft mit: ${prdArg}`);
|
|
8368
9423
|
} else if (subcommand === "list") {
|
|
8369
9424
|
const epics = listEpics(epicsDir);
|
|
@@ -8390,7 +9445,7 @@ Task: "${taskDescription}"
|
|
|
8390
9445
|
console.error("Verf\xFCgbar: fh epic create | list | show");
|
|
8391
9446
|
}
|
|
8392
9447
|
} else if (command === "product") {
|
|
8393
|
-
const prdsDir =
|
|
9448
|
+
const prdsDir = path40.join(forgehiveDir, "memory", "prds");
|
|
8394
9449
|
if (subcommand === "prd") {
|
|
8395
9450
|
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
8396
9451
|
if (!title) {
|
|
@@ -8398,7 +9453,7 @@ Task: "${taskDescription}"
|
|
|
8398
9453
|
process.exit(1);
|
|
8399
9454
|
}
|
|
8400
9455
|
const prd = createPrd(prdsDir, title);
|
|
8401
|
-
const relPath =
|
|
9456
|
+
const relPath = path40.relative(projectRoot, prd.filepath);
|
|
8402
9457
|
console.log(`\u2714 ${prd.id} erstellt: ${relPath}`);
|
|
8403
9458
|
console.log(` Bearbeite die Datei um Anforderungen zu definieren.`);
|
|
8404
9459
|
} else if (subcommand === "list") {
|
|
@@ -8423,10 +9478,10 @@ Task: "${taskDescription}"
|
|
|
8423
9478
|
console.error(`${id} nicht gefunden`);
|
|
8424
9479
|
process.exit(1);
|
|
8425
9480
|
}
|
|
8426
|
-
console.log(
|
|
9481
|
+
console.log(fs39.readFileSync(prd.filepath, "utf8"));
|
|
8427
9482
|
} else if (subcommand === "status") {
|
|
8428
|
-
const epicsDir =
|
|
8429
|
-
const storiesDir =
|
|
9483
|
+
const epicsDir = path40.join(forgehiveDir, "memory", "epics");
|
|
9484
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8430
9485
|
const prds = listPrds(prdsDir);
|
|
8431
9486
|
const epics = listEpics(epicsDir);
|
|
8432
9487
|
const stories = listStories(storiesDir);
|
|
@@ -8469,14 +9524,14 @@ Task: "${taskDescription}"
|
|
|
8469
9524
|
}
|
|
8470
9525
|
} else if (subcommand === "roadmap") {
|
|
8471
9526
|
const content = generateRoadmap(forgehiveDir);
|
|
8472
|
-
const roadmapPath =
|
|
8473
|
-
|
|
9527
|
+
const roadmapPath = path40.join(projectRoot, "ROADMAP.md");
|
|
9528
|
+
fs39.writeFileSync(roadmapPath, content, "utf8");
|
|
8474
9529
|
console.log(`\u2714 ROADMAP.md generiert: ${roadmapPath}`);
|
|
8475
9530
|
} else {
|
|
8476
9531
|
console.error("Verf\xFCgbar: fh product prd | list | show | status | roadmap");
|
|
8477
9532
|
}
|
|
8478
9533
|
} else if (command === "velocity") {
|
|
8479
|
-
const velocityFile =
|
|
9534
|
+
const velocityFile = path40.join(forgehiveDir, "memory", "velocity.md");
|
|
8480
9535
|
if (subcommand === "record") {
|
|
8481
9536
|
const sprintNum = parseInt(rest[0] ?? "0", 10);
|
|
8482
9537
|
const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
|
|
@@ -8495,7 +9550,7 @@ Task: "${taskDescription}"
|
|
|
8495
9550
|
console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
|
|
8496
9551
|
}
|
|
8497
9552
|
} else if (command === "docs") {
|
|
8498
|
-
const docsDir =
|
|
9553
|
+
const docsDir = path40.join(projectRoot, "docs");
|
|
8499
9554
|
if (!subcommand || subcommand === "list") {
|
|
8500
9555
|
const existing = listExistingDocs(projectRoot);
|
|
8501
9556
|
if (existing.length === 0) {
|
|
@@ -8509,29 +9564,29 @@ Task: "${taskDescription}"
|
|
|
8509
9564
|
console.log(" fh docs adr <titel> \u2014 Architecture Decision Record");
|
|
8510
9565
|
} else {
|
|
8511
9566
|
console.log(`Vorhandene Dokumentation (${existing.length} Dateien):`);
|
|
8512
|
-
for (const d of existing) console.log(` ${
|
|
9567
|
+
for (const d of existing) console.log(` ${path40.relative(projectRoot, d)}`);
|
|
8513
9568
|
console.log("");
|
|
8514
9569
|
console.log("Aktualisieren: fh docs user | api | onboard | changelog");
|
|
8515
9570
|
}
|
|
8516
9571
|
} else if (subcommand === "user") {
|
|
8517
|
-
|
|
9572
|
+
fs39.mkdirSync(docsDir, { recursive: true });
|
|
8518
9573
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8519
|
-
const outputPath = outputArg ??
|
|
9574
|
+
const outputPath = outputArg ?? path40.join(docsDir, "user-guide.md");
|
|
8520
9575
|
const guide = generateUserGuide(projectRoot, forgehiveDir);
|
|
8521
|
-
|
|
9576
|
+
fs39.writeFileSync(outputPath, guide, "utf8");
|
|
8522
9577
|
console.log(`\u2714 User Guide geschrieben: ${outputPath}`);
|
|
8523
9578
|
} else if (subcommand === "api") {
|
|
8524
|
-
|
|
9579
|
+
fs39.mkdirSync(docsDir, { recursive: true });
|
|
8525
9580
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8526
|
-
const outputPath = outputArg ??
|
|
9581
|
+
const outputPath = outputArg ?? path40.join(docsDir, "api.md");
|
|
8527
9582
|
const ref = generateApiReference(projectRoot);
|
|
8528
|
-
|
|
9583
|
+
fs39.writeFileSync(outputPath, ref, "utf8");
|
|
8529
9584
|
console.log(`\u2714 API-Referenz geschrieben: ${outputPath}`);
|
|
8530
9585
|
} else if (subcommand === "onboard") {
|
|
8531
9586
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8532
|
-
const outputPath = outputArg ??
|
|
9587
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "ONBOARDING.md");
|
|
8533
9588
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
8534
|
-
|
|
9589
|
+
fs39.writeFileSync(outputPath, doc, "utf8");
|
|
8535
9590
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
8536
9591
|
} else if (subcommand === "changelog") {
|
|
8537
9592
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
@@ -8541,15 +9596,15 @@ Task: "${taskDescription}"
|
|
|
8541
9596
|
const commits = parseGitLog(rawLog);
|
|
8542
9597
|
let pkg = {};
|
|
8543
9598
|
try {
|
|
8544
|
-
pkg = JSON.parse(
|
|
9599
|
+
pkg = JSON.parse(fs39.readFileSync(path40.join(projectRoot, "package.json"), "utf8"));
|
|
8545
9600
|
} catch {
|
|
8546
9601
|
}
|
|
8547
9602
|
const pkgVersion = pkg.version ?? "unreleased";
|
|
8548
9603
|
const md = formatChangelog(commits, pkgVersion);
|
|
8549
|
-
const outputPath = outputArg ??
|
|
9604
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "CHANGELOG.md");
|
|
8550
9605
|
let existing = "";
|
|
8551
|
-
if (
|
|
8552
|
-
|
|
9606
|
+
if (fs39.existsSync(outputPath)) existing = fs39.readFileSync(outputPath, "utf8");
|
|
9607
|
+
fs39.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
8553
9608
|
console.log(`\u2714 CHANGELOG.md aktualisiert (${commits.length} Commits)`);
|
|
8554
9609
|
} else if (subcommand === "adr") {
|
|
8555
9610
|
const title = rest.join(" ");
|
|
@@ -8557,9 +9612,9 @@ Task: "${taskDescription}"
|
|
|
8557
9612
|
console.error("Usage: fh docs adr <titel>");
|
|
8558
9613
|
process.exit(1);
|
|
8559
9614
|
}
|
|
8560
|
-
const adrsDir =
|
|
8561
|
-
|
|
8562
|
-
const existing =
|
|
9615
|
+
const adrsDir = path40.join(forgehiveDir, "memory", "adrs");
|
|
9616
|
+
fs39.mkdirSync(adrsDir, { recursive: true });
|
|
9617
|
+
const existing = fs39.readdirSync(adrsDir).filter((f) => f.endsWith(".md")).length;
|
|
8563
9618
|
const adrId = String(existing + 1).padStart(4, "0");
|
|
8564
9619
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8565
9620
|
const filename = `${adrId}-${slug}.md`;
|
|
@@ -8580,13 +9635,13 @@ Task: "${taskDescription}"
|
|
|
8580
9635
|
|
|
8581
9636
|
(Beschreibe die Auswirkungen dieser Entscheidung.)
|
|
8582
9637
|
`;
|
|
8583
|
-
|
|
9638
|
+
fs39.writeFileSync(path40.join(adrsDir, filename), content, "utf8");
|
|
8584
9639
|
console.log(`\u2714 ADR erstellt: .forgehive/memory/adrs/${filename}`);
|
|
8585
9640
|
} else {
|
|
8586
9641
|
console.error("Verf\xFCgbar: fh docs [list|user|api|onboard|changelog|adr <titel>]");
|
|
8587
9642
|
}
|
|
8588
9643
|
} else if (command === "github") {
|
|
8589
|
-
const storiesDir =
|
|
9644
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8590
9645
|
if (subcommand === "setup") {
|
|
8591
9646
|
const readline = await import("node:readline/promises");
|
|
8592
9647
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -8654,6 +9709,16 @@ Task: "${taskDescription}"
|
|
|
8654
9709
|
}
|
|
8655
9710
|
console.log(`
|
|
8656
9711
|
${created} ${created === 1 ? "Story" : "Stories"} erstellt, ${skipped} \xFCbersprungen.`);
|
|
9712
|
+
try {
|
|
9713
|
+
const [syncOwner, syncRepo] = config.repo.split("/");
|
|
9714
|
+
const openPrNumbers = await fetchOpenPRs(syncOwner, syncRepo, creds.GITHUB_TOKEN);
|
|
9715
|
+
fs39.writeFileSync(
|
|
9716
|
+
path40.join(forgehiveDir, "github-pr-cache.json"),
|
|
9717
|
+
JSON.stringify({ open: openPrNumbers, ts: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
9718
|
+
"utf8"
|
|
9719
|
+
);
|
|
9720
|
+
} catch {
|
|
9721
|
+
}
|
|
8657
9722
|
} else if (subcommand === "pr") {
|
|
8658
9723
|
const prNumberRaw = rest[0];
|
|
8659
9724
|
if (!prNumberRaw || isNaN(parseInt(prNumberRaw, 10))) {
|
|
@@ -8686,9 +9751,9 @@ Task: "${taskDescription}"
|
|
|
8686
9751
|
process.exit(1);
|
|
8687
9752
|
}
|
|
8688
9753
|
const contextMd = formatPRContext(pr, files, config.repo);
|
|
8689
|
-
|
|
8690
|
-
const outputPath =
|
|
8691
|
-
|
|
9754
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
9755
|
+
const outputPath = path40.join(forgehiveDir, `github-pr-${prNumber}.md`);
|
|
9756
|
+
fs39.writeFileSync(outputPath, contextMd, "utf8");
|
|
8692
9757
|
console.log(` \u2713 PR-Kontext gespeichert: .forgehive/github-pr-${prNumber}.md`);
|
|
8693
9758
|
console.log("");
|
|
8694
9759
|
console.log(` PR context gespeichert. Starte Review: fh party run review`);
|
|
@@ -8696,9 +9761,45 @@ Task: "${taskDescription}"
|
|
|
8696
9761
|
console.log("Verf\xFCgbar: fh github setup | fh github sync | fh github pr <number>");
|
|
8697
9762
|
console.log("Hilfe: fh --help");
|
|
8698
9763
|
}
|
|
9764
|
+
} else if (command === "outcomes") {
|
|
9765
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
9766
|
+
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
9767
|
+
process.exit(1);
|
|
9768
|
+
}
|
|
9769
|
+
const { aggregateByAgent: aggregateByAgent2, aggregateByTaskType: aggregateByTaskType2, readOutcomes: readOutcomes2 } = await Promise.resolve().then(() => (init_outcomes(), outcomes_exports));
|
|
9770
|
+
const entries = readOutcomes2(forgehiveDir);
|
|
9771
|
+
if (entries.length === 0) {
|
|
9772
|
+
console.log("Keine Outcome-Daten vorhanden. Starte 'fh auto' oder 'fh party run' um Daten zu sammeln.");
|
|
9773
|
+
process.exit(0);
|
|
9774
|
+
}
|
|
9775
|
+
const byAgent = aggregateByAgent2(forgehiveDir);
|
|
9776
|
+
const byType = aggregateByTaskType2(forgehiveDir);
|
|
9777
|
+
console.log(`
|
|
9778
|
+
Outcome-\xDCbersicht (${entries.length} Runs)
|
|
9779
|
+
`);
|
|
9780
|
+
console.log("\u2500\u2500 Agenten \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
9781
|
+
for (const [agent, stats] of Object.entries(byAgent)) {
|
|
9782
|
+
const avgMin = (stats.avgDurationMs / 6e4).toFixed(1);
|
|
9783
|
+
const rating = stats.avgRating !== null ? ` \u2605 ${stats.avgRating.toFixed(1)}` : "";
|
|
9784
|
+
const types2 = Object.keys(stats.taskTypes).join(", ");
|
|
9785
|
+
console.log(` ${agent.padEnd(8)} \xD8 ${avgMin} min [${types2}]${rating}`);
|
|
9786
|
+
}
|
|
9787
|
+
console.log("\n\u2500\u2500 Task-Typen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
9788
|
+
for (const [type2, agg] of Object.entries(byType)) {
|
|
9789
|
+
console.log(` ${type2.padEnd(16)} ${agg.count}x`);
|
|
9790
|
+
}
|
|
9791
|
+
const last = entries.slice(-5).reverse();
|
|
9792
|
+
console.log("\n\u2500\u2500 Letzte 5 Runs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
9793
|
+
for (const e of last) {
|
|
9794
|
+
const mins = (e.durationMs / 6e4).toFixed(1);
|
|
9795
|
+
const rating = e.rating !== null ? ` \u2605 ${e.rating}` : "";
|
|
9796
|
+
const agents = e.agents.join("+");
|
|
9797
|
+
console.log(` ${e.runId} ${e.taskType} ${agents} ${mins} min${rating}`);
|
|
9798
|
+
}
|
|
9799
|
+
process.exit(0);
|
|
8699
9800
|
} else {
|
|
8700
9801
|
console.error("Unbekannter Befehl: " + command);
|
|
8701
|
-
console.error("Verf\xFCgbar: init | confirm | rollback | scan | status | ci | map | onboard | changelog | metrics | docs | story [create|list|show|sprint|done] | epic [create|list|show] | velocity [show|record] | sync [push|pull] | run <issue-url> | github [setup|sync|pr] | cost | memory | skills | party | wire | mcp | security");
|
|
9802
|
+
console.error("Verf\xFCgbar: init | confirm | rollback | scan | status | auto | ci | map | onboard | changelog | metrics | docs | story [create|list|show|sprint|done] | epic [create|list|show] | velocity [show|record] | sync [push|pull] | run <issue-url> | github [setup|sync|pr] | cost | memory | skills | party | outcomes | wire | mcp | security");
|
|
8702
9803
|
console.error("Hilfe: fh --help");
|
|
8703
9804
|
process.exit(1);
|
|
8704
9805
|
}
|