forgehive 0.9.0 → 1.0.1
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 +1253 -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,17 @@ 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
|
-
|
|
8463
|
+
if (subcommand === "--update" || rest.includes("--update")) {
|
|
8464
|
+
console.log(`Meintest du: fh scan --update`);
|
|
8465
|
+
process.exit(1);
|
|
8466
|
+
}
|
|
8467
|
+
const forgehiveDirExists = fs39.existsSync(forgehiveDir);
|
|
7679
8468
|
if (forgehiveDirExists && subcommand !== "--force" && !rest.includes("--force")) {
|
|
7680
8469
|
console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
|
|
7681
8470
|
console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
|
|
@@ -7693,9 +8482,9 @@ if (command === "init") {
|
|
|
7693
8482
|
const block = loadClaudeMdBlock();
|
|
7694
8483
|
writeForgehiveDir(projectRoot, scanResult, capMap, block);
|
|
7695
8484
|
const hash = computeHash(projectRoot);
|
|
7696
|
-
|
|
7697
|
-
const runtimeDir =
|
|
7698
|
-
|
|
8485
|
+
fs39.writeFileSync(path40.join(forgehiveDir, ".scan-hash"), hash, "utf8");
|
|
8486
|
+
const runtimeDir = path40.join(
|
|
8487
|
+
path40.dirname(new URL(import.meta.url).pathname),
|
|
7699
8488
|
"..",
|
|
7700
8489
|
"forgehive"
|
|
7701
8490
|
);
|
|
@@ -7737,7 +8526,7 @@ if (command === "init") {
|
|
|
7737
8526
|
process.exit(1);
|
|
7738
8527
|
}
|
|
7739
8528
|
} else if (command === "memory") {
|
|
7740
|
-
if (!
|
|
8529
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7741
8530
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7742
8531
|
process.exit(1);
|
|
7743
8532
|
}
|
|
@@ -7746,7 +8535,7 @@ if (command === "init") {
|
|
|
7746
8535
|
} else if (subcommand === "clean") {
|
|
7747
8536
|
cleanMemory(forgehiveDir);
|
|
7748
8537
|
} else if (subcommand === "export") {
|
|
7749
|
-
const outputPath = rest[0] ??
|
|
8538
|
+
const outputPath = rest[0] ?? path40.join(projectRoot, "forgehive-memory-export.md");
|
|
7750
8539
|
try {
|
|
7751
8540
|
exportMemory(forgehiveDir, outputPath);
|
|
7752
8541
|
} catch (err) {
|
|
@@ -7794,7 +8583,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7794
8583
|
} else if (subcommand === "snapshot") {
|
|
7795
8584
|
const snapAction = rest[0];
|
|
7796
8585
|
if (snapAction === "export") {
|
|
7797
|
-
const outPath = rest[1] ??
|
|
8586
|
+
const outPath = rest[1] ?? path40.join(
|
|
7798
8587
|
projectRoot,
|
|
7799
8588
|
`forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
|
|
7800
8589
|
);
|
|
@@ -7834,11 +8623,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7834
8623
|
process.exit(1);
|
|
7835
8624
|
}
|
|
7836
8625
|
} else if (command === "scan" && subcommand === "--update") {
|
|
7837
|
-
if (!
|
|
8626
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7838
8627
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7839
8628
|
process.exit(1);
|
|
7840
8629
|
}
|
|
7841
|
-
const savedHash =
|
|
8630
|
+
const savedHash = fs39.existsSync(path40.join(forgehiveDir, ".scan-hash")) ? fs39.readFileSync(path40.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
|
|
7842
8631
|
const currentHash = computeHash(projectRoot);
|
|
7843
8632
|
if (savedHash === currentHash) {
|
|
7844
8633
|
console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
|
|
@@ -7846,7 +8635,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7846
8635
|
}
|
|
7847
8636
|
console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
|
|
7848
8637
|
const oldDoc = jsYaml.load(
|
|
7849
|
-
|
|
8638
|
+
fs39.readFileSync(path40.join(forgehiveDir, "capabilities.yaml"), "utf8")
|
|
7850
8639
|
);
|
|
7851
8640
|
const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
|
|
7852
8641
|
const scanResult = scan(projectRoot);
|
|
@@ -7866,16 +8655,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7866
8655
|
console.log();
|
|
7867
8656
|
const block = loadClaudeMdBlock();
|
|
7868
8657
|
writeForgehiveDir(projectRoot, scanResult, newMap, block);
|
|
7869
|
-
|
|
8658
|
+
fs39.writeFileSync(path40.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
|
|
7870
8659
|
console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
|
|
7871
8660
|
console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
|
|
7872
8661
|
}
|
|
7873
8662
|
} else if (command === "scan" && subcommand === "--check") {
|
|
7874
|
-
if (!
|
|
8663
|
+
if (!fs39.existsSync(path40.join(forgehiveDir, ".scan-hash"))) {
|
|
7875
8664
|
console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
|
|
7876
8665
|
process.exit(1);
|
|
7877
8666
|
}
|
|
7878
|
-
const saved =
|
|
8667
|
+
const saved = fs39.readFileSync(path40.join(forgehiveDir, ".scan-hash"), "utf8").trim();
|
|
7879
8668
|
const current = computeHash(projectRoot);
|
|
7880
8669
|
if (saved !== current) {
|
|
7881
8670
|
console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
|
|
@@ -7884,7 +8673,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7884
8673
|
}
|
|
7885
8674
|
console.log("\u2713 capabilities.yaml ist aktuell");
|
|
7886
8675
|
} else if (command === "skills") {
|
|
7887
|
-
if (!
|
|
8676
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7888
8677
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7889
8678
|
process.exit(1);
|
|
7890
8679
|
}
|
|
@@ -7920,7 +8709,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7920
8709
|
process.exit(1);
|
|
7921
8710
|
}
|
|
7922
8711
|
} else if (command === "party") {
|
|
7923
|
-
if (!
|
|
8712
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
7924
8713
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7925
8714
|
process.exit(1);
|
|
7926
8715
|
}
|
|
@@ -8041,7 +8830,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
8041
8830
|
}
|
|
8042
8831
|
}
|
|
8043
8832
|
} else if (command === "wire") {
|
|
8044
|
-
if (!
|
|
8833
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8045
8834
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8046
8835
|
process.exit(1);
|
|
8047
8836
|
}
|
|
@@ -8073,16 +8862,47 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
8073
8862
|
console.error(`Fehler: ${err.message}`);
|
|
8074
8863
|
process.exit(1);
|
|
8075
8864
|
}
|
|
8865
|
+
} else if (command === "auto") {
|
|
8866
|
+
if (subcommand === "status" || subcommand === "list") {
|
|
8867
|
+
const runs = listRuns(forgehiveDir);
|
|
8868
|
+
if (runs.length === 0) {
|
|
8869
|
+
console.log("Keine Runs gefunden.");
|
|
8870
|
+
} else {
|
|
8871
|
+
runs.slice(0, 10).forEach((r) => console.log(formatRunStatus(r)));
|
|
8872
|
+
}
|
|
8873
|
+
process.exit(0);
|
|
8874
|
+
}
|
|
8875
|
+
if (subcommand === "resume") {
|
|
8876
|
+
const runId = rest[0];
|
|
8877
|
+
if (!runId) {
|
|
8878
|
+
console.error("Usage: fh auto resume <run-id>");
|
|
8879
|
+
process.exit(1);
|
|
8880
|
+
}
|
|
8881
|
+
await resumeRun(runId, forgehiveDir, projectRoot);
|
|
8882
|
+
process.exit(0);
|
|
8883
|
+
}
|
|
8884
|
+
const autoFlag = subcommand === "--auto";
|
|
8885
|
+
const entryPoint = autoFlag ? rest[0] : subcommand;
|
|
8886
|
+
if (!entryPoint) {
|
|
8887
|
+
console.error("Usage: fh auto <US-N|text>");
|
|
8888
|
+
process.exit(1);
|
|
8889
|
+
}
|
|
8890
|
+
await startRun(entryPoint, forgehiveDir, projectRoot, autoFlag);
|
|
8891
|
+
process.exit(0);
|
|
8076
8892
|
} else if (command === "status") {
|
|
8077
8893
|
console.log(projectStatus(projectRoot, forgehiveDir));
|
|
8078
8894
|
const dashboard = formatDashboard(projectRoot, forgehiveDir);
|
|
8079
8895
|
if (dashboard) console.log(dashboard);
|
|
8896
|
+
} else if (command === "next") {
|
|
8897
|
+
const { formatNextFull: formatNextFull2 } = await Promise.resolve().then(() => (init_next(), next_exports));
|
|
8898
|
+
console.log(formatNextFull2(projectRoot, forgehiveDir));
|
|
8899
|
+
process.exit(0);
|
|
8080
8900
|
} else if (command === "cost") {
|
|
8081
8901
|
const allArgs = [subcommand, ...rest].filter((a) => Boolean(a));
|
|
8082
8902
|
const limitIdx = allArgs.indexOf("--limit");
|
|
8083
8903
|
const alertIdx = allArgs.indexOf("--alert");
|
|
8084
8904
|
if (limitIdx !== -1) {
|
|
8085
|
-
if (!
|
|
8905
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8086
8906
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8087
8907
|
process.exit(1);
|
|
8088
8908
|
}
|
|
@@ -8108,24 +8928,24 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
8108
8928
|
}
|
|
8109
8929
|
const sessions = parseCostSessions(projectRoot);
|
|
8110
8930
|
console.log(formatCostReport(sessions, range));
|
|
8111
|
-
if (
|
|
8931
|
+
if (fs39.existsSync(forgehiveDir)) {
|
|
8112
8932
|
const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
|
|
8113
8933
|
const status = checkSpendStatus(forgehiveDir, total);
|
|
8114
8934
|
if (status.message) console.log("\n" + status.message);
|
|
8115
8935
|
}
|
|
8116
8936
|
}
|
|
8117
8937
|
} else if (command === "watch") {
|
|
8118
|
-
if (!
|
|
8938
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8119
8939
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8120
8940
|
process.exit(1);
|
|
8121
8941
|
}
|
|
8122
8942
|
const isSmartMode = subcommand === "--smart" || rest.includes("--smart");
|
|
8123
8943
|
if (isSmartMode) {
|
|
8124
8944
|
let testCmd = "npm test";
|
|
8125
|
-
const capsPath =
|
|
8126
|
-
if (
|
|
8945
|
+
const capsPath = path40.join(forgehiveDir, "capabilities.yaml");
|
|
8946
|
+
if (fs39.existsSync(capsPath)) {
|
|
8127
8947
|
try {
|
|
8128
|
-
const caps = jsYaml.load(
|
|
8948
|
+
const caps = jsYaml.load(fs39.readFileSync(capsPath, "utf8"));
|
|
8129
8949
|
if (caps?.test_command) testCmd = caps.test_command;
|
|
8130
8950
|
} catch {
|
|
8131
8951
|
}
|
|
@@ -8158,7 +8978,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
8158
8978
|
});
|
|
8159
8979
|
}
|
|
8160
8980
|
} else if (command === "mcp") {
|
|
8161
|
-
if (!
|
|
8981
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8162
8982
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8163
8983
|
process.exit(1);
|
|
8164
8984
|
}
|
|
@@ -8269,7 +9089,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8269
9089
|
process.exit(1);
|
|
8270
9090
|
}
|
|
8271
9091
|
} else if (command === "security") {
|
|
8272
|
-
if (!
|
|
9092
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
8273
9093
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
8274
9094
|
process.exit(1);
|
|
8275
9095
|
}
|
|
@@ -8322,8 +9142,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8322
9142
|
high: high.length
|
|
8323
9143
|
}
|
|
8324
9144
|
});
|
|
8325
|
-
|
|
8326
|
-
|
|
9145
|
+
fs39.writeFileSync(
|
|
9146
|
+
path40.join(forgehiveDir, "security-last-scan.txt"),
|
|
8327
9147
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
8328
9148
|
"utf8"
|
|
8329
9149
|
);
|
|
@@ -8360,8 +9180,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8360
9180
|
`);
|
|
8361
9181
|
const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
|
|
8362
9182
|
const text = formatSecurityReport(report);
|
|
8363
|
-
const reportPath =
|
|
8364
|
-
|
|
9183
|
+
const reportPath = path40.join(forgehiveDir, "security-report.md");
|
|
9184
|
+
fs39.writeFileSync(reportPath, text, "utf8");
|
|
8365
9185
|
console.log(text);
|
|
8366
9186
|
console.log(`
|
|
8367
9187
|
\u2713 Report gespeichert: ${reportPath}`);
|
|
@@ -8373,8 +9193,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8373
9193
|
} else if (subcommand === "permissions") {
|
|
8374
9194
|
const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
8375
9195
|
writePermissions2(forgehiveDir);
|
|
8376
|
-
const permPath =
|
|
8377
|
-
console.log(
|
|
9196
|
+
const permPath = path40.join(forgehiveDir, "harness", "permissions.yaml");
|
|
9197
|
+
console.log(fs39.readFileSync(permPath, "utf8"));
|
|
8378
9198
|
} else {
|
|
8379
9199
|
console.error(`Unbekannter security-Subcommand: ${subcommand}`);
|
|
8380
9200
|
console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
|
|
@@ -8386,18 +9206,18 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8386
9206
|
const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
|
|
8387
9207
|
const initFlag = allCiArgs.includes("--init");
|
|
8388
9208
|
if (initFlag) {
|
|
8389
|
-
const ghDir =
|
|
8390
|
-
|
|
8391
|
-
const outPath =
|
|
8392
|
-
|
|
9209
|
+
const ghDir = path40.join(projectRoot, ".github", "workflows");
|
|
9210
|
+
fs39.mkdirSync(ghDir, { recursive: true });
|
|
9211
|
+
const outPath = path40.join(ghDir, "forgehive.yml");
|
|
9212
|
+
fs39.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
|
|
8393
9213
|
console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
|
|
8394
9214
|
} else {
|
|
8395
9215
|
const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
|
|
8396
9216
|
const output = formatCiReport(report, format);
|
|
8397
9217
|
console.log(output);
|
|
8398
9218
|
if (format === "json") {
|
|
8399
|
-
|
|
8400
|
-
|
|
9219
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
9220
|
+
fs39.writeFileSync(path40.join(forgehiveDir, "ci-report.json"), output, "utf8");
|
|
8401
9221
|
}
|
|
8402
9222
|
if (report.status === "fail") process.exit(1);
|
|
8403
9223
|
}
|
|
@@ -8405,29 +9225,29 @@ Setze diese Umgebungsvariablen:`);
|
|
|
8405
9225
|
const semanticFlag = rest.includes("--semantic");
|
|
8406
9226
|
const map2 = generateMap(projectRoot);
|
|
8407
9227
|
const semantic = buildSemanticMap(projectRoot);
|
|
8408
|
-
const mapPath =
|
|
8409
|
-
|
|
9228
|
+
const mapPath = path40.join(forgehiveDir, "map.md");
|
|
9229
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
8410
9230
|
if (semanticFlag) {
|
|
8411
9231
|
const fullMd = formatMap(map2, semantic);
|
|
8412
9232
|
const marker = "\n## Semantic Graph\n";
|
|
8413
9233
|
const markerIdx = fullMd.indexOf(marker);
|
|
8414
9234
|
const semanticMd = markerIdx >= 0 ? "## Semantic Graph\n" + fullMd.slice(markerIdx + marker.length) : fullMd;
|
|
8415
|
-
|
|
9235
|
+
fs39.writeFileSync(mapPath, fullMd, "utf8");
|
|
8416
9236
|
console.log(semanticMd);
|
|
8417
9237
|
console.log(`
|
|
8418
9238
|
\u2714 Codebase-Map gespeichert (Semantic Graph hervorgehoben): ${mapPath}`);
|
|
8419
9239
|
} else {
|
|
8420
9240
|
const md = formatMap(map2, semantic);
|
|
8421
|
-
|
|
9241
|
+
fs39.writeFileSync(mapPath, md, "utf8");
|
|
8422
9242
|
console.log(md);
|
|
8423
9243
|
console.log(`
|
|
8424
9244
|
\u2714 Codebase-Map gespeichert: ${mapPath}`);
|
|
8425
9245
|
}
|
|
8426
9246
|
} else if (command === "onboard") {
|
|
8427
9247
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8428
|
-
const outputPath = outputArg ??
|
|
9248
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "ONBOARDING.md");
|
|
8429
9249
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
8430
|
-
|
|
9250
|
+
fs39.writeFileSync(outputPath, doc, "utf8");
|
|
8431
9251
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
8432
9252
|
} else if (command === "ask") {
|
|
8433
9253
|
const taskArg = subcommand ? [subcommand, ...rest.filter((r) => !r.startsWith("--"))].join(" ") : rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
@@ -8450,28 +9270,28 @@ Task: "${taskArg}"
|
|
|
8450
9270
|
const commits = parseGitLog(rawLog);
|
|
8451
9271
|
let version2 = "unreleased";
|
|
8452
9272
|
try {
|
|
8453
|
-
const pkgPath =
|
|
8454
|
-
if (
|
|
8455
|
-
const pkg = JSON.parse(
|
|
9273
|
+
const pkgPath = path40.join(projectRoot, "package.json");
|
|
9274
|
+
if (fs39.existsSync(pkgPath)) {
|
|
9275
|
+
const pkg = JSON.parse(fs39.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
|
|
8456
9276
|
version2 = pkg.version ?? "unreleased";
|
|
8457
9277
|
}
|
|
8458
9278
|
} catch {
|
|
8459
9279
|
}
|
|
8460
9280
|
const md = formatChangelog(commits, version2);
|
|
8461
|
-
const outputPath = outputArg ??
|
|
9281
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "CHANGELOG.md");
|
|
8462
9282
|
let existing = "";
|
|
8463
|
-
if (
|
|
8464
|
-
|
|
9283
|
+
if (fs39.existsSync(outputPath)) existing = fs39.readFileSync(outputPath, "utf8");
|
|
9284
|
+
fs39.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
8465
9285
|
console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
|
|
8466
9286
|
console.log(` ${commits.length} Commits verarbeitet`);
|
|
8467
9287
|
} else if (command === "metrics") {
|
|
8468
9288
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
|
|
8469
9289
|
const rawLog = getMetricsGitLog(projectRoot, sinceArg);
|
|
8470
9290
|
const stats = parseCommitStats(rawLog);
|
|
8471
|
-
const md = formatMetrics(stats,
|
|
8472
|
-
const metricsPath =
|
|
8473
|
-
|
|
8474
|
-
|
|
9291
|
+
const md = formatMetrics(stats, path40.basename(projectRoot));
|
|
9292
|
+
const metricsPath = path40.join(forgehiveDir, "metrics.md");
|
|
9293
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
9294
|
+
fs39.writeFileSync(metricsPath, md, "utf8");
|
|
8475
9295
|
console.log(md);
|
|
8476
9296
|
console.log(`
|
|
8477
9297
|
\u2714 Metrics gespeichert: ${metricsPath}`);
|
|
@@ -8518,7 +9338,7 @@ Task: "${taskDescription}"
|
|
|
8518
9338
|
console.log(` fh run status \u2014 aktive Sessions anzeigen`);
|
|
8519
9339
|
}
|
|
8520
9340
|
} else if (command === "story") {
|
|
8521
|
-
const storiesDir =
|
|
9341
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8522
9342
|
if (subcommand === "create") {
|
|
8523
9343
|
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
8524
9344
|
const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
|
|
@@ -8529,7 +9349,7 @@ Task: "${taskDescription}"
|
|
|
8529
9349
|
}
|
|
8530
9350
|
const story = createStory(storiesDir, title, epicArg);
|
|
8531
9351
|
if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
|
|
8532
|
-
console.log(`\u2714 ${story.id} erstellt: ${
|
|
9352
|
+
console.log(`\u2714 ${story.id} erstellt: ${path40.join(storiesDir, story.id + ".md")}`);
|
|
8533
9353
|
console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
|
|
8534
9354
|
} else if (subcommand === "list") {
|
|
8535
9355
|
const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
|
|
@@ -8578,8 +9398,8 @@ Task: "${taskDescription}"
|
|
|
8578
9398
|
console.error("Verf\xFCgbar: fh story create | list | show | done");
|
|
8579
9399
|
}
|
|
8580
9400
|
} else if (command === "epic") {
|
|
8581
|
-
const epicsDir =
|
|
8582
|
-
const storiesDir =
|
|
9401
|
+
const epicsDir = path40.join(forgehiveDir, "memory", "epics");
|
|
9402
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8583
9403
|
if (subcommand === "create") {
|
|
8584
9404
|
const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
|
|
8585
9405
|
const prdArg = rest.includes("--prd") ? rest[rest.indexOf("--prd") + 1] : void 0;
|
|
@@ -8597,12 +9417,12 @@ Task: "${taskDescription}"
|
|
|
8597
9417
|
process.exit(1);
|
|
8598
9418
|
}
|
|
8599
9419
|
if (prdArg) {
|
|
8600
|
-
const prdsDir =
|
|
9420
|
+
const prdsDir = path40.join(forgehiveDir, "memory", "prds");
|
|
8601
9421
|
const prd = getPrd(prdsDir, prdArg);
|
|
8602
9422
|
if (!prd) console.warn(`\u26A0 PRD ${prdArg} nicht gefunden \u2014 Epic wird trotzdem erstellt`);
|
|
8603
9423
|
}
|
|
8604
9424
|
const epic = createEpic(epicsDir, title, goalArg, prdArg);
|
|
8605
|
-
console.log(`\u2714 ${epic.id} erstellt: ${
|
|
9425
|
+
console.log(`\u2714 ${epic.id} erstellt: ${path40.join(epicsDir, epic.id + ".md")}`);
|
|
8606
9426
|
if (prdArg) console.log(` Verkn\xFCpft mit: ${prdArg}`);
|
|
8607
9427
|
} else if (subcommand === "list") {
|
|
8608
9428
|
const epics = listEpics(epicsDir);
|
|
@@ -8629,7 +9449,7 @@ Task: "${taskDescription}"
|
|
|
8629
9449
|
console.error("Verf\xFCgbar: fh epic create | list | show");
|
|
8630
9450
|
}
|
|
8631
9451
|
} else if (command === "product") {
|
|
8632
|
-
const prdsDir =
|
|
9452
|
+
const prdsDir = path40.join(forgehiveDir, "memory", "prds");
|
|
8633
9453
|
if (subcommand === "prd") {
|
|
8634
9454
|
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
8635
9455
|
if (!title) {
|
|
@@ -8637,7 +9457,7 @@ Task: "${taskDescription}"
|
|
|
8637
9457
|
process.exit(1);
|
|
8638
9458
|
}
|
|
8639
9459
|
const prd = createPrd(prdsDir, title);
|
|
8640
|
-
const relPath =
|
|
9460
|
+
const relPath = path40.relative(projectRoot, prd.filepath);
|
|
8641
9461
|
console.log(`\u2714 ${prd.id} erstellt: ${relPath}`);
|
|
8642
9462
|
console.log(` Bearbeite die Datei um Anforderungen zu definieren.`);
|
|
8643
9463
|
} else if (subcommand === "list") {
|
|
@@ -8662,10 +9482,10 @@ Task: "${taskDescription}"
|
|
|
8662
9482
|
console.error(`${id} nicht gefunden`);
|
|
8663
9483
|
process.exit(1);
|
|
8664
9484
|
}
|
|
8665
|
-
console.log(
|
|
9485
|
+
console.log(fs39.readFileSync(prd.filepath, "utf8"));
|
|
8666
9486
|
} else if (subcommand === "status") {
|
|
8667
|
-
const epicsDir =
|
|
8668
|
-
const storiesDir =
|
|
9487
|
+
const epicsDir = path40.join(forgehiveDir, "memory", "epics");
|
|
9488
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8669
9489
|
const prds = listPrds(prdsDir);
|
|
8670
9490
|
const epics = listEpics(epicsDir);
|
|
8671
9491
|
const stories = listStories(storiesDir);
|
|
@@ -8708,14 +9528,14 @@ Task: "${taskDescription}"
|
|
|
8708
9528
|
}
|
|
8709
9529
|
} else if (subcommand === "roadmap") {
|
|
8710
9530
|
const content = generateRoadmap(forgehiveDir);
|
|
8711
|
-
const roadmapPath =
|
|
8712
|
-
|
|
9531
|
+
const roadmapPath = path40.join(projectRoot, "ROADMAP.md");
|
|
9532
|
+
fs39.writeFileSync(roadmapPath, content, "utf8");
|
|
8713
9533
|
console.log(`\u2714 ROADMAP.md generiert: ${roadmapPath}`);
|
|
8714
9534
|
} else {
|
|
8715
9535
|
console.error("Verf\xFCgbar: fh product prd | list | show | status | roadmap");
|
|
8716
9536
|
}
|
|
8717
9537
|
} else if (command === "velocity") {
|
|
8718
|
-
const velocityFile =
|
|
9538
|
+
const velocityFile = path40.join(forgehiveDir, "memory", "velocity.md");
|
|
8719
9539
|
if (subcommand === "record") {
|
|
8720
9540
|
const sprintNum = parseInt(rest[0] ?? "0", 10);
|
|
8721
9541
|
const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
|
|
@@ -8734,7 +9554,7 @@ Task: "${taskDescription}"
|
|
|
8734
9554
|
console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
|
|
8735
9555
|
}
|
|
8736
9556
|
} else if (command === "docs") {
|
|
8737
|
-
const docsDir =
|
|
9557
|
+
const docsDir = path40.join(projectRoot, "docs");
|
|
8738
9558
|
if (!subcommand || subcommand === "list") {
|
|
8739
9559
|
const existing = listExistingDocs(projectRoot);
|
|
8740
9560
|
if (existing.length === 0) {
|
|
@@ -8748,29 +9568,29 @@ Task: "${taskDescription}"
|
|
|
8748
9568
|
console.log(" fh docs adr <titel> \u2014 Architecture Decision Record");
|
|
8749
9569
|
} else {
|
|
8750
9570
|
console.log(`Vorhandene Dokumentation (${existing.length} Dateien):`);
|
|
8751
|
-
for (const d of existing) console.log(` ${
|
|
9571
|
+
for (const d of existing) console.log(` ${path40.relative(projectRoot, d)}`);
|
|
8752
9572
|
console.log("");
|
|
8753
9573
|
console.log("Aktualisieren: fh docs user | api | onboard | changelog");
|
|
8754
9574
|
}
|
|
8755
9575
|
} else if (subcommand === "user") {
|
|
8756
|
-
|
|
9576
|
+
fs39.mkdirSync(docsDir, { recursive: true });
|
|
8757
9577
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8758
|
-
const outputPath = outputArg ??
|
|
9578
|
+
const outputPath = outputArg ?? path40.join(docsDir, "user-guide.md");
|
|
8759
9579
|
const guide = generateUserGuide(projectRoot, forgehiveDir);
|
|
8760
|
-
|
|
9580
|
+
fs39.writeFileSync(outputPath, guide, "utf8");
|
|
8761
9581
|
console.log(`\u2714 User Guide geschrieben: ${outputPath}`);
|
|
8762
9582
|
} else if (subcommand === "api") {
|
|
8763
|
-
|
|
9583
|
+
fs39.mkdirSync(docsDir, { recursive: true });
|
|
8764
9584
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8765
|
-
const outputPath = outputArg ??
|
|
9585
|
+
const outputPath = outputArg ?? path40.join(docsDir, "api.md");
|
|
8766
9586
|
const ref = generateApiReference(projectRoot);
|
|
8767
|
-
|
|
9587
|
+
fs39.writeFileSync(outputPath, ref, "utf8");
|
|
8768
9588
|
console.log(`\u2714 API-Referenz geschrieben: ${outputPath}`);
|
|
8769
9589
|
} else if (subcommand === "onboard") {
|
|
8770
9590
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
8771
|
-
const outputPath = outputArg ??
|
|
9591
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "ONBOARDING.md");
|
|
8772
9592
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
8773
|
-
|
|
9593
|
+
fs39.writeFileSync(outputPath, doc, "utf8");
|
|
8774
9594
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
8775
9595
|
} else if (subcommand === "changelog") {
|
|
8776
9596
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
@@ -8780,15 +9600,15 @@ Task: "${taskDescription}"
|
|
|
8780
9600
|
const commits = parseGitLog(rawLog);
|
|
8781
9601
|
let pkg = {};
|
|
8782
9602
|
try {
|
|
8783
|
-
pkg = JSON.parse(
|
|
9603
|
+
pkg = JSON.parse(fs39.readFileSync(path40.join(projectRoot, "package.json"), "utf8"));
|
|
8784
9604
|
} catch {
|
|
8785
9605
|
}
|
|
8786
9606
|
const pkgVersion = pkg.version ?? "unreleased";
|
|
8787
9607
|
const md = formatChangelog(commits, pkgVersion);
|
|
8788
|
-
const outputPath = outputArg ??
|
|
9608
|
+
const outputPath = outputArg ?? path40.join(projectRoot, "CHANGELOG.md");
|
|
8789
9609
|
let existing = "";
|
|
8790
|
-
if (
|
|
8791
|
-
|
|
9610
|
+
if (fs39.existsSync(outputPath)) existing = fs39.readFileSync(outputPath, "utf8");
|
|
9611
|
+
fs39.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
8792
9612
|
console.log(`\u2714 CHANGELOG.md aktualisiert (${commits.length} Commits)`);
|
|
8793
9613
|
} else if (subcommand === "adr") {
|
|
8794
9614
|
const title = rest.join(" ");
|
|
@@ -8796,9 +9616,9 @@ Task: "${taskDescription}"
|
|
|
8796
9616
|
console.error("Usage: fh docs adr <titel>");
|
|
8797
9617
|
process.exit(1);
|
|
8798
9618
|
}
|
|
8799
|
-
const adrsDir =
|
|
8800
|
-
|
|
8801
|
-
const existing =
|
|
9619
|
+
const adrsDir = path40.join(forgehiveDir, "memory", "adrs");
|
|
9620
|
+
fs39.mkdirSync(adrsDir, { recursive: true });
|
|
9621
|
+
const existing = fs39.readdirSync(adrsDir).filter((f) => f.endsWith(".md")).length;
|
|
8802
9622
|
const adrId = String(existing + 1).padStart(4, "0");
|
|
8803
9623
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8804
9624
|
const filename = `${adrId}-${slug}.md`;
|
|
@@ -8819,13 +9639,13 @@ Task: "${taskDescription}"
|
|
|
8819
9639
|
|
|
8820
9640
|
(Beschreibe die Auswirkungen dieser Entscheidung.)
|
|
8821
9641
|
`;
|
|
8822
|
-
|
|
9642
|
+
fs39.writeFileSync(path40.join(adrsDir, filename), content, "utf8");
|
|
8823
9643
|
console.log(`\u2714 ADR erstellt: .forgehive/memory/adrs/${filename}`);
|
|
8824
9644
|
} else {
|
|
8825
9645
|
console.error("Verf\xFCgbar: fh docs [list|user|api|onboard|changelog|adr <titel>]");
|
|
8826
9646
|
}
|
|
8827
9647
|
} else if (command === "github") {
|
|
8828
|
-
const storiesDir =
|
|
9648
|
+
const storiesDir = path40.join(forgehiveDir, "memory", "stories");
|
|
8829
9649
|
if (subcommand === "setup") {
|
|
8830
9650
|
const readline = await import("node:readline/promises");
|
|
8831
9651
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -8896,8 +9716,8 @@ Task: "${taskDescription}"
|
|
|
8896
9716
|
try {
|
|
8897
9717
|
const [syncOwner, syncRepo] = config.repo.split("/");
|
|
8898
9718
|
const openPrNumbers = await fetchOpenPRs(syncOwner, syncRepo, creds.GITHUB_TOKEN);
|
|
8899
|
-
|
|
8900
|
-
|
|
9719
|
+
fs39.writeFileSync(
|
|
9720
|
+
path40.join(forgehiveDir, "github-pr-cache.json"),
|
|
8901
9721
|
JSON.stringify({ open: openPrNumbers, ts: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
8902
9722
|
"utf8"
|
|
8903
9723
|
);
|
|
@@ -8935,9 +9755,9 @@ Task: "${taskDescription}"
|
|
|
8935
9755
|
process.exit(1);
|
|
8936
9756
|
}
|
|
8937
9757
|
const contextMd = formatPRContext(pr, files, config.repo);
|
|
8938
|
-
|
|
8939
|
-
const outputPath =
|
|
8940
|
-
|
|
9758
|
+
fs39.mkdirSync(forgehiveDir, { recursive: true });
|
|
9759
|
+
const outputPath = path40.join(forgehiveDir, `github-pr-${prNumber}.md`);
|
|
9760
|
+
fs39.writeFileSync(outputPath, contextMd, "utf8");
|
|
8941
9761
|
console.log(` \u2713 PR-Kontext gespeichert: .forgehive/github-pr-${prNumber}.md`);
|
|
8942
9762
|
console.log("");
|
|
8943
9763
|
console.log(` PR context gespeichert. Starte Review: fh party run review`);
|
|
@@ -8945,9 +9765,45 @@ Task: "${taskDescription}"
|
|
|
8945
9765
|
console.log("Verf\xFCgbar: fh github setup | fh github sync | fh github pr <number>");
|
|
8946
9766
|
console.log("Hilfe: fh --help");
|
|
8947
9767
|
}
|
|
9768
|
+
} else if (command === "outcomes") {
|
|
9769
|
+
if (!fs39.existsSync(forgehiveDir)) {
|
|
9770
|
+
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
9771
|
+
process.exit(1);
|
|
9772
|
+
}
|
|
9773
|
+
const { aggregateByAgent: aggregateByAgent2, aggregateByTaskType: aggregateByTaskType2, readOutcomes: readOutcomes2 } = await Promise.resolve().then(() => (init_outcomes(), outcomes_exports));
|
|
9774
|
+
const entries = readOutcomes2(forgehiveDir);
|
|
9775
|
+
if (entries.length === 0) {
|
|
9776
|
+
console.log("Keine Outcome-Daten vorhanden. Starte 'fh auto' oder 'fh party run' um Daten zu sammeln.");
|
|
9777
|
+
process.exit(0);
|
|
9778
|
+
}
|
|
9779
|
+
const byAgent = aggregateByAgent2(forgehiveDir);
|
|
9780
|
+
const byType = aggregateByTaskType2(forgehiveDir);
|
|
9781
|
+
console.log(`
|
|
9782
|
+
Outcome-\xDCbersicht (${entries.length} Runs)
|
|
9783
|
+
`);
|
|
9784
|
+
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");
|
|
9785
|
+
for (const [agent, stats] of Object.entries(byAgent)) {
|
|
9786
|
+
const avgMin = (stats.avgDurationMs / 6e4).toFixed(1);
|
|
9787
|
+
const rating = stats.avgRating !== null ? ` \u2605 ${stats.avgRating.toFixed(1)}` : "";
|
|
9788
|
+
const types2 = Object.keys(stats.taskTypes).join(", ");
|
|
9789
|
+
console.log(` ${agent.padEnd(8)} \xD8 ${avgMin} min [${types2}]${rating}`);
|
|
9790
|
+
}
|
|
9791
|
+
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");
|
|
9792
|
+
for (const [type2, agg] of Object.entries(byType)) {
|
|
9793
|
+
console.log(` ${type2.padEnd(16)} ${agg.count}x`);
|
|
9794
|
+
}
|
|
9795
|
+
const last = entries.slice(-5).reverse();
|
|
9796
|
+
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");
|
|
9797
|
+
for (const e of last) {
|
|
9798
|
+
const mins = (e.durationMs / 6e4).toFixed(1);
|
|
9799
|
+
const rating = e.rating !== null ? ` \u2605 ${e.rating}` : "";
|
|
9800
|
+
const agents = e.agents.join("+");
|
|
9801
|
+
console.log(` ${e.runId} ${e.taskType} ${agents} ${mins} min${rating}`);
|
|
9802
|
+
}
|
|
9803
|
+
process.exit(0);
|
|
8948
9804
|
} else {
|
|
8949
9805
|
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");
|
|
9806
|
+
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
9807
|
console.error("Hilfe: fh --help");
|
|
8952
9808
|
process.exit(1);
|
|
8953
9809
|
}
|