forgehive 0.9.0 → 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 +1249 -397
- 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,20 +5247,21 @@ 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;
|
|
4862
5257
|
}
|
|
4863
5258
|
function readYamlFrontmatterFiles(dir) {
|
|
4864
|
-
if (!
|
|
5259
|
+
if (!fs17.existsSync(dir)) return [];
|
|
4865
5260
|
const results = [];
|
|
4866
|
-
for (const filename of
|
|
5261
|
+
for (const filename of fs17.readdirSync(dir)) {
|
|
4867
5262
|
if (!filename.endsWith(".md")) continue;
|
|
4868
5263
|
try {
|
|
4869
|
-
const content =
|
|
5264
|
+
const content = fs17.readFileSync(path17.join(dir, filename), "utf8");
|
|
4870
5265
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
4871
5266
|
if (!match) continue;
|
|
4872
5267
|
const parsed = jsYaml.load(match[1]);
|
|
@@ -4877,13 +5272,13 @@ function readYamlFrontmatterFiles(dir) {
|
|
|
4877
5272
|
return results;
|
|
4878
5273
|
}
|
|
4879
5274
|
function formatSprintSection(forgehiveDir2) {
|
|
4880
|
-
const storiesDir =
|
|
4881
|
-
if (!
|
|
5275
|
+
const storiesDir = path17.join(forgehiveDir2, "memory", "stories");
|
|
5276
|
+
if (!fs17.existsSync(storiesDir)) return "";
|
|
4882
5277
|
const stories = readYamlFrontmatterFiles(storiesDir);
|
|
4883
5278
|
const open = stories.filter((s) => s.status !== "done");
|
|
4884
5279
|
const inSprint = stories.filter((s) => s.status === "in-sprint");
|
|
4885
5280
|
const next = inSprint[0] ?? open[0] ?? null;
|
|
4886
|
-
const epicsDir =
|
|
5281
|
+
const epicsDir = path17.join(forgehiveDir2, "memory", "epics");
|
|
4887
5282
|
const activeEpics = readYamlFrontmatterFiles(epicsDir).filter((e) => e.status === "active" || e.status === "in-progress");
|
|
4888
5283
|
const lines = ["SPRINT"];
|
|
4889
5284
|
lines.push(` \u25CF ${open.length} offen \u25CF ${inSprint.length} in-sprint`);
|
|
@@ -4908,10 +5303,10 @@ function formatCodeHealthSection(projectRoot2, forgehiveDir2) {
|
|
|
4908
5303
|
changed === 0 ? " \u2713 Arbeitsbaum sauber" : ` \u26A0 ${changed} ge\xE4nderte Datei(en) \u2014 nicht committed`
|
|
4909
5304
|
);
|
|
4910
5305
|
}
|
|
4911
|
-
const secPath =
|
|
4912
|
-
if (
|
|
5306
|
+
const secPath = path17.join(forgehiveDir2, "security-last-scan.txt");
|
|
5307
|
+
if (fs17.existsSync(secPath)) {
|
|
4913
5308
|
try {
|
|
4914
|
-
const ts =
|
|
5309
|
+
const ts = fs17.readFileSync(secPath, "utf8").trim();
|
|
4915
5310
|
const days = Math.floor((Date.now() - new Date(ts).getTime()) / 864e5);
|
|
4916
5311
|
lines.push(
|
|
4917
5312
|
days > 7 ? ` \u26A0 Security-Scan: ${days} Tage alt \u2014 fh security scan` : ` \u2713 Security-Scan: ${days} Tage alt`
|
|
@@ -4919,22 +5314,42 @@ function formatCodeHealthSection(projectRoot2, forgehiveDir2) {
|
|
|
4919
5314
|
} catch {
|
|
4920
5315
|
}
|
|
4921
5316
|
}
|
|
4922
|
-
const prCachePath =
|
|
4923
|
-
if (
|
|
5317
|
+
const prCachePath = path17.join(forgehiveDir2, "github-pr-cache.json");
|
|
5318
|
+
if (fs17.existsSync(prCachePath)) {
|
|
4924
5319
|
try {
|
|
4925
|
-
const cache = JSON.parse(
|
|
5320
|
+
const cache = JSON.parse(fs17.readFileSync(prCachePath, "utf8"));
|
|
4926
5321
|
if (cache.open.length > 0) {
|
|
4927
5322
|
lines.push(` \u25CF ${cache.open.length} offene PR(s): #${cache.open.join(", #")}`);
|
|
4928
5323
|
}
|
|
4929
5324
|
} catch {
|
|
4930
5325
|
}
|
|
4931
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
|
+
}
|
|
4932
5347
|
return lines.join("\n");
|
|
4933
5348
|
}
|
|
4934
5349
|
function formatWatchSection(forgehiveDir2) {
|
|
4935
|
-
const logPath =
|
|
4936
|
-
if (!
|
|
4937
|
-
const events =
|
|
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) => {
|
|
4938
5353
|
try {
|
|
4939
5354
|
return JSON.parse(line);
|
|
4940
5355
|
} catch {
|
|
@@ -4958,19 +5373,20 @@ function formatDashboard(projectRoot2, forgehiveDir2) {
|
|
|
4958
5373
|
const sections = [
|
|
4959
5374
|
formatSprintSection(forgehiveDir2),
|
|
4960
5375
|
formatCodeHealthSection(projectRoot2, forgehiveDir2),
|
|
4961
|
-
formatWatchSection(forgehiveDir2)
|
|
5376
|
+
formatWatchSection(forgehiveDir2),
|
|
5377
|
+
formatNextCompact(projectRoot2, forgehiveDir2)
|
|
4962
5378
|
].filter(Boolean);
|
|
4963
5379
|
if (sections.length === 0) return "";
|
|
4964
5380
|
return "\n" + sections.join("\n\n");
|
|
4965
5381
|
}
|
|
4966
5382
|
function checkDrift(projectRoot2, forgehiveDir2) {
|
|
4967
|
-
const scanPath =
|
|
4968
|
-
if (!
|
|
5383
|
+
const scanPath = path17.join(forgehiveDir2, "scan-result.yaml");
|
|
5384
|
+
if (!fs17.existsSync(scanPath)) {
|
|
4969
5385
|
return { daysSinceScan: null, recentCommits: 0, isDrifted: false };
|
|
4970
5386
|
}
|
|
4971
5387
|
let scannedAt = null;
|
|
4972
5388
|
try {
|
|
4973
|
-
const raw = jsYaml.load(
|
|
5389
|
+
const raw = jsYaml.load(fs17.readFileSync(scanPath, "utf8"));
|
|
4974
5390
|
scannedAt = raw?.scanned_at ?? null;
|
|
4975
5391
|
} catch {
|
|
4976
5392
|
}
|
|
@@ -4993,14 +5409,14 @@ function checkDrift(projectRoot2, forgehiveDir2) {
|
|
|
4993
5409
|
}
|
|
4994
5410
|
function projectStatus(projectRoot2, forgehiveDir2) {
|
|
4995
5411
|
const lines = ["ForgeHive Status", ""];
|
|
4996
|
-
if (!
|
|
5412
|
+
if (!fs17.existsSync(forgehiveDir2)) {
|
|
4997
5413
|
lines.push("\u2717 Not initialized \u2014 run: fh init");
|
|
4998
5414
|
return lines.join("\n");
|
|
4999
5415
|
}
|
|
5000
|
-
const capsPath =
|
|
5001
|
-
if (
|
|
5416
|
+
const capsPath = path17.join(forgehiveDir2, "capabilities.yaml");
|
|
5417
|
+
if (fs17.existsSync(capsPath)) {
|
|
5002
5418
|
try {
|
|
5003
|
-
const raw = jsYaml.load(
|
|
5419
|
+
const raw = jsYaml.load(fs17.readFileSync(capsPath, "utf8"));
|
|
5004
5420
|
const caps = raw && typeof raw === "object" ? raw : {};
|
|
5005
5421
|
const confirmed = caps.capabilities?.confirmed?.length ?? 0;
|
|
5006
5422
|
const inferred = caps.capabilities?.inferred?.length ?? 0;
|
|
@@ -5011,10 +5427,10 @@ function projectStatus(projectRoot2, forgehiveDir2) {
|
|
|
5011
5427
|
lines.push("\u26A0 Capabilities: capabilities.yaml unlesbar");
|
|
5012
5428
|
}
|
|
5013
5429
|
}
|
|
5014
|
-
const claudeMdPath =
|
|
5015
|
-
if (
|
|
5430
|
+
const claudeMdPath = path17.join(projectRoot2, "CLAUDE.md");
|
|
5431
|
+
if (fs17.existsSync(claudeMdPath)) {
|
|
5016
5432
|
try {
|
|
5017
|
-
const content =
|
|
5433
|
+
const content = fs17.readFileSync(claudeMdPath, "utf8");
|
|
5018
5434
|
const hasBlock = content.includes("<!-- forgehive:start -->");
|
|
5019
5435
|
lines.push(hasBlock ? "\u2713 CLAUDE.md block present" : "\u26A0 CLAUDE.md block missing \u2014 run: fh init");
|
|
5020
5436
|
} catch {
|
|
@@ -5023,15 +5439,15 @@ function projectStatus(projectRoot2, forgehiveDir2) {
|
|
|
5023
5439
|
} else {
|
|
5024
5440
|
lines.push("\u26A0 CLAUDE.md not found");
|
|
5025
5441
|
}
|
|
5026
|
-
const skillsDir =
|
|
5027
|
-
const generated = countMdFiles(
|
|
5028
|
-
const expert = countMdFiles(
|
|
5029
|
-
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"));
|
|
5030
5446
|
lines.push(`\u2713 Skills: ${generated} generated, ${expert} expert, ${workflows} workflows`);
|
|
5031
|
-
const mcpPath =
|
|
5032
|
-
if (
|
|
5447
|
+
const mcpPath = path17.join(projectRoot2, ".mcp.json");
|
|
5448
|
+
if (fs17.existsSync(mcpPath)) {
|
|
5033
5449
|
try {
|
|
5034
|
-
const mcp = JSON.parse(
|
|
5450
|
+
const mcp = JSON.parse(fs17.readFileSync(mcpPath, "utf8"));
|
|
5035
5451
|
const services = Object.keys(mcp.mcpServers ?? {});
|
|
5036
5452
|
lines.push(
|
|
5037
5453
|
services.length > 0 ? `\u2713 MCP: ${services.join(", ")}` : " MCP: none configured"
|
|
@@ -5042,17 +5458,17 @@ function projectStatus(projectRoot2, forgehiveDir2) {
|
|
|
5042
5458
|
} else {
|
|
5043
5459
|
lines.push(" MCP: none configured (fh wire <service>)");
|
|
5044
5460
|
}
|
|
5045
|
-
const memoryDir =
|
|
5046
|
-
if (
|
|
5047
|
-
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"));
|
|
5048
5464
|
lines.push(`\u2713 Memory: ${files.length} file(s)`);
|
|
5049
5465
|
} else {
|
|
5050
5466
|
lines.push(" Memory: not initialized");
|
|
5051
5467
|
}
|
|
5052
|
-
const scanPath =
|
|
5053
|
-
if (
|
|
5468
|
+
const scanPath = path17.join(forgehiveDir2, "scan-result.yaml");
|
|
5469
|
+
if (fs17.existsSync(scanPath)) {
|
|
5054
5470
|
try {
|
|
5055
|
-
const raw = jsYaml.load(
|
|
5471
|
+
const raw = jsYaml.load(fs17.readFileSync(scanPath, "utf8"));
|
|
5056
5472
|
const scan2 = raw && typeof raw === "object" ? raw : {};
|
|
5057
5473
|
lines.push(`\u2713 Last scan: ${scan2.scanned_at ?? "unknown"}`);
|
|
5058
5474
|
} catch {
|
|
@@ -5066,24 +5482,24 @@ function projectStatus(projectRoot2, forgehiveDir2) {
|
|
|
5066
5482
|
} else if (drift.daysSinceScan !== null) {
|
|
5067
5483
|
lines.push(`\u2713 Kontext aktuell (${drift.daysSinceScan} Tage, ${drift.recentCommits} Commits seit Scan)`);
|
|
5068
5484
|
}
|
|
5069
|
-
const agentMemDir =
|
|
5070
|
-
if (
|
|
5071
|
-
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"));
|
|
5072
5488
|
lines.push(`\u2713 Agent memory: ${agents.length} agent(s)`);
|
|
5073
5489
|
}
|
|
5074
5490
|
return lines.join("\n");
|
|
5075
5491
|
}
|
|
5076
5492
|
|
|
5077
5493
|
// src/watch.ts
|
|
5078
|
-
import
|
|
5079
|
-
import
|
|
5494
|
+
import fs18 from "node:fs";
|
|
5495
|
+
import path18 from "node:path";
|
|
5080
5496
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
5081
5497
|
function checkProjectHash(projectRoot2, forgehiveDir2, claudeMdBlock) {
|
|
5082
|
-
const hashPath =
|
|
5083
|
-
const savedHash =
|
|
5498
|
+
const hashPath = path18.join(forgehiveDir2, ".scan-hash");
|
|
5499
|
+
const savedHash = fs18.existsSync(hashPath) ? fs18.readFileSync(hashPath, "utf8").trim() : null;
|
|
5084
5500
|
const currentHash = computeHash(projectRoot2);
|
|
5085
5501
|
if (currentHash === savedHash) return false;
|
|
5086
|
-
|
|
5502
|
+
fs18.writeFileSync(hashPath, currentHash, "utf8");
|
|
5087
5503
|
const scanResult = scan(projectRoot2);
|
|
5088
5504
|
const capMap = mapSignalsToCapabilities(scanResult);
|
|
5089
5505
|
writeForgehiveDir(projectRoot2, scanResult, capMap, claudeMdBlock);
|
|
@@ -5100,7 +5516,7 @@ function watchProject(projectRoot2, forgehiveDir2, claudeMdBlock, onUpdate = def
|
|
|
5100
5516
|
"go.mod",
|
|
5101
5517
|
"pyproject.toml",
|
|
5102
5518
|
"composer.json"
|
|
5103
|
-
].map((f) =>
|
|
5519
|
+
].map((f) => path18.join(projectRoot2, f)).filter((f) => fs18.existsSync(f));
|
|
5104
5520
|
let debounce = null;
|
|
5105
5521
|
const watchers = [];
|
|
5106
5522
|
function schedule() {
|
|
@@ -5112,12 +5528,12 @@ function watchProject(projectRoot2, forgehiveDir2, claudeMdBlock, onUpdate = def
|
|
|
5112
5528
|
}
|
|
5113
5529
|
for (const f of candidates) {
|
|
5114
5530
|
try {
|
|
5115
|
-
watchers.push(
|
|
5531
|
+
watchers.push(fs18.watch(f, schedule));
|
|
5116
5532
|
} catch {
|
|
5117
5533
|
}
|
|
5118
5534
|
}
|
|
5119
5535
|
try {
|
|
5120
|
-
watchers.push(
|
|
5536
|
+
watchers.push(fs18.watch(projectRoot2, schedule));
|
|
5121
5537
|
} catch {
|
|
5122
5538
|
}
|
|
5123
5539
|
return () => {
|
|
@@ -5136,14 +5552,14 @@ function defaultOnUpdate(changed) {
|
|
|
5136
5552
|
}
|
|
5137
5553
|
}
|
|
5138
5554
|
function appendWatchEvent(forgehiveDir2, event) {
|
|
5139
|
-
const logPath =
|
|
5140
|
-
|
|
5555
|
+
const logPath = path18.join(forgehiveDir2, "watch-events.jsonl");
|
|
5556
|
+
fs18.appendFileSync(logPath, JSON.stringify(event) + "\n", "utf8");
|
|
5141
5557
|
try {
|
|
5142
|
-
const stat =
|
|
5558
|
+
const stat = fs18.statSync(logPath);
|
|
5143
5559
|
if (stat.size > 25e3) {
|
|
5144
|
-
const lines =
|
|
5560
|
+
const lines = fs18.readFileSync(logPath, "utf8").trim().split("\n").filter(Boolean);
|
|
5145
5561
|
if (lines.length >= 500) {
|
|
5146
|
-
|
|
5562
|
+
fs18.writeFileSync(logPath, lines.slice(-500).join("\n") + "\n", "utf8");
|
|
5147
5563
|
}
|
|
5148
5564
|
}
|
|
5149
5565
|
} catch {
|
|
@@ -5166,9 +5582,9 @@ function detectErrorType(output) {
|
|
|
5166
5582
|
return null;
|
|
5167
5583
|
}
|
|
5168
5584
|
function smartWatch(projectRoot2, forgehiveDir2, testCmd, onSuggest) {
|
|
5169
|
-
const srcDir =
|
|
5170
|
-
const testDir =
|
|
5171
|
-
const watchDirs = [srcDir, testDir].filter((d) =>
|
|
5585
|
+
const srcDir = path18.join(projectRoot2, "src");
|
|
5586
|
+
const testDir = path18.join(projectRoot2, "test");
|
|
5587
|
+
const watchDirs = [srcDir, testDir].filter((d) => fs18.existsSync(d));
|
|
5172
5588
|
let debounce = null;
|
|
5173
5589
|
const watchers = [];
|
|
5174
5590
|
function runAndAnalyze(changedFile) {
|
|
@@ -5181,7 +5597,7 @@ function smartWatch(projectRoot2, forgehiveDir2, testCmd, onSuggest) {
|
|
|
5181
5597
|
const output = (result.stdout ?? "") + (result.stderr ?? "");
|
|
5182
5598
|
const detection = detectErrorType(output);
|
|
5183
5599
|
if (!detection) return;
|
|
5184
|
-
const relFile =
|
|
5600
|
+
const relFile = path18.relative(projectRoot2, changedFile);
|
|
5185
5601
|
console.log(`
|
|
5186
5602
|
\u26A0 ${relFile} \u2014 ${detection.reason}`);
|
|
5187
5603
|
console.log(` \u2192 ${detection.agent} ist der richtige Agent.`);
|
|
@@ -5201,9 +5617,9 @@ function smartWatch(projectRoot2, forgehiveDir2, testCmd, onSuggest) {
|
|
|
5201
5617
|
}
|
|
5202
5618
|
for (const dir of watchDirs) {
|
|
5203
5619
|
try {
|
|
5204
|
-
const watcher =
|
|
5620
|
+
const watcher = fs18.watch(dir, { recursive: true }, (_evt, filename) => {
|
|
5205
5621
|
if (filename && (filename.endsWith(".ts") || filename.endsWith(".js"))) {
|
|
5206
|
-
schedule(
|
|
5622
|
+
schedule(path18.join(dir, filename));
|
|
5207
5623
|
}
|
|
5208
5624
|
});
|
|
5209
5625
|
watchers.push(watcher);
|
|
@@ -5222,8 +5638,8 @@ function smartWatch(projectRoot2, forgehiveDir2, testCmd, onSuggest) {
|
|
|
5222
5638
|
}
|
|
5223
5639
|
|
|
5224
5640
|
// src/cost.ts
|
|
5225
|
-
import
|
|
5226
|
-
import
|
|
5641
|
+
import fs19 from "node:fs";
|
|
5642
|
+
import path19 from "node:path";
|
|
5227
5643
|
import os2 from "node:os";
|
|
5228
5644
|
var PRICING = {
|
|
5229
5645
|
input: 3,
|
|
@@ -5234,31 +5650,31 @@ var PRICING = {
|
|
|
5234
5650
|
function calcCost(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
|
|
5235
5651
|
return inputTokens / 1e6 * PRICING.input + outputTokens / 1e6 * PRICING.output + cacheCreationTokens / 1e6 * PRICING.cacheCreation + cacheReadTokens / 1e6 * PRICING.cacheRead;
|
|
5236
5652
|
}
|
|
5237
|
-
function parseCostSessions(_projectRoot, claudeHome =
|
|
5238
|
-
const projectsDir =
|
|
5239
|
-
if (!
|
|
5240
|
-
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);
|
|
5241
5657
|
const sessions = [];
|
|
5242
5658
|
for (const dir of matchDirs) {
|
|
5243
|
-
const dirPath =
|
|
5659
|
+
const dirPath = path19.join(projectsDir, dir);
|
|
5244
5660
|
let stat;
|
|
5245
5661
|
try {
|
|
5246
|
-
stat =
|
|
5662
|
+
stat = fs19.statSync(dirPath);
|
|
5247
5663
|
} catch {
|
|
5248
5664
|
continue;
|
|
5249
5665
|
}
|
|
5250
5666
|
if (!stat.isDirectory()) continue;
|
|
5251
5667
|
let files;
|
|
5252
5668
|
try {
|
|
5253
|
-
files =
|
|
5669
|
+
files = fs19.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
5254
5670
|
} catch {
|
|
5255
5671
|
continue;
|
|
5256
5672
|
}
|
|
5257
5673
|
for (const jsonlFile of files) {
|
|
5258
|
-
const filePath =
|
|
5674
|
+
const filePath = path19.join(dirPath, jsonlFile);
|
|
5259
5675
|
let inputTokens = 0, outputTokens = 0, cacheCreationTokens = 0, cacheReadTokens = 0;
|
|
5260
5676
|
try {
|
|
5261
|
-
const lines =
|
|
5677
|
+
const lines = fs19.readFileSync(filePath, "utf8").split("\n").filter(Boolean);
|
|
5262
5678
|
for (const line of lines) {
|
|
5263
5679
|
try {
|
|
5264
5680
|
const entry = JSON.parse(line);
|
|
@@ -5271,7 +5687,7 @@ function parseCostSessions(_projectRoot, claudeHome = path17.join(os2.homedir(),
|
|
|
5271
5687
|
} catch {
|
|
5272
5688
|
}
|
|
5273
5689
|
}
|
|
5274
|
-
const mtime =
|
|
5690
|
+
const mtime = fs19.statSync(filePath).mtime;
|
|
5275
5691
|
sessions.push({
|
|
5276
5692
|
sessionFile: jsonlFile,
|
|
5277
5693
|
date: mtime.toISOString().slice(0, 10),
|
|
@@ -5319,28 +5735,28 @@ function formatCostReport(sessions, range) {
|
|
|
5319
5735
|
}
|
|
5320
5736
|
|
|
5321
5737
|
// src/mcp-auth.ts
|
|
5322
|
-
import
|
|
5323
|
-
import
|
|
5738
|
+
import fs20 from "node:fs";
|
|
5739
|
+
import path20 from "node:path";
|
|
5324
5740
|
import os3 from "node:os";
|
|
5325
5741
|
function getCredsPath() {
|
|
5326
|
-
const dir =
|
|
5327
|
-
|
|
5328
|
-
return
|
|
5742
|
+
const dir = path20.join(os3.homedir(), ".forgehive");
|
|
5743
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
5744
|
+
return path20.join(dir, "credentials.json");
|
|
5329
5745
|
}
|
|
5330
5746
|
function loadStore() {
|
|
5331
5747
|
const p = getCredsPath();
|
|
5332
|
-
if (!
|
|
5748
|
+
if (!fs20.existsSync(p)) return {};
|
|
5333
5749
|
try {
|
|
5334
|
-
return JSON.parse(
|
|
5750
|
+
return JSON.parse(fs20.readFileSync(p, "utf8"));
|
|
5335
5751
|
} catch {
|
|
5336
5752
|
return {};
|
|
5337
5753
|
}
|
|
5338
5754
|
}
|
|
5339
5755
|
function saveStore(store) {
|
|
5340
5756
|
const p = getCredsPath();
|
|
5341
|
-
|
|
5757
|
+
fs20.writeFileSync(p, JSON.stringify(store, null, 2), "utf8");
|
|
5342
5758
|
try {
|
|
5343
|
-
|
|
5759
|
+
fs20.chmodSync(p, 384);
|
|
5344
5760
|
} catch {
|
|
5345
5761
|
}
|
|
5346
5762
|
}
|
|
@@ -5387,8 +5803,8 @@ function checkMcpTrust(packageName) {
|
|
|
5387
5803
|
}
|
|
5388
5804
|
|
|
5389
5805
|
// src/mcp-registry.ts
|
|
5390
|
-
import
|
|
5391
|
-
import
|
|
5806
|
+
import fs21 from "node:fs";
|
|
5807
|
+
import path21 from "node:path";
|
|
5392
5808
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5393
5809
|
function parseRegistryResponse(raw) {
|
|
5394
5810
|
if (!raw || typeof raw !== "object") return [];
|
|
@@ -5425,11 +5841,11 @@ function searchRegistry(query) {
|
|
|
5425
5841
|
}
|
|
5426
5842
|
function addMcpFromRegistry(projectRoot2, forgehiveDir2, packageName, envKeys) {
|
|
5427
5843
|
const serverId = packageName.replace(/^@[^/]+\//, "") || packageName;
|
|
5428
|
-
const mcpPath =
|
|
5844
|
+
const mcpPath = path21.join(projectRoot2, ".mcp.json");
|
|
5429
5845
|
let mcpConfig = { mcpServers: {} };
|
|
5430
|
-
if (
|
|
5846
|
+
if (fs21.existsSync(mcpPath)) {
|
|
5431
5847
|
try {
|
|
5432
|
-
mcpConfig = JSON.parse(
|
|
5848
|
+
mcpConfig = JSON.parse(fs21.readFileSync(mcpPath, "utf8"));
|
|
5433
5849
|
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
5434
5850
|
} catch {
|
|
5435
5851
|
mcpConfig = { mcpServers: {} };
|
|
@@ -5444,12 +5860,12 @@ function addMcpFromRegistry(projectRoot2, forgehiveDir2, packageName, envKeys) {
|
|
|
5444
5860
|
args: ["-y", packageName],
|
|
5445
5861
|
...envKeys.length > 0 ? { env: envObj } : {}
|
|
5446
5862
|
};
|
|
5447
|
-
|
|
5448
|
-
const skillDir =
|
|
5449
|
-
|
|
5450
|
-
const skillPath =
|
|
5451
|
-
if (!
|
|
5452
|
-
|
|
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, [
|
|
5453
5869
|
`# ${serverId} Workflow Skill`,
|
|
5454
5870
|
"",
|
|
5455
5871
|
`## Setup`,
|
|
@@ -5464,8 +5880,8 @@ function addMcpFromRegistry(projectRoot2, forgehiveDir2, packageName, envKeys) {
|
|
|
5464
5880
|
}
|
|
5465
5881
|
|
|
5466
5882
|
// src/security-scan.ts
|
|
5467
|
-
import
|
|
5468
|
-
import
|
|
5883
|
+
import fs22 from "node:fs";
|
|
5884
|
+
import path22 from "node:path";
|
|
5469
5885
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5470
5886
|
var SECRET_PATTERNS = [
|
|
5471
5887
|
{ name: "Anthropic Key", pattern: /sk-ant-[a-zA-Z0-9-]{20,}/g },
|
|
@@ -5484,16 +5900,16 @@ function collectScanFiles(dir, exts = SCAN_EXTS) {
|
|
|
5484
5900
|
function walk(current) {
|
|
5485
5901
|
let entries;
|
|
5486
5902
|
try {
|
|
5487
|
-
entries =
|
|
5903
|
+
entries = fs22.readdirSync(current, { withFileTypes: true });
|
|
5488
5904
|
} catch {
|
|
5489
5905
|
return;
|
|
5490
5906
|
}
|
|
5491
5907
|
for (const entry of entries) {
|
|
5492
5908
|
if (IGNORE_DIRS.includes(entry.name)) continue;
|
|
5493
|
-
const full =
|
|
5909
|
+
const full = path22.join(current, entry.name);
|
|
5494
5910
|
if (entry.isDirectory()) {
|
|
5495
5911
|
walk(full);
|
|
5496
|
-
} else if (entry.isFile() && exts.includes(
|
|
5912
|
+
} else if (entry.isFile() && exts.includes(path22.extname(entry.name))) {
|
|
5497
5913
|
results.push(full);
|
|
5498
5914
|
}
|
|
5499
5915
|
}
|
|
@@ -5507,7 +5923,7 @@ function scanSecrets(projectRoot2) {
|
|
|
5507
5923
|
for (const file of files) {
|
|
5508
5924
|
let content;
|
|
5509
5925
|
try {
|
|
5510
|
-
content =
|
|
5926
|
+
content = fs22.readFileSync(file, "utf8");
|
|
5511
5927
|
} catch {
|
|
5512
5928
|
continue;
|
|
5513
5929
|
}
|
|
@@ -5574,7 +5990,7 @@ function scanSast(projectRoot2) {
|
|
|
5574
5990
|
for (const file of files) {
|
|
5575
5991
|
let content;
|
|
5576
5992
|
try {
|
|
5577
|
-
content =
|
|
5993
|
+
content = fs22.readFileSync(file, "utf8");
|
|
5578
5994
|
} catch {
|
|
5579
5995
|
continue;
|
|
5580
5996
|
}
|
|
@@ -5599,8 +6015,8 @@ function scanSast(projectRoot2) {
|
|
|
5599
6015
|
return findings;
|
|
5600
6016
|
}
|
|
5601
6017
|
function scanDeps(projectRoot2) {
|
|
5602
|
-
const pkgJsonPath =
|
|
5603
|
-
if (!
|
|
6018
|
+
const pkgJsonPath = path22.join(projectRoot2, "package.json");
|
|
6019
|
+
if (!fs22.existsSync(pkgJsonPath)) return [];
|
|
5604
6020
|
const result = spawnSync7("npm", ["audit", "--json"], {
|
|
5605
6021
|
cwd: projectRoot2,
|
|
5606
6022
|
encoding: "utf8",
|
|
@@ -5629,19 +6045,19 @@ function scanDeps(projectRoot2) {
|
|
|
5629
6045
|
}
|
|
5630
6046
|
|
|
5631
6047
|
// src/audit.ts
|
|
5632
|
-
import
|
|
5633
|
-
import
|
|
6048
|
+
import fs23 from "node:fs";
|
|
6049
|
+
import path23 from "node:path";
|
|
5634
6050
|
function getAuditPath(forgehiveDir2) {
|
|
5635
|
-
return
|
|
6051
|
+
return path23.join(forgehiveDir2, "audit.log");
|
|
5636
6052
|
}
|
|
5637
6053
|
function logAuditEvent(forgehiveDir2, event) {
|
|
5638
6054
|
const line = JSON.stringify(event) + "\n";
|
|
5639
|
-
|
|
6055
|
+
fs23.appendFileSync(getAuditPath(forgehiveDir2), line, "utf8");
|
|
5640
6056
|
}
|
|
5641
6057
|
function getAuditLog(forgehiveDir2, limit = 50) {
|
|
5642
6058
|
const p = getAuditPath(forgehiveDir2);
|
|
5643
|
-
if (!
|
|
5644
|
-
const lines =
|
|
6059
|
+
if (!fs23.existsSync(p)) return [];
|
|
6060
|
+
const lines = fs23.readFileSync(p, "utf8").trim().split("\n").filter(Boolean);
|
|
5645
6061
|
const recent = lines.slice(-limit);
|
|
5646
6062
|
return recent.map((l) => {
|
|
5647
6063
|
try {
|
|
@@ -5666,19 +6082,19 @@ function formatAuditReport(events) {
|
|
|
5666
6082
|
|
|
5667
6083
|
// src/spend-limits.ts
|
|
5668
6084
|
init_js_yaml();
|
|
5669
|
-
import
|
|
5670
|
-
import
|
|
6085
|
+
import fs24 from "node:fs";
|
|
6086
|
+
import path24 from "node:path";
|
|
5671
6087
|
function getConfigPath(forgehiveDir2) {
|
|
5672
|
-
return
|
|
6088
|
+
return path24.join(forgehiveDir2, "cost-config.yaml");
|
|
5673
6089
|
}
|
|
5674
6090
|
function setSpendLimit(forgehiveDir2, config) {
|
|
5675
|
-
|
|
6091
|
+
fs24.writeFileSync(getConfigPath(forgehiveDir2), jsYaml.dump(config), "utf8");
|
|
5676
6092
|
}
|
|
5677
6093
|
function getSpendConfig(forgehiveDir2) {
|
|
5678
6094
|
const p = getConfigPath(forgehiveDir2);
|
|
5679
|
-
if (!
|
|
6095
|
+
if (!fs24.existsSync(p)) return {};
|
|
5680
6096
|
try {
|
|
5681
|
-
return jsYaml.load(
|
|
6097
|
+
return jsYaml.load(fs24.readFileSync(p, "utf8"));
|
|
5682
6098
|
} catch {
|
|
5683
6099
|
return {};
|
|
5684
6100
|
}
|
|
@@ -5773,7 +6189,7 @@ function formatSecurityReport(report) {
|
|
|
5773
6189
|
}
|
|
5774
6190
|
|
|
5775
6191
|
// src/ci.ts
|
|
5776
|
-
import
|
|
6192
|
+
import path25 from "node:path";
|
|
5777
6193
|
function generateCiReport(projectRoot2, forgehiveDir2, failOn = "high") {
|
|
5778
6194
|
const secrets = scanSecrets(projectRoot2);
|
|
5779
6195
|
const sast = scanSast(projectRoot2);
|
|
@@ -5790,7 +6206,7 @@ function generateCiReport(projectRoot2, forgehiveDir2, failOn = "high") {
|
|
|
5790
6206
|
}
|
|
5791
6207
|
return {
|
|
5792
6208
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5793
|
-
project:
|
|
6209
|
+
project: path25.basename(projectRoot2),
|
|
5794
6210
|
status: failedOn ? "fail" : "pass",
|
|
5795
6211
|
secrets,
|
|
5796
6212
|
sast,
|
|
@@ -5864,8 +6280,8 @@ jobs:
|
|
|
5864
6280
|
}
|
|
5865
6281
|
|
|
5866
6282
|
// src/map.ts
|
|
5867
|
-
import
|
|
5868
|
-
import
|
|
6283
|
+
import fs25 from "node:fs";
|
|
6284
|
+
import path26 from "node:path";
|
|
5869
6285
|
var MAP_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"];
|
|
5870
6286
|
var IGNORE_DIRS2 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
|
|
5871
6287
|
var IMPORT_PATTERNS = [
|
|
@@ -5887,11 +6303,11 @@ function extractImports(content) {
|
|
|
5887
6303
|
function walkFiles(dir) {
|
|
5888
6304
|
const results = [];
|
|
5889
6305
|
function walk(current) {
|
|
5890
|
-
for (const entry of
|
|
6306
|
+
for (const entry of fs25.readdirSync(current, { withFileTypes: true })) {
|
|
5891
6307
|
if (IGNORE_DIRS2.includes(entry.name)) continue;
|
|
5892
|
-
const full =
|
|
6308
|
+
const full = path26.join(current, entry.name);
|
|
5893
6309
|
if (entry.isDirectory()) walk(full);
|
|
5894
|
-
else if (entry.isFile() && MAP_EXTS.includes(
|
|
6310
|
+
else if (entry.isFile() && MAP_EXTS.includes(path26.extname(entry.name)))
|
|
5895
6311
|
results.push(full);
|
|
5896
6312
|
}
|
|
5897
6313
|
}
|
|
@@ -5903,7 +6319,7 @@ function generateMap(projectRoot2) {
|
|
|
5903
6319
|
const files = filePaths.map((filePath) => {
|
|
5904
6320
|
let content = "";
|
|
5905
6321
|
try {
|
|
5906
|
-
content =
|
|
6322
|
+
content = fs25.readFileSync(filePath, "utf8");
|
|
5907
6323
|
} catch {
|
|
5908
6324
|
}
|
|
5909
6325
|
const rawLines = content.split("\n");
|
|
@@ -5911,7 +6327,7 @@ function generateMap(projectRoot2) {
|
|
|
5911
6327
|
const imports = extractImports(content);
|
|
5912
6328
|
return {
|
|
5913
6329
|
path: filePath,
|
|
5914
|
-
relativePath:
|
|
6330
|
+
relativePath: path26.relative(projectRoot2, filePath),
|
|
5915
6331
|
lines,
|
|
5916
6332
|
imports
|
|
5917
6333
|
};
|
|
@@ -6000,8 +6416,8 @@ function formatMap(map2, semantic) {
|
|
|
6000
6416
|
}
|
|
6001
6417
|
|
|
6002
6418
|
// src/semantic-map.ts
|
|
6003
|
-
import
|
|
6004
|
-
import
|
|
6419
|
+
import fs26 from "node:fs";
|
|
6420
|
+
import path27 from "node:path";
|
|
6005
6421
|
var IGNORE_DIRS3 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
|
|
6006
6422
|
var MAP_EXTS2 = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
|
|
6007
6423
|
var IMPORT_PATTERNS2 = {
|
|
@@ -6052,15 +6468,15 @@ function walkFiles2(dir) {
|
|
|
6052
6468
|
function walk(current) {
|
|
6053
6469
|
let entries;
|
|
6054
6470
|
try {
|
|
6055
|
-
entries =
|
|
6471
|
+
entries = fs26.readdirSync(current, { withFileTypes: true });
|
|
6056
6472
|
} catch {
|
|
6057
6473
|
return;
|
|
6058
6474
|
}
|
|
6059
6475
|
for (const entry of entries) {
|
|
6060
6476
|
if (IGNORE_DIRS3.includes(entry.name)) continue;
|
|
6061
|
-
const full =
|
|
6477
|
+
const full = path27.join(current, entry.name);
|
|
6062
6478
|
if (entry.isDirectory()) walk(full);
|
|
6063
|
-
else if (entry.isFile() && MAP_EXTS2.includes(
|
|
6479
|
+
else if (entry.isFile() && MAP_EXTS2.includes(path27.extname(entry.name))) {
|
|
6064
6480
|
results.push(full);
|
|
6065
6481
|
}
|
|
6066
6482
|
}
|
|
@@ -6106,15 +6522,15 @@ function _extractExports(content, lang) {
|
|
|
6106
6522
|
}
|
|
6107
6523
|
function _resolveImport(fromFile, importPath, allFiles) {
|
|
6108
6524
|
if (!importPath.startsWith(".")) return null;
|
|
6109
|
-
const fromDir =
|
|
6110
|
-
const base =
|
|
6525
|
+
const fromDir = path27.dirname(fromFile);
|
|
6526
|
+
const base = path27.resolve(fromDir, importPath);
|
|
6111
6527
|
if (allFiles.includes(base)) return base;
|
|
6112
6528
|
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".py", ".go"]) {
|
|
6113
6529
|
const candidate = base + ext;
|
|
6114
6530
|
if (allFiles.includes(candidate)) return candidate;
|
|
6115
6531
|
}
|
|
6116
6532
|
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
6117
|
-
const candidate =
|
|
6533
|
+
const candidate = path27.join(base, "index" + ext);
|
|
6118
6534
|
if (allFiles.includes(candidate)) return candidate;
|
|
6119
6535
|
}
|
|
6120
6536
|
return null;
|
|
@@ -6162,10 +6578,10 @@ function buildSemanticMap(projectRoot2) {
|
|
|
6162
6578
|
for (const filePath of allFiles) {
|
|
6163
6579
|
let content = "";
|
|
6164
6580
|
try {
|
|
6165
|
-
content =
|
|
6581
|
+
content = fs26.readFileSync(filePath, "utf8");
|
|
6166
6582
|
} catch {
|
|
6167
6583
|
}
|
|
6168
|
-
const ext =
|
|
6584
|
+
const ext = path27.extname(filePath);
|
|
6169
6585
|
const lang = langOf(ext);
|
|
6170
6586
|
const rawImps = _extractImports(content, lang);
|
|
6171
6587
|
const resolvedImps = [];
|
|
@@ -6183,8 +6599,8 @@ function buildSemanticMap(projectRoot2) {
|
|
|
6183
6599
|
|
|
6184
6600
|
// src/onboard.ts
|
|
6185
6601
|
init_js_yaml();
|
|
6186
|
-
import
|
|
6187
|
-
import
|
|
6602
|
+
import fs27 from "node:fs";
|
|
6603
|
+
import path28 from "node:path";
|
|
6188
6604
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
6189
6605
|
function getRecentCommits(projectRoot2, n = 20) {
|
|
6190
6606
|
const result = spawnSync8("git", ["log", `--oneline`, `-${n}`], {
|
|
@@ -6195,30 +6611,30 @@ function getRecentCommits(projectRoot2, n = 20) {
|
|
|
6195
6611
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
6196
6612
|
}
|
|
6197
6613
|
function readCapabilities(forgehiveDir2) {
|
|
6198
|
-
const capPath =
|
|
6199
|
-
if (!
|
|
6614
|
+
const capPath = path28.join(forgehiveDir2, "capabilities.yaml");
|
|
6615
|
+
if (!fs27.existsSync(capPath)) return {};
|
|
6200
6616
|
try {
|
|
6201
|
-
return jsYaml.load(
|
|
6617
|
+
return jsYaml.load(fs27.readFileSync(capPath, "utf8")) ?? {};
|
|
6202
6618
|
} catch {
|
|
6203
6619
|
return {};
|
|
6204
6620
|
}
|
|
6205
6621
|
}
|
|
6206
6622
|
function readMemoryFiles(forgehiveDir2) {
|
|
6207
|
-
const memDir =
|
|
6208
|
-
if (!
|
|
6623
|
+
const memDir = path28.join(forgehiveDir2, "memory");
|
|
6624
|
+
if (!fs27.existsSync(memDir)) return {};
|
|
6209
6625
|
const result = {};
|
|
6210
|
-
for (const f of
|
|
6211
|
-
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");
|
|
6212
6628
|
}
|
|
6213
6629
|
return result;
|
|
6214
6630
|
}
|
|
6215
6631
|
function listAdrs2(forgehiveDir2) {
|
|
6216
|
-
const adrsDir =
|
|
6217
|
-
if (!
|
|
6218
|
-
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"));
|
|
6219
6635
|
}
|
|
6220
6636
|
function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
|
|
6221
|
-
const projectName =
|
|
6637
|
+
const projectName = path28.basename(projectRoot2);
|
|
6222
6638
|
const caps = readCapabilities(forgehiveDir2);
|
|
6223
6639
|
const memFiles = readMemoryFiles(forgehiveDir2);
|
|
6224
6640
|
const commits = getRecentCommits(projectRoot2);
|
|
@@ -6433,13 +6849,13 @@ function getMetricsGitLog(projectRoot2, since) {
|
|
|
6433
6849
|
}
|
|
6434
6850
|
|
|
6435
6851
|
// src/sync.ts
|
|
6436
|
-
import
|
|
6437
|
-
import
|
|
6852
|
+
import fs28 from "node:fs";
|
|
6853
|
+
import path29 from "node:path";
|
|
6438
6854
|
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
6439
6855
|
function getSyncStatus(forgehiveDir2) {
|
|
6440
|
-
const memDir =
|
|
6441
|
-
const files =
|
|
6442
|
-
const projectRoot2 =
|
|
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);
|
|
6443
6859
|
const configResult = spawnSync11(
|
|
6444
6860
|
"git",
|
|
6445
6861
|
["config", "--get", "forgehive.sync-remote"],
|
|
@@ -6455,18 +6871,18 @@ function getSyncStatus(forgehiveDir2) {
|
|
|
6455
6871
|
return { files, hasRemote: remote !== null, remote, branch };
|
|
6456
6872
|
}
|
|
6457
6873
|
function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6458
|
-
const projectRoot2 =
|
|
6459
|
-
const memDir =
|
|
6460
|
-
if (!
|
|
6874
|
+
const projectRoot2 = path29.dirname(forgehiveDir2);
|
|
6875
|
+
const memDir = path29.join(forgehiveDir2, "memory");
|
|
6876
|
+
if (!fs28.existsSync(memDir)) {
|
|
6461
6877
|
return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
|
|
6462
6878
|
}
|
|
6463
|
-
const files =
|
|
6879
|
+
const files = fs28.readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
6464
6880
|
if (files.length === 0) {
|
|
6465
6881
|
return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
|
|
6466
6882
|
}
|
|
6467
6883
|
const addResult = spawnSync11(
|
|
6468
6884
|
"git",
|
|
6469
|
-
["add",
|
|
6885
|
+
["add", path29.join(".forgehive", "memory")],
|
|
6470
6886
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6471
6887
|
);
|
|
6472
6888
|
if (addResult.status !== 0) {
|
|
@@ -6494,9 +6910,9 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6494
6910
|
return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
|
|
6495
6911
|
}
|
|
6496
6912
|
function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6497
|
-
const projectRoot2 =
|
|
6498
|
-
const memDir =
|
|
6499
|
-
|
|
6913
|
+
const projectRoot2 = path29.dirname(forgehiveDir2);
|
|
6914
|
+
const memDir = path29.join(forgehiveDir2, "memory");
|
|
6915
|
+
fs28.mkdirSync(memDir, { recursive: true });
|
|
6500
6916
|
const fetchResult = spawnSync11(
|
|
6501
6917
|
"git",
|
|
6502
6918
|
["fetch", remote, branch],
|
|
@@ -6516,16 +6932,16 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6516
6932
|
const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
|
|
6517
6933
|
const imported = [];
|
|
6518
6934
|
for (const remotePath of remoteFiles) {
|
|
6519
|
-
const filename =
|
|
6520
|
-
const localPath =
|
|
6521
|
-
if (!
|
|
6935
|
+
const filename = path29.basename(remotePath);
|
|
6936
|
+
const localPath = path29.join(memDir, filename);
|
|
6937
|
+
if (!fs28.existsSync(localPath)) {
|
|
6522
6938
|
const contentResult = spawnSync11(
|
|
6523
6939
|
"git",
|
|
6524
6940
|
["show", `${remote}/${branch}:${remotePath}`],
|
|
6525
6941
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6526
6942
|
);
|
|
6527
6943
|
if (contentResult.status === 0) {
|
|
6528
|
-
|
|
6944
|
+
fs28.writeFileSync(localPath, contentResult.stdout, "utf8");
|
|
6529
6945
|
imported.push(filename);
|
|
6530
6946
|
}
|
|
6531
6947
|
}
|
|
@@ -6538,8 +6954,8 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6538
6954
|
}
|
|
6539
6955
|
|
|
6540
6956
|
// src/background.ts
|
|
6541
|
-
import
|
|
6542
|
-
import
|
|
6957
|
+
import fs29 from "node:fs";
|
|
6958
|
+
import path30 from "node:path";
|
|
6543
6959
|
import { spawn } from "node:child_process";
|
|
6544
6960
|
var AGENT_ROLES = {
|
|
6545
6961
|
kai: "Senior Engineer \u2014 implements features and fixes bugs",
|
|
@@ -6586,23 +7002,23 @@ Instructions:
|
|
|
6586
7002
|
Work autonomously. Do not ask for clarification \u2014 use your best judgment based on the issue description and codebase context.`;
|
|
6587
7003
|
}
|
|
6588
7004
|
function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
6589
|
-
const logsDir =
|
|
6590
|
-
|
|
7005
|
+
const logsDir = path30.join(forgehiveDir2, "background-runs");
|
|
7006
|
+
fs29.mkdirSync(logsDir, { recursive: true });
|
|
6591
7007
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6592
|
-
const logFile =
|
|
7008
|
+
const logFile = path30.join(logsDir, `${agentId}-${timestamp2}.log`);
|
|
6593
7009
|
const prompt = buildAgentPrompt(issueUrl, agentId);
|
|
6594
|
-
const logStream =
|
|
7010
|
+
const logStream = fs29.openSync(logFile, "w");
|
|
6595
7011
|
const child = spawn(
|
|
6596
7012
|
"claude",
|
|
6597
7013
|
["-p", prompt, "--output-format", "text"],
|
|
6598
7014
|
{
|
|
6599
|
-
cwd:
|
|
7015
|
+
cwd: path30.dirname(forgehiveDir2),
|
|
6600
7016
|
detached: true,
|
|
6601
7017
|
stdio: ["ignore", logStream, logStream]
|
|
6602
7018
|
}
|
|
6603
7019
|
);
|
|
6604
7020
|
child.unref();
|
|
6605
|
-
|
|
7021
|
+
fs29.closeSync(logStream);
|
|
6606
7022
|
return {
|
|
6607
7023
|
pid: child.pid,
|
|
6608
7024
|
logFile,
|
|
@@ -6612,11 +7028,11 @@ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
|
6612
7028
|
|
|
6613
7029
|
// src/stories.ts
|
|
6614
7030
|
init_js_yaml();
|
|
6615
|
-
import
|
|
6616
|
-
import
|
|
7031
|
+
import fs30 from "node:fs";
|
|
7032
|
+
import path31 from "node:path";
|
|
6617
7033
|
function nextStoryId(storiesDir) {
|
|
6618
|
-
if (!
|
|
6619
|
-
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));
|
|
6620
7036
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6621
7037
|
return `US-${max + 1}`;
|
|
6622
7038
|
}
|
|
@@ -6648,7 +7064,7 @@ ${acLines || "- [ ] (noch nicht definiert)"}
|
|
|
6648
7064
|
}
|
|
6649
7065
|
function parseStoryFile(filePath) {
|
|
6650
7066
|
try {
|
|
6651
|
-
const content =
|
|
7067
|
+
const content = fs30.readFileSync(filePath, "utf8");
|
|
6652
7068
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6653
7069
|
if (!match) return null;
|
|
6654
7070
|
const data = jsYaml.load(match[1]);
|
|
@@ -6668,7 +7084,7 @@ function parseStoryFile(filePath) {
|
|
|
6668
7084
|
}
|
|
6669
7085
|
}
|
|
6670
7086
|
function createStory(storiesDir, title, epicId) {
|
|
6671
|
-
|
|
7087
|
+
fs30.mkdirSync(storiesDir, { recursive: true });
|
|
6672
7088
|
const id = nextStoryId(storiesDir);
|
|
6673
7089
|
const story = {
|
|
6674
7090
|
id,
|
|
@@ -6681,33 +7097,33 @@ function createStory(storiesDir, title, epicId) {
|
|
|
6681
7097
|
epicId: epicId ?? null,
|
|
6682
7098
|
status: "backlog"
|
|
6683
7099
|
};
|
|
6684
|
-
|
|
7100
|
+
fs30.writeFileSync(path31.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6685
7101
|
return story;
|
|
6686
7102
|
}
|
|
6687
7103
|
function listStories(storiesDir) {
|
|
6688
|
-
if (!
|
|
6689
|
-
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) => {
|
|
6690
7106
|
const na = parseInt(a.id.replace("US-", ""), 10);
|
|
6691
7107
|
const nb = parseInt(b.id.replace("US-", ""), 10);
|
|
6692
7108
|
return na - nb;
|
|
6693
7109
|
});
|
|
6694
7110
|
}
|
|
6695
7111
|
function getStory(storiesDir, id) {
|
|
6696
|
-
const filePath =
|
|
6697
|
-
if (!
|
|
7112
|
+
const filePath = path31.join(storiesDir, `${id}.md`);
|
|
7113
|
+
if (!fs30.existsSync(filePath)) return null;
|
|
6698
7114
|
return parseStoryFile(filePath);
|
|
6699
7115
|
}
|
|
6700
7116
|
function updateStoryPoints(storiesDir, id, points) {
|
|
6701
7117
|
const story = getStory(storiesDir, id);
|
|
6702
7118
|
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6703
7119
|
story.points = points;
|
|
6704
|
-
|
|
7120
|
+
fs30.writeFileSync(path31.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6705
7121
|
}
|
|
6706
7122
|
function updateStoryStatus(storiesDir, id, status) {
|
|
6707
7123
|
const story = getStory(storiesDir, id);
|
|
6708
7124
|
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6709
7125
|
story.status = status;
|
|
6710
|
-
|
|
7126
|
+
fs30.writeFileSync(path31.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6711
7127
|
}
|
|
6712
7128
|
function formatStoryCard(story) {
|
|
6713
7129
|
const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
|
|
@@ -6726,11 +7142,11 @@ function formatStoryCard(story) {
|
|
|
6726
7142
|
|
|
6727
7143
|
// src/epics.ts
|
|
6728
7144
|
init_js_yaml();
|
|
6729
|
-
import
|
|
6730
|
-
import
|
|
7145
|
+
import fs31 from "node:fs";
|
|
7146
|
+
import path32 from "node:path";
|
|
6731
7147
|
function nextEpicId(epicsDir) {
|
|
6732
|
-
if (!
|
|
6733
|
-
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));
|
|
6734
7150
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6735
7151
|
return `EPC-${max + 1}`;
|
|
6736
7152
|
}
|
|
@@ -6758,7 +7174,7 @@ ${storyLines || "(noch keine Stories)"}
|
|
|
6758
7174
|
}
|
|
6759
7175
|
function parseEpicFile(filePath) {
|
|
6760
7176
|
try {
|
|
6761
|
-
const content =
|
|
7177
|
+
const content = fs31.readFileSync(filePath, "utf8");
|
|
6762
7178
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6763
7179
|
if (!match) return null;
|
|
6764
7180
|
const data = jsYaml.load(match[1]);
|
|
@@ -6775,23 +7191,23 @@ function parseEpicFile(filePath) {
|
|
|
6775
7191
|
}
|
|
6776
7192
|
}
|
|
6777
7193
|
function createEpic(epicsDir, title, goal, prdId) {
|
|
6778
|
-
|
|
7194
|
+
fs31.mkdirSync(epicsDir, { recursive: true });
|
|
6779
7195
|
const id = nextEpicId(epicsDir);
|
|
6780
7196
|
const epic = { id, title, goal: goal ?? "", stories: [], status: "active", prdId: prdId ?? null };
|
|
6781
|
-
|
|
7197
|
+
fs31.writeFileSync(path32.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
|
|
6782
7198
|
return epic;
|
|
6783
7199
|
}
|
|
6784
7200
|
function listEpics(epicsDir) {
|
|
6785
|
-
if (!
|
|
6786
|
-
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) => {
|
|
6787
7203
|
const na = parseInt(a.id.replace("EPC-", ""), 10);
|
|
6788
7204
|
const nb = parseInt(b.id.replace("EPC-", ""), 10);
|
|
6789
7205
|
return na - nb;
|
|
6790
7206
|
});
|
|
6791
7207
|
}
|
|
6792
7208
|
function getEpic(epicsDir, id) {
|
|
6793
|
-
const filePath =
|
|
6794
|
-
if (!
|
|
7209
|
+
const filePath = path32.join(epicsDir, `${id}.md`);
|
|
7210
|
+
if (!fs31.existsSync(filePath)) return null;
|
|
6795
7211
|
return parseEpicFile(filePath);
|
|
6796
7212
|
}
|
|
6797
7213
|
function formatEpicCard(epic, stories) {
|
|
@@ -6815,14 +7231,14 @@ function formatEpicCard(epic, stories) {
|
|
|
6815
7231
|
|
|
6816
7232
|
// src/product.ts
|
|
6817
7233
|
init_js_yaml();
|
|
6818
|
-
import
|
|
6819
|
-
import
|
|
7234
|
+
import fs32 from "node:fs";
|
|
7235
|
+
import path33 from "node:path";
|
|
6820
7236
|
function slugify2(title) {
|
|
6821
7237
|
return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-");
|
|
6822
7238
|
}
|
|
6823
7239
|
function nextPrdId(prdsDir) {
|
|
6824
|
-
if (!
|
|
6825
|
-
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));
|
|
6826
7242
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6827
7243
|
return `PRD-${max + 1}`;
|
|
6828
7244
|
}
|
|
@@ -6860,7 +7276,7 @@ ${frontmatter}---
|
|
|
6860
7276
|
}
|
|
6861
7277
|
function parsePrdFile(filePath) {
|
|
6862
7278
|
try {
|
|
6863
|
-
const content =
|
|
7279
|
+
const content = fs32.readFileSync(filePath, "utf8");
|
|
6864
7280
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6865
7281
|
if (!match) return null;
|
|
6866
7282
|
const data = jsYaml.load(match[1]);
|
|
@@ -6877,37 +7293,37 @@ function parsePrdFile(filePath) {
|
|
|
6877
7293
|
}
|
|
6878
7294
|
}
|
|
6879
7295
|
function createPrd(prdsDir, title) {
|
|
6880
|
-
|
|
7296
|
+
fs32.mkdirSync(prdsDir, { recursive: true });
|
|
6881
7297
|
const id = nextPrdId(prdsDir);
|
|
6882
7298
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6883
7299
|
const slug = slugify2(title);
|
|
6884
7300
|
const filename = `${today}-${slug}.md`;
|
|
6885
|
-
const filepath =
|
|
7301
|
+
const filepath = path33.join(prdsDir, filename);
|
|
6886
7302
|
const prd = { id, title, date: today, status: "draft", filepath };
|
|
6887
|
-
|
|
7303
|
+
fs32.writeFileSync(filepath, prdToMarkdown(prd), "utf8");
|
|
6888
7304
|
return prd;
|
|
6889
7305
|
}
|
|
6890
7306
|
function listPrds(prdsDir) {
|
|
6891
|
-
if (!
|
|
6892
|
-
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) => {
|
|
6893
7309
|
const na = parseInt(a.id.replace("PRD-", ""), 10);
|
|
6894
7310
|
const nb = parseInt(b.id.replace("PRD-", ""), 10);
|
|
6895
7311
|
return na - nb;
|
|
6896
7312
|
});
|
|
6897
7313
|
}
|
|
6898
7314
|
function getPrd(prdsDir, id) {
|
|
6899
|
-
if (!
|
|
6900
|
-
const files =
|
|
7315
|
+
if (!fs32.existsSync(prdsDir)) return null;
|
|
7316
|
+
const files = fs32.readdirSync(prdsDir).filter((f) => f.endsWith(".md"));
|
|
6901
7317
|
for (const f of files) {
|
|
6902
|
-
const prd = parsePrdFile(
|
|
7318
|
+
const prd = parsePrdFile(path33.join(prdsDir, f));
|
|
6903
7319
|
if (prd && prd.id === id) return prd;
|
|
6904
7320
|
}
|
|
6905
7321
|
return null;
|
|
6906
7322
|
}
|
|
6907
7323
|
function generateRoadmap(forgehiveDir2) {
|
|
6908
|
-
const prdsDir =
|
|
6909
|
-
const epicsDir =
|
|
6910
|
-
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");
|
|
6911
7327
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6912
7328
|
const prds = listPrds(prdsDir);
|
|
6913
7329
|
const epics = listEpics(epicsDir);
|
|
@@ -6977,24 +7393,24 @@ function generateRoadmap(forgehiveDir2) {
|
|
|
6977
7393
|
}
|
|
6978
7394
|
|
|
6979
7395
|
// src/velocity.ts
|
|
6980
|
-
import
|
|
6981
|
-
import
|
|
7396
|
+
import fs33 from "node:fs";
|
|
7397
|
+
import path34 from "node:path";
|
|
6982
7398
|
var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
|
|
6983
7399
|
function recordVelocity(velocityFile, sprint, committed, delivered) {
|
|
6984
7400
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6985
7401
|
const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
|
|
6986
7402
|
const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
|
|
6987
7403
|
`;
|
|
6988
|
-
if (!
|
|
6989
|
-
|
|
6990
|
-
|
|
7404
|
+
if (!fs33.existsSync(velocityFile)) {
|
|
7405
|
+
fs33.mkdirSync(path34.dirname(velocityFile), { recursive: true });
|
|
7406
|
+
fs33.writeFileSync(velocityFile, HEADER + row, "utf8");
|
|
6991
7407
|
} else {
|
|
6992
|
-
|
|
7408
|
+
fs33.appendFileSync(velocityFile, row, "utf8");
|
|
6993
7409
|
}
|
|
6994
7410
|
}
|
|
6995
7411
|
function getVelocityHistory(velocityFile) {
|
|
6996
|
-
if (!
|
|
6997
|
-
const content =
|
|
7412
|
+
if (!fs33.existsSync(velocityFile)) return [];
|
|
7413
|
+
const content = fs33.readFileSync(velocityFile, "utf8");
|
|
6998
7414
|
const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
|
|
6999
7415
|
return rows.map((row) => {
|
|
7000
7416
|
const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
|
|
@@ -7035,8 +7451,8 @@ function formatVelocityReport(history) {
|
|
|
7035
7451
|
|
|
7036
7452
|
// src/docs.ts
|
|
7037
7453
|
init_js_yaml();
|
|
7038
|
-
import
|
|
7039
|
-
import
|
|
7454
|
+
import fs34 from "node:fs";
|
|
7455
|
+
import path35 from "node:path";
|
|
7040
7456
|
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
7041
7457
|
var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
|
|
7042
7458
|
var IGNORE_DIRS4 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
|
|
@@ -7047,10 +7463,10 @@ var EXPORT_PATTERNS2 = [
|
|
|
7047
7463
|
/^export\s+default\s+(?:function\s+)?(\w+)?/gm
|
|
7048
7464
|
];
|
|
7049
7465
|
function readCapabilities2(forgehiveDir2) {
|
|
7050
|
-
const capPath =
|
|
7051
|
-
if (!
|
|
7466
|
+
const capPath = path35.join(forgehiveDir2, "capabilities.yaml");
|
|
7467
|
+
if (!fs34.existsSync(capPath)) return {};
|
|
7052
7468
|
try {
|
|
7053
|
-
return jsYaml.load(
|
|
7469
|
+
return jsYaml.load(fs34.readFileSync(capPath, "utf8")) ?? {};
|
|
7054
7470
|
} catch {
|
|
7055
7471
|
return {};
|
|
7056
7472
|
}
|
|
@@ -7077,11 +7493,11 @@ function extractCapabilityInfo(caps) {
|
|
|
7077
7493
|
return { language, packageManager };
|
|
7078
7494
|
}
|
|
7079
7495
|
function readMemoryFiles2(forgehiveDir2) {
|
|
7080
|
-
const memDir =
|
|
7081
|
-
if (!
|
|
7496
|
+
const memDir = path35.join(forgehiveDir2, "memory");
|
|
7497
|
+
if (!fs34.existsSync(memDir)) return {};
|
|
7082
7498
|
const result = {};
|
|
7083
|
-
for (const f of
|
|
7084
|
-
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");
|
|
7085
7501
|
}
|
|
7086
7502
|
return result;
|
|
7087
7503
|
}
|
|
@@ -7104,19 +7520,19 @@ function extractExports(content) {
|
|
|
7104
7520
|
function walkSourceFiles(dir) {
|
|
7105
7521
|
const results = [];
|
|
7106
7522
|
function walk(current) {
|
|
7107
|
-
if (!
|
|
7108
|
-
for (const entry of
|
|
7523
|
+
if (!fs34.existsSync(current)) return;
|
|
7524
|
+
for (const entry of fs34.readdirSync(current, { withFileTypes: true })) {
|
|
7109
7525
|
if (IGNORE_DIRS4.includes(entry.name)) continue;
|
|
7110
|
-
const full =
|
|
7526
|
+
const full = path35.join(current, entry.name);
|
|
7111
7527
|
if (entry.isDirectory()) walk(full);
|
|
7112
|
-
else if (entry.isFile() && SOURCE_EXTS.includes(
|
|
7528
|
+
else if (entry.isFile() && SOURCE_EXTS.includes(path35.extname(entry.name))) results.push(full);
|
|
7113
7529
|
}
|
|
7114
7530
|
}
|
|
7115
7531
|
walk(dir);
|
|
7116
7532
|
return results;
|
|
7117
7533
|
}
|
|
7118
7534
|
function generateUserGuide(projectRoot2, forgehiveDir2) {
|
|
7119
|
-
const projectName =
|
|
7535
|
+
const projectName = path35.basename(projectRoot2);
|
|
7120
7536
|
const caps = readCapabilities2(forgehiveDir2);
|
|
7121
7537
|
const { language: lang, packageManager, entryPoints } = extractCapabilityInfo(caps);
|
|
7122
7538
|
const memFiles = readMemoryFiles2(forgehiveDir2);
|
|
@@ -7195,8 +7611,8 @@ function generateUserGuide(projectRoot2, forgehiveDir2) {
|
|
|
7195
7611
|
return lines.join("\n");
|
|
7196
7612
|
}
|
|
7197
7613
|
function generateApiReference(projectRoot2) {
|
|
7198
|
-
const srcDir =
|
|
7199
|
-
const searchDir =
|
|
7614
|
+
const srcDir = path35.join(projectRoot2, "src");
|
|
7615
|
+
const searchDir = fs34.existsSync(srcDir) ? srcDir : projectRoot2;
|
|
7200
7616
|
const files = walkSourceFiles(searchDir);
|
|
7201
7617
|
const lines = [];
|
|
7202
7618
|
lines.push("# API Reference");
|
|
@@ -7210,13 +7626,13 @@ function generateApiReference(projectRoot2) {
|
|
|
7210
7626
|
for (const filePath of files) {
|
|
7211
7627
|
let content = "";
|
|
7212
7628
|
try {
|
|
7213
|
-
content =
|
|
7629
|
+
content = fs34.readFileSync(filePath, "utf8");
|
|
7214
7630
|
} catch {
|
|
7215
7631
|
continue;
|
|
7216
7632
|
}
|
|
7217
7633
|
const exports = extractExports(content);
|
|
7218
7634
|
if (exports.length === 0) continue;
|
|
7219
|
-
const relPath =
|
|
7635
|
+
const relPath = path35.relative(projectRoot2, filePath);
|
|
7220
7636
|
lines.push(`## \`${relPath}\``);
|
|
7221
7637
|
lines.push("");
|
|
7222
7638
|
lines.push("**Exports:**");
|
|
@@ -7228,23 +7644,23 @@ function generateApiReference(projectRoot2) {
|
|
|
7228
7644
|
}
|
|
7229
7645
|
function listExistingDocs(projectRoot2) {
|
|
7230
7646
|
const docs = [];
|
|
7231
|
-
const docsDir =
|
|
7232
|
-
if (
|
|
7233
|
-
for (const f of
|
|
7234
|
-
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));
|
|
7235
7651
|
}
|
|
7236
7652
|
}
|
|
7237
7653
|
const rootDocs = ["README.md", "CHANGELOG.md", "ONBOARDING.md", "CONTRIBUTING.md"];
|
|
7238
7654
|
for (const f of rootDocs) {
|
|
7239
|
-
const full =
|
|
7240
|
-
if (
|
|
7655
|
+
const full = path35.join(projectRoot2, f);
|
|
7656
|
+
if (fs34.existsSync(full)) docs.push(full);
|
|
7241
7657
|
}
|
|
7242
7658
|
return docs;
|
|
7243
7659
|
}
|
|
7244
7660
|
|
|
7245
7661
|
// src/router.ts
|
|
7246
|
-
import
|
|
7247
|
-
import
|
|
7662
|
+
import fs35 from "node:fs";
|
|
7663
|
+
import path36 from "node:path";
|
|
7248
7664
|
var AGENT_KEYWORDS = {
|
|
7249
7665
|
vera: ["security", "auth", "vulnerability", "owasp", "cve", "gdpr", "compliance", "penetration", "exploit"],
|
|
7250
7666
|
sam: ["test", "coverage", "qa", "quality", "spec", "regression", "fixture", "assertion", "e2e"],
|
|
@@ -7353,9 +7769,9 @@ function routeTask(task) {
|
|
|
7353
7769
|
}
|
|
7354
7770
|
function resolveWorktreePath(forgehiveDir2, agent) {
|
|
7355
7771
|
if (!agent) return void 0;
|
|
7356
|
-
const worktreeFile =
|
|
7357
|
-
if (
|
|
7358
|
-
return
|
|
7772
|
+
const worktreeFile = path36.join(forgehiveDir2, "agents", agent, "worktree");
|
|
7773
|
+
if (fs35.existsSync(worktreeFile)) {
|
|
7774
|
+
return fs35.readFileSync(worktreeFile, "utf8").trim();
|
|
7359
7775
|
}
|
|
7360
7776
|
return void 0;
|
|
7361
7777
|
}
|
|
@@ -7384,17 +7800,18 @@ function formatRoutingResult(result, worktreePath) {
|
|
|
7384
7800
|
|
|
7385
7801
|
// src/github.ts
|
|
7386
7802
|
init_js_yaml();
|
|
7387
|
-
import
|
|
7388
|
-
import
|
|
7803
|
+
import fs36 from "node:fs";
|
|
7804
|
+
import path37 from "node:path";
|
|
7389
7805
|
import https from "node:https";
|
|
7806
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
7390
7807
|
function loadGitHubConfig(forgehiveDir2) {
|
|
7391
|
-
const configPath =
|
|
7392
|
-
if (!
|
|
7808
|
+
const configPath = path37.join(forgehiveDir2, "github.yaml");
|
|
7809
|
+
if (!fs36.existsSync(configPath)) {
|
|
7393
7810
|
throw new Error("Nicht konfiguriert. F\xFChre zuerst aus: fh github setup");
|
|
7394
7811
|
}
|
|
7395
7812
|
let parsed;
|
|
7396
7813
|
try {
|
|
7397
|
-
parsed = jsYaml.load(
|
|
7814
|
+
parsed = jsYaml.load(fs36.readFileSync(configPath, "utf8"));
|
|
7398
7815
|
} catch {
|
|
7399
7816
|
throw new Error("Ung\xFCltige Konfigurationsdatei: .forgehive/github.yaml ist kein g\xFCltiges YAML.");
|
|
7400
7817
|
}
|
|
@@ -7408,9 +7825,9 @@ function loadGitHubConfig(forgehiveDir2) {
|
|
|
7408
7825
|
return { repo: data.repo };
|
|
7409
7826
|
}
|
|
7410
7827
|
function saveGitHubConfig(forgehiveDir2, config) {
|
|
7411
|
-
|
|
7412
|
-
const configPath =
|
|
7413
|
-
|
|
7828
|
+
fs36.mkdirSync(forgehiveDir2, { recursive: true });
|
|
7829
|
+
const configPath = path37.join(forgehiveDir2, "github.yaml");
|
|
7830
|
+
fs36.writeFileSync(configPath, jsYaml.dump({ repo: config.repo }), "utf8");
|
|
7414
7831
|
}
|
|
7415
7832
|
function githubGet(apiPath, token) {
|
|
7416
7833
|
return new Promise((resolve, reject) => {
|
|
@@ -7492,22 +7909,22 @@ async function fetchPRFiles(owner, repo, number, token) {
|
|
|
7492
7909
|
return files;
|
|
7493
7910
|
}
|
|
7494
7911
|
function issueAlreadySynced(storiesDir, issueNumber) {
|
|
7495
|
-
if (!
|
|
7912
|
+
if (!fs36.existsSync(storiesDir)) return false;
|
|
7496
7913
|
const pattern = new RegExp(`GitHub: #${issueNumber}(?![\\d\\w])`);
|
|
7497
|
-
return
|
|
7914
|
+
return fs36.readdirSync(storiesDir).filter((f) => f.endsWith(".md")).some((f) => pattern.test(fs36.readFileSync(path37.join(storiesDir, f), "utf8")));
|
|
7498
7915
|
}
|
|
7499
7916
|
function appendGitHubRef(storiesDir, storyId, issue, repoFullName) {
|
|
7500
|
-
const filePath =
|
|
7501
|
-
if (!
|
|
7917
|
+
const filePath = path37.join(storiesDir, `${storyId}.md`);
|
|
7918
|
+
if (!fs36.existsSync(filePath)) {
|
|
7502
7919
|
throw new Error(`Story-Datei nicht gefunden: ${storyId}.md`);
|
|
7503
7920
|
}
|
|
7504
|
-
const existing =
|
|
7921
|
+
const existing = fs36.readFileSync(filePath, "utf8");
|
|
7505
7922
|
const ref = `
|
|
7506
7923
|
## GitHub
|
|
7507
7924
|
|
|
7508
7925
|
GitHub: #${issue.number} \u2014 ${issue.html_url}
|
|
7509
7926
|
`;
|
|
7510
|
-
|
|
7927
|
+
fs36.writeFileSync(filePath, existing.trimEnd() + "\n" + ref, "utf8");
|
|
7511
7928
|
}
|
|
7512
7929
|
function formatPRContext(pr, files, repoFullName) {
|
|
7513
7930
|
const body = pr.body?.trim() || "(keine Beschreibung)";
|
|
@@ -7543,6 +7960,366 @@ async function fetchOpenPRs(owner, repo, token) {
|
|
|
7543
7960
|
}
|
|
7544
7961
|
return prs.map((pr) => pr.number);
|
|
7545
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
|
+
}
|
|
7546
8323
|
|
|
7547
8324
|
// src/cli.ts
|
|
7548
8325
|
import { createRequire } from "node:module";
|
|
@@ -7550,7 +8327,7 @@ var require2 = createRequire(import.meta.url);
|
|
|
7550
8327
|
var { version } = require2("../package.json");
|
|
7551
8328
|
var [, , command, subcommand, ...rest] = process.argv;
|
|
7552
8329
|
var projectRoot = process.cwd();
|
|
7553
|
-
var forgehiveDir =
|
|
8330
|
+
var forgehiveDir = path40.join(projectRoot, ".forgehive");
|
|
7554
8331
|
if (command === "--version" || command === "-v") {
|
|
7555
8332
|
console.log(version);
|
|
7556
8333
|
process.exit(0);
|
|
@@ -7566,7 +8343,14 @@ SETUP
|
|
|
7566
8343
|
fh init [--yes] Set up forgehive in the current project
|
|
7567
8344
|
fh confirm Activate capabilities (draft \u2192 confirmed)
|
|
7568
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
|
+
|
|
7569
8352
|
fh status Show current project state
|
|
8353
|
+
fh next Zeigt n\xE4chste empfohlene Aktion (Phase + Befehl)
|
|
7570
8354
|
fh watch [--smart] Observe project; --smart runs tests on change and suggests agents
|
|
7571
8355
|
fh scan --update Re-scan project after changes
|
|
7572
8356
|
fh scan --check Check if scan is still current
|
|
@@ -7623,6 +8407,7 @@ TEAM
|
|
|
7623
8407
|
|
|
7624
8408
|
AGENTS & MCP
|
|
7625
8409
|
fh party [--set name|run|status|cleanup]
|
|
8410
|
+
fh outcomes Show party/auto run statistics and last 5 runs
|
|
7626
8411
|
fh wire <service> Configure MCP server
|
|
7627
8412
|
fh mcp auth add|list|remove Manage credentials
|
|
7628
8413
|
fh mcp search <query> Search MCP registry
|
|
@@ -7638,19 +8423,19 @@ COST
|
|
|
7638
8423
|
process.exit(0);
|
|
7639
8424
|
}
|
|
7640
8425
|
function loadClaudeMdBlock() {
|
|
7641
|
-
const templatePath =
|
|
7642
|
-
|
|
8426
|
+
const templatePath = path40.join(
|
|
8427
|
+
path40.dirname(new URL(import.meta.url).pathname),
|
|
7643
8428
|
"..",
|
|
7644
8429
|
"forgehive",
|
|
7645
8430
|
"templates",
|
|
7646
8431
|
"claude-md.block.md"
|
|
7647
8432
|
);
|
|
7648
|
-
if (!
|
|
7649
|
-
return
|
|
8433
|
+
if (!fs39.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
|
|
8434
|
+
return fs39.readFileSync(templatePath, "utf8");
|
|
7650
8435
|
}
|
|
7651
8436
|
async function promptConfirm(question) {
|
|
7652
8437
|
if (!process.stdin.isTTY) return false;
|
|
7653
|
-
const rl =
|
|
8438
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
7654
8439
|
return new Promise((resolve) => {
|
|
7655
8440
|
rl.question(question, (answer) => {
|
|
7656
8441
|
rl.close();
|
|
@@ -7669,13 +8454,13 @@ function buildCapabilitySummary(ids) {
|
|
|
7669
8454
|
}
|
|
7670
8455
|
if (command === "init") {
|
|
7671
8456
|
(async () => {
|
|
7672
|
-
const gitCheck =
|
|
8457
|
+
const gitCheck = spawnSync15("git", ["--version"], { stdio: "ignore" });
|
|
7673
8458
|
if (gitCheck.error || gitCheck.status !== 0) {
|
|
7674
8459
|
console.error("Fehler: git nicht gefunden.");
|
|
7675
8460
|
console.error(" Installation: https://git-scm.com");
|
|
7676
8461
|
process.exit(1);
|
|
7677
8462
|
}
|
|
7678
|
-
const forgehiveDirExists =
|
|
8463
|
+
const forgehiveDirExists = fs39.existsSync(forgehiveDir);
|
|
7679
8464
|
if (forgehiveDirExists && subcommand !== "--force" && !rest.includes("--force")) {
|
|
7680
8465
|
console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
|
|
7681
8466
|
console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
|
|
@@ -7693,9 +8478,9 @@ if (command === "init") {
|
|
|
7693
8478
|
const block = loadClaudeMdBlock();
|
|
7694
8479
|
writeForgehiveDir(projectRoot, scanResult, capMap, block);
|
|
7695
8480
|
const hash = computeHash(projectRoot);
|
|
7696
|
-
|
|
7697
|
-
const runtimeDir =
|
|
7698
|
-
|
|
8481
|
+
fs39.writeFileSync(path40.join(forgehiveDir, ".scan-hash"), hash, "utf8");
|
|
8482
|
+
const runtimeDir = path40.join(
|
|
8483
|
+
path40.dirname(new URL(import.meta.url).pathname),
|
|
7699
8484
|
"..",
|
|
7700
8485
|
"forgehive"
|
|
7701
8486
|
);
|
|
@@ -7737,7 +8522,7 @@ if (command === "init") {
|
|
|
7737
8522
|
process.exit(1);
|
|
7738
8523
|
}
|
|
7739
8524
|
} else if (command === "memory") {
|
|
7740
|
-
if (!
|
|
8525
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7741
8526
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7742
8527
|
process.exit(1);
|
|
7743
8528
|
}
|
|
@@ -7746,7 +8531,7 @@ if (command === "init") {
|
|
|
7746
8531
|
} else if (subcommand === "clean") {
|
|
7747
8532
|
cleanMemory(forgehiveDir);
|
|
7748
8533
|
} else if (subcommand === "export") {
|
|
7749
|
-
const outputPath = rest[0] ??
|
|
8534
|
+
const outputPath = rest[0] ?? path40.join(projectRoot, "forgehive-memory-export.md");
|
|
7750
8535
|
try {
|
|
7751
8536
|
exportMemory(forgehiveDir, outputPath);
|
|
7752
8537
|
} catch (err) {
|
|
@@ -7794,7 +8579,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7794
8579
|
} else if (subcommand === "snapshot") {
|
|
7795
8580
|
const snapAction = rest[0];
|
|
7796
8581
|
if (snapAction === "export") {
|
|
7797
|
-
const outPath = rest[1] ??
|
|
8582
|
+
const outPath = rest[1] ?? path40.join(
|
|
7798
8583
|
projectRoot,
|
|
7799
8584
|
`forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
|
|
7800
8585
|
);
|
|
@@ -7834,11 +8619,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7834
8619
|
process.exit(1);
|
|
7835
8620
|
}
|
|
7836
8621
|
} else if (command === "scan" && subcommand === "--update") {
|
|
7837
|
-
if (!
|
|
8622
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7838
8623
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7839
8624
|
process.exit(1);
|
|
7840
8625
|
}
|
|
7841
|
-
const savedHash =
|
|
8626
|
+
const savedHash = fs39.existsSync(path40.join(forgehiveDir, ".scan-hash")) ? fs39.readFileSync(path40.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
|
|
7842
8627
|
const currentHash = computeHash(projectRoot);
|
|
7843
8628
|
if (savedHash === currentHash) {
|
|
7844
8629
|
console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
|
|
@@ -7846,7 +8631,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7846
8631
|
}
|
|
7847
8632
|
console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
|
|
7848
8633
|
const oldDoc = jsYaml.load(
|
|
7849
|
-
|
|
8634
|
+
fs39.readFileSync(path40.join(forgehiveDir, "capabilities.yaml"), "utf8")
|
|
7850
8635
|
);
|
|
7851
8636
|
const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
|
|
7852
8637
|
const scanResult = scan(projectRoot);
|
|
@@ -7866,16 +8651,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7866
8651
|
console.log();
|
|
7867
8652
|
const block = loadClaudeMdBlock();
|
|
7868
8653
|
writeForgehiveDir(projectRoot, scanResult, newMap, block);
|
|
7869
|
-
|
|
8654
|
+
fs39.writeFileSync(path40.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
|
|
7870
8655
|
console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
|
|
7871
8656
|
console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
|
|
7872
8657
|
}
|
|
7873
8658
|
} else if (command === "scan" && subcommand === "--check") {
|
|
7874
|
-
if (!
|
|
8659
|
+
if (!fs39.existsSync(path40.join(forgehiveDir, ".scan-hash"))) {
|
|
7875
8660
|
console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
|
|
7876
8661
|
process.exit(1);
|
|
7877
8662
|
}
|
|
7878
|
-
const saved =
|
|
8663
|
+
const saved = fs39.readFileSync(path40.join(forgehiveDir, ".scan-hash"), "utf8").trim();
|
|
7879
8664
|
const current = computeHash(projectRoot);
|
|
7880
8665
|
if (saved !== current) {
|
|
7881
8666
|
console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
|
|
@@ -7884,7 +8669,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7884
8669
|
}
|
|
7885
8670
|
console.log("\u2713 capabilities.yaml ist aktuell");
|
|
7886
8671
|
} else if (command === "skills") {
|
|
7887
|
-
if (!
|
|
8672
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7888
8673
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7889
8674
|
process.exit(1);
|
|
7890
8675
|
}
|
|
@@ -7920,7 +8705,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7920
8705
|
process.exit(1);
|
|
7921
8706
|
}
|
|
7922
8707
|
} else if (command === "party") {
|
|
7923
|
-
if (!
|
|
8708
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7924
8709
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7925
8710
|
process.exit(1);
|
|
7926
8711
|
}
|
|
@@ -8041,7 +8826,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
8041
8826
|
}
|
|
8042
8827
|
}
|
|
8043
8828
|
} else if (command === "wire") {
|
|
8044
|
-
if (!
|
|
8829
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8045
8830
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8046
8831
|
process.exit(1);
|
|
8047
8832
|
}
|
|
@@ -8073,16 +8858,47 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
8073
8858
|
console.error(`Fehler: ${err.message}`);
|
|
8074
8859
|
process.exit(1);
|
|
8075
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);
|
|
8076
8888
|
} else if (command === "status") {
|
|
8077
8889
|
console.log(projectStatus(projectRoot, forgehiveDir));
|
|
8078
8890
|
const dashboard = formatDashboard(projectRoot, forgehiveDir);
|
|
8079
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);
|
|
8080
8896
|
} else if (command === "cost") {
|
|
8081
8897
|
const allArgs = [subcommand, ...rest].filter((a) => Boolean(a));
|
|
8082
8898
|
const limitIdx = allArgs.indexOf("--limit");
|
|
8083
8899
|
const alertIdx = allArgs.indexOf("--alert");
|
|
8084
8900
|
if (limitIdx !== -1) {
|
|
8085
|
-
if (!
|
|
8901
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8086
8902
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8087
8903
|
process.exit(1);
|
|
8088
8904
|
}
|
|
@@ -8108,24 +8924,24 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
8108
8924
|
}
|
|
8109
8925
|
const sessions = parseCostSessions(projectRoot);
|
|
8110
8926
|
console.log(formatCostReport(sessions, range));
|
|
8111
|
-
if (
|
|
8927
|
+
if (fs39.existsSync(forgehiveDir)) {
|
|
8112
8928
|
const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
|
|
8113
8929
|
const status = checkSpendStatus(forgehiveDir, total);
|
|
8114
8930
|
if (status.message) console.log("\n" + status.message);
|
|
8115
8931
|
}
|
|
8116
8932
|
}
|
|
8117
8933
|
} else if (command === "watch") {
|
|
8118
|
-
if (!
|
|
8934
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8119
8935
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8120
8936
|
process.exit(1);
|
|
8121
8937
|
}
|
|
8122
8938
|
const isSmartMode = subcommand === "--smart" || rest.includes("--smart");
|
|
8123
8939
|
if (isSmartMode) {
|
|
8124
8940
|
let testCmd = "npm test";
|
|
8125
|
-
const capsPath =
|
|
8126
|
-
if (
|
|
8941
|
+
const capsPath = path40.join(forgehiveDir, "capabilities.yaml");
|
|
8942
|
+
if (fs39.existsSync(capsPath)) {
|
|
8127
8943
|
try {
|
|
8128
|
-
const caps = jsYaml.load(
|
|
8944
|
+
const caps = jsYaml.load(fs39.readFileSync(capsPath, "utf8"));
|
|
8129
8945
|
if (caps?.test_command) testCmd = caps.test_command;
|
|
8130
8946
|
} catch {
|
|
8131
8947
|
}
|
|
@@ -8158,7 +8974,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
8158
8974
|
});
|
|
8159
8975
|
}
|
|
8160
8976
|
} else if (command === "mcp") {
|
|
8161
|
-
if (!
|
|
8977
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8162
8978
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8163
8979
|
process.exit(1);
|
|
8164
8980
|
}
|
|
@@ -8269,7 +9085,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8269
9085
|
process.exit(1);
|
|
8270
9086
|
}
|
|
8271
9087
|
} else if (command === "security") {
|
|
8272
|
-
if (!
|
|
9088
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8273
9089
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8274
9090
|
process.exit(1);
|
|
8275
9091
|
}
|
|
@@ -8322,8 +9138,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8322
9138
|
high: high.length
|
|
8323
9139
|
}
|
|
8324
9140
|
});
|
|
8325
|
-
|
|
8326
|
-
|
|
9141
|
+
fs39.writeFileSync(
|
|
9142
|
+
path40.join(forgehiveDir, "security-last-scan.txt"),
|
|
8327
9143
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
8328
9144
|
"utf8"
|
|
8329
9145
|
);
|
|
@@ -8360,8 +9176,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8360
9176
|
`);
|
|
8361
9177
|
const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
|
|
8362
9178
|
const text = formatSecurityReport(report);
|
|
8363
|
-
const reportPath =
|
|
8364
|
-
|
|
9179
|
+
const reportPath = path40.join(forgehiveDir, "security-report.md");
|
|
9180
|
+
fs39.writeFileSync(reportPath, text, "utf8");
|
|
8365
9181
|
console.log(text);
|
|
8366
9182
|
console.log(`
|
|
8367
9183
|
\u2713 Report gespeichert: ${reportPath}`);
|
|
@@ -8373,8 +9189,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8373
9189
|
} else if (subcommand === "permissions") {
|
|
8374
9190
|
const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
8375
9191
|
writePermissions2(forgehiveDir);
|
|
8376
|
-
const permPath =
|
|
8377
|
-
console.log(
|
|
9192
|
+
const permPath = path40.join(forgehiveDir, "harness", "permissions.yaml");
|
|
9193
|
+
console.log(fs39.readFileSync(permPath, "utf8"));
|
|
8378
9194
|
} else {
|
|
8379
9195
|
console.error(`Unbekannter security-Subcommand: ${subcommand}`);
|
|
8380
9196
|
console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
|
|
@@ -8386,18 +9202,18 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8386
9202
|
const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
|
|
8387
9203
|
const initFlag = allCiArgs.includes("--init");
|
|
8388
9204
|
if (initFlag) {
|
|
8389
|
-
const ghDir =
|
|
8390
|
-
|
|
8391
|
-
const outPath =
|
|
8392
|
-
|
|
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");
|
|
8393
9209
|
console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
|
|
8394
9210
|
} else {
|
|
8395
9211
|
const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
|
|
8396
9212
|
const output = formatCiReport(report, format);
|
|
8397
9213
|
console.log(output);
|
|
8398
9214
|
if (format === "json") {
|
|
8399
|
-
|
|
8400
|
-
|
|
9215
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
9216
|
+
fs39.writeFileSync(path40.join(forgehiveDir, "ci-report.json"), output, "utf8");
|
|
8401
9217
|
}
|
|
8402
9218
|
if (report.status === "fail") process.exit(1);
|
|
8403
9219
|
}
|
|
@@ -8405,29 +9221,29 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8405
9221
|
const semanticFlag = rest.includes("--semantic");
|
|
8406
9222
|
const map2 = generateMap(projectRoot);
|
|
8407
9223
|
const semantic = buildSemanticMap(projectRoot);
|
|
8408
|
-
const mapPath =
|
|
8409
|
-
|
|
9224
|
+
const mapPath = path40.join(forgehiveDir, "map.md");
|
|
9225
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
8410
9226
|
if (semanticFlag) {
|
|
8411
9227
|
const fullMd = formatMap(map2, semantic);
|
|
8412
9228
|
const marker = "\n## Semantic Graph\n";
|
|
8413
9229
|
const markerIdx = fullMd.indexOf(marker);
|
|
8414
9230
|
const semanticMd = markerIdx >= 0 ? "## Semantic Graph\n" + fullMd.slice(markerIdx + marker.length) : fullMd;
|
|
8415
|
-
|
|
9231
|
+
fs39.writeFileSync(mapPath, fullMd, "utf8");
|
|
8416
9232
|
console.log(semanticMd);
|
|
8417
9233
|
console.log(`
|
|
8418
9234
|
\u2714 Codebase-Map gespeichert (Semantic Graph hervorgehoben): ${mapPath}`);
|
|
8419
9235
|
} else {
|
|
8420
9236
|
const md = formatMap(map2, semantic);
|
|
8421
|
-
|
|
9237
|
+
fs39.writeFileSync(mapPath, md, "utf8");
|
|
8422
9238
|
console.log(md);
|
|
8423
9239
|
console.log(`
|
|
8424
9240
|
\u2714 Codebase-Map gespeichert: ${mapPath}`);
|
|
8425
9241
|
}
|
|
8426
9242
|
} else if (command === "onboard") {
|
|
8427
9243
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8428
|
-
const outputPath = outputArg ??
|
|
9244
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "ONBOARDING.md");
|
|
8429
9245
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
8430
|
-
|
|
9246
|
+
fs39.writeFileSync(outputPath, doc, "utf8");
|
|
8431
9247
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
8432
9248
|
} else if (command === "ask") {
|
|
8433
9249
|
const taskArg = subcommand ? [subcommand, ...rest.filter((r) => !r.startsWith("--"))].join(" ") : rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
@@ -8450,28 +9266,28 @@ Task: "${taskArg}"
|
|
|
8450
9266
|
const commits = parseGitLog(rawLog);
|
|
8451
9267
|
let version2 = "unreleased";
|
|
8452
9268
|
try {
|
|
8453
|
-
const pkgPath =
|
|
8454
|
-
if (
|
|
8455
|
-
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, ""));
|
|
8456
9272
|
version2 = pkg.version ?? "unreleased";
|
|
8457
9273
|
}
|
|
8458
9274
|
} catch {
|
|
8459
9275
|
}
|
|
8460
9276
|
const md = formatChangelog(commits, version2);
|
|
8461
|
-
const outputPath = outputArg ??
|
|
9277
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "CHANGELOG.md");
|
|
8462
9278
|
let existing = "";
|
|
8463
|
-
if (
|
|
8464
|
-
|
|
9279
|
+
if (fs39.existsSync(outputPath)) existing = fs39.readFileSync(outputPath, "utf8");
|
|
9280
|
+
fs39.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
8465
9281
|
console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
|
|
8466
9282
|
console.log(` ${commits.length} Commits verarbeitet`);
|
|
8467
9283
|
} else if (command === "metrics") {
|
|
8468
9284
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
|
|
8469
9285
|
const rawLog = getMetricsGitLog(projectRoot, sinceArg);
|
|
8470
9286
|
const stats = parseCommitStats(rawLog);
|
|
8471
|
-
const md = formatMetrics(stats,
|
|
8472
|
-
const metricsPath =
|
|
8473
|
-
|
|
8474
|
-
|
|
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");
|
|
8475
9291
|
console.log(md);
|
|
8476
9292
|
console.log(`
|
|
8477
9293
|
\u2714 Metrics gespeichert: ${metricsPath}`);
|
|
@@ -8518,7 +9334,7 @@ Task: "${taskDescription}"
|
|
|
8518
9334
|
console.log(` fh run status \u2014 aktive Sessions anzeigen`);
|
|
8519
9335
|
}
|
|
8520
9336
|
} else if (command === "story") {
|
|
8521
|
-
const storiesDir =
|
|
9337
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8522
9338
|
if (subcommand === "create") {
|
|
8523
9339
|
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
8524
9340
|
const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
|
|
@@ -8529,7 +9345,7 @@ Task: "${taskDescription}"
|
|
|
8529
9345
|
}
|
|
8530
9346
|
const story = createStory(storiesDir, title, epicArg);
|
|
8531
9347
|
if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
|
|
8532
|
-
console.log(`\u2714 ${story.id} erstellt: ${
|
|
9348
|
+
console.log(`\u2714 ${story.id} erstellt: ${path40.join(storiesDir, story.id + ".md")}`);
|
|
8533
9349
|
console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
|
|
8534
9350
|
} else if (subcommand === "list") {
|
|
8535
9351
|
const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
|
|
@@ -8578,8 +9394,8 @@ Task: "${taskDescription}"
|
|
|
8578
9394
|
console.error("Verf\xFCgbar: fh story create | list | show | done");
|
|
8579
9395
|
}
|
|
8580
9396
|
} else if (command === "epic") {
|
|
8581
|
-
const epicsDir =
|
|
8582
|
-
const storiesDir =
|
|
9397
|
+
const epicsDir = path40.join(forgehiveDir, "memory", "epics");
|
|
9398
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8583
9399
|
if (subcommand === "create") {
|
|
8584
9400
|
const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
|
|
8585
9401
|
const prdArg = rest.includes("--prd") ? rest[rest.indexOf("--prd") + 1] : void 0;
|
|
@@ -8597,12 +9413,12 @@ Task: "${taskDescription}"
|
|
|
8597
9413
|
process.exit(1);
|
|
8598
9414
|
}
|
|
8599
9415
|
if (prdArg) {
|
|
8600
|
-
const prdsDir =
|
|
9416
|
+
const prdsDir = path40.join(forgehiveDir, "memory", "prds");
|
|
8601
9417
|
const prd = getPrd(prdsDir, prdArg);
|
|
8602
9418
|
if (!prd) console.warn(`\u26A0 PRD ${prdArg} nicht gefunden \u2014 Epic wird trotzdem erstellt`);
|
|
8603
9419
|
}
|
|
8604
9420
|
const epic = createEpic(epicsDir, title, goalArg, prdArg);
|
|
8605
|
-
console.log(`\u2714 ${epic.id} erstellt: ${
|
|
9421
|
+
console.log(`\u2714 ${epic.id} erstellt: ${path40.join(epicsDir, epic.id + ".md")}`);
|
|
8606
9422
|
if (prdArg) console.log(` Verkn\xFCpft mit: ${prdArg}`);
|
|
8607
9423
|
} else if (subcommand === "list") {
|
|
8608
9424
|
const epics = listEpics(epicsDir);
|
|
@@ -8629,7 +9445,7 @@ Task: "${taskDescription}"
|
|
|
8629
9445
|
console.error("Verf\xFCgbar: fh epic create | list | show");
|
|
8630
9446
|
}
|
|
8631
9447
|
} else if (command === "product") {
|
|
8632
|
-
const prdsDir =
|
|
9448
|
+
const prdsDir = path40.join(forgehiveDir, "memory", "prds");
|
|
8633
9449
|
if (subcommand === "prd") {
|
|
8634
9450
|
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
8635
9451
|
if (!title) {
|
|
@@ -8637,7 +9453,7 @@ Task: "${taskDescription}"
|
|
|
8637
9453
|
process.exit(1);
|
|
8638
9454
|
}
|
|
8639
9455
|
const prd = createPrd(prdsDir, title);
|
|
8640
|
-
const relPath =
|
|
9456
|
+
const relPath = path40.relative(projectRoot, prd.filepath);
|
|
8641
9457
|
console.log(`\u2714 ${prd.id} erstellt: ${relPath}`);
|
|
8642
9458
|
console.log(` Bearbeite die Datei um Anforderungen zu definieren.`);
|
|
8643
9459
|
} else if (subcommand === "list") {
|
|
@@ -8662,10 +9478,10 @@ Task: "${taskDescription}"
|
|
|
8662
9478
|
console.error(`${id} nicht gefunden`);
|
|
8663
9479
|
process.exit(1);
|
|
8664
9480
|
}
|
|
8665
|
-
console.log(
|
|
9481
|
+
console.log(fs39.readFileSync(prd.filepath, "utf8"));
|
|
8666
9482
|
} else if (subcommand === "status") {
|
|
8667
|
-
const epicsDir =
|
|
8668
|
-
const storiesDir =
|
|
9483
|
+
const epicsDir = path40.join(forgehiveDir, "memory", "epics");
|
|
9484
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8669
9485
|
const prds = listPrds(prdsDir);
|
|
8670
9486
|
const epics = listEpics(epicsDir);
|
|
8671
9487
|
const stories = listStories(storiesDir);
|
|
@@ -8708,14 +9524,14 @@ Task: "${taskDescription}"
|
|
|
8708
9524
|
}
|
|
8709
9525
|
} else if (subcommand === "roadmap") {
|
|
8710
9526
|
const content = generateRoadmap(forgehiveDir);
|
|
8711
|
-
const roadmapPath =
|
|
8712
|
-
|
|
9527
|
+
const roadmapPath = path40.join(projectRoot, "ROADMAP.md");
|
|
9528
|
+
fs39.writeFileSync(roadmapPath, content, "utf8");
|
|
8713
9529
|
console.log(`\u2714 ROADMAP.md generiert: ${roadmapPath}`);
|
|
8714
9530
|
} else {
|
|
8715
9531
|
console.error("Verf\xFCgbar: fh product prd | list | show | status | roadmap");
|
|
8716
9532
|
}
|
|
8717
9533
|
} else if (command === "velocity") {
|
|
8718
|
-
const velocityFile =
|
|
9534
|
+
const velocityFile = path40.join(forgehiveDir, "memory", "velocity.md");
|
|
8719
9535
|
if (subcommand === "record") {
|
|
8720
9536
|
const sprintNum = parseInt(rest[0] ?? "0", 10);
|
|
8721
9537
|
const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
|
|
@@ -8734,7 +9550,7 @@ Task: "${taskDescription}"
|
|
|
8734
9550
|
console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
|
|
8735
9551
|
}
|
|
8736
9552
|
} else if (command === "docs") {
|
|
8737
|
-
const docsDir =
|
|
9553
|
+
const docsDir = path40.join(projectRoot, "docs");
|
|
8738
9554
|
if (!subcommand || subcommand === "list") {
|
|
8739
9555
|
const existing = listExistingDocs(projectRoot);
|
|
8740
9556
|
if (existing.length === 0) {
|
|
@@ -8748,29 +9564,29 @@ Task: "${taskDescription}"
|
|
|
8748
9564
|
console.log(" fh docs adr <titel> \u2014 Architecture Decision Record");
|
|
8749
9565
|
} else {
|
|
8750
9566
|
console.log(`Vorhandene Dokumentation (${existing.length} Dateien):`);
|
|
8751
|
-
for (const d of existing) console.log(` ${
|
|
9567
|
+
for (const d of existing) console.log(` ${path40.relative(projectRoot, d)}`);
|
|
8752
9568
|
console.log("");
|
|
8753
9569
|
console.log("Aktualisieren: fh docs user | api | onboard | changelog");
|
|
8754
9570
|
}
|
|
8755
9571
|
} else if (subcommand === "user") {
|
|
8756
|
-
|
|
9572
|
+
fs39.mkdirSync(docsDir, { recursive: true });
|
|
8757
9573
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8758
|
-
const outputPath = outputArg ??
|
|
9574
|
+
const outputPath = outputArg ?? path40.join(docsDir, "user-guide.md");
|
|
8759
9575
|
const guide = generateUserGuide(projectRoot, forgehiveDir);
|
|
8760
|
-
|
|
9576
|
+
fs39.writeFileSync(outputPath, guide, "utf8");
|
|
8761
9577
|
console.log(`\u2714 User Guide geschrieben: ${outputPath}`);
|
|
8762
9578
|
} else if (subcommand === "api") {
|
|
8763
|
-
|
|
9579
|
+
fs39.mkdirSync(docsDir, { recursive: true });
|
|
8764
9580
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8765
|
-
const outputPath = outputArg ??
|
|
9581
|
+
const outputPath = outputArg ?? path40.join(docsDir, "api.md");
|
|
8766
9582
|
const ref = generateApiReference(projectRoot);
|
|
8767
|
-
|
|
9583
|
+
fs39.writeFileSync(outputPath, ref, "utf8");
|
|
8768
9584
|
console.log(`\u2714 API-Referenz geschrieben: ${outputPath}`);
|
|
8769
9585
|
} else if (subcommand === "onboard") {
|
|
8770
9586
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8771
|
-
const outputPath = outputArg ??
|
|
9587
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "ONBOARDING.md");
|
|
8772
9588
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
8773
|
-
|
|
9589
|
+
fs39.writeFileSync(outputPath, doc, "utf8");
|
|
8774
9590
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
8775
9591
|
} else if (subcommand === "changelog") {
|
|
8776
9592
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
@@ -8780,15 +9596,15 @@ Task: "${taskDescription}"
|
|
|
8780
9596
|
const commits = parseGitLog(rawLog);
|
|
8781
9597
|
let pkg = {};
|
|
8782
9598
|
try {
|
|
8783
|
-
pkg = JSON.parse(
|
|
9599
|
+
pkg = JSON.parse(fs39.readFileSync(path40.join(projectRoot, "package.json"), "utf8"));
|
|
8784
9600
|
} catch {
|
|
8785
9601
|
}
|
|
8786
9602
|
const pkgVersion = pkg.version ?? "unreleased";
|
|
8787
9603
|
const md = formatChangelog(commits, pkgVersion);
|
|
8788
|
-
const outputPath = outputArg ??
|
|
9604
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "CHANGELOG.md");
|
|
8789
9605
|
let existing = "";
|
|
8790
|
-
if (
|
|
8791
|
-
|
|
9606
|
+
if (fs39.existsSync(outputPath)) existing = fs39.readFileSync(outputPath, "utf8");
|
|
9607
|
+
fs39.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
8792
9608
|
console.log(`\u2714 CHANGELOG.md aktualisiert (${commits.length} Commits)`);
|
|
8793
9609
|
} else if (subcommand === "adr") {
|
|
8794
9610
|
const title = rest.join(" ");
|
|
@@ -8796,9 +9612,9 @@ Task: "${taskDescription}"
|
|
|
8796
9612
|
console.error("Usage: fh docs adr <titel>");
|
|
8797
9613
|
process.exit(1);
|
|
8798
9614
|
}
|
|
8799
|
-
const adrsDir =
|
|
8800
|
-
|
|
8801
|
-
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;
|
|
8802
9618
|
const adrId = String(existing + 1).padStart(4, "0");
|
|
8803
9619
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8804
9620
|
const filename = `${adrId}-${slug}.md`;
|
|
@@ -8819,13 +9635,13 @@ Task: "${taskDescription}"
|
|
|
8819
9635
|
|
|
8820
9636
|
(Beschreibe die Auswirkungen dieser Entscheidung.)
|
|
8821
9637
|
`;
|
|
8822
|
-
|
|
9638
|
+
fs39.writeFileSync(path40.join(adrsDir, filename), content, "utf8");
|
|
8823
9639
|
console.log(`\u2714 ADR erstellt: .forgehive/memory/adrs/${filename}`);
|
|
8824
9640
|
} else {
|
|
8825
9641
|
console.error("Verf\xFCgbar: fh docs [list|user|api|onboard|changelog|adr <titel>]");
|
|
8826
9642
|
}
|
|
8827
9643
|
} else if (command === "github") {
|
|
8828
|
-
const storiesDir =
|
|
9644
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8829
9645
|
if (subcommand === "setup") {
|
|
8830
9646
|
const readline = await import("node:readline/promises");
|
|
8831
9647
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -8896,8 +9712,8 @@ Task: "${taskDescription}"
|
|
|
8896
9712
|
try {
|
|
8897
9713
|
const [syncOwner, syncRepo] = config.repo.split("/");
|
|
8898
9714
|
const openPrNumbers = await fetchOpenPRs(syncOwner, syncRepo, creds.GITHUB_TOKEN);
|
|
8899
|
-
|
|
8900
|
-
|
|
9715
|
+
fs39.writeFileSync(
|
|
9716
|
+
path40.join(forgehiveDir, "github-pr-cache.json"),
|
|
8901
9717
|
JSON.stringify({ open: openPrNumbers, ts: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
8902
9718
|
"utf8"
|
|
8903
9719
|
);
|
|
@@ -8935,9 +9751,9 @@ Task: "${taskDescription}"
|
|
|
8935
9751
|
process.exit(1);
|
|
8936
9752
|
}
|
|
8937
9753
|
const contextMd = formatPRContext(pr, files, config.repo);
|
|
8938
|
-
|
|
8939
|
-
const outputPath =
|
|
8940
|
-
|
|
9754
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
9755
|
+
const outputPath = path40.join(forgehiveDir, `github-pr-${prNumber}.md`);
|
|
9756
|
+
fs39.writeFileSync(outputPath, contextMd, "utf8");
|
|
8941
9757
|
console.log(` \u2713 PR-Kontext gespeichert: .forgehive/github-pr-${prNumber}.md`);
|
|
8942
9758
|
console.log("");
|
|
8943
9759
|
console.log(` PR context gespeichert. Starte Review: fh party run review`);
|
|
@@ -8945,9 +9761,45 @@ Task: "${taskDescription}"
|
|
|
8945
9761
|
console.log("Verf\xFCgbar: fh github setup | fh github sync | fh github pr <number>");
|
|
8946
9762
|
console.log("Hilfe: fh --help");
|
|
8947
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);
|
|
8948
9800
|
} else {
|
|
8949
9801
|
console.error("Unbekannter Befehl: " + command);
|
|
8950
|
-
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");
|
|
8951
9803
|
console.error("Hilfe: fh --help");
|
|
8952
9804
|
process.exit(1);
|
|
8953
9805
|
}
|