fifony 0.1.42 → 0.1.43
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/app/dist/assets/{CommandPalette-DNR5umI1.js → CommandPalette-M4VAMxCU.js} +1 -1
- package/app/dist/assets/{KeyboardShortcutsHelp-Dpl19F20.js → KeyboardShortcutsHelp-DkvPUXQq.js} +1 -1
- package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +1 -0
- package/app/dist/assets/analytics.lazy-zVJdF880.js +1 -0
- package/app/dist/assets/{api-ChEctgc5.js → api-CkVfYg_m.js} +1 -1
- package/app/dist/assets/{createLucideIcon-R47sXufx.js → createLucideIcon-Dfk_Hxud.js} +1 -1
- package/app/dist/assets/index-BpiCi7Ew.css +1 -0
- package/app/dist/assets/index-D2INW0zc.js +47 -0
- package/app/dist/assets/vendor-BEoYbFV1.js +9 -0
- package/app/dist/index.html +5 -5
- package/app/dist/service-worker.js +9 -4
- package/bin/fifony.js +3 -0
- package/dist/agent/pty-daemon.js +177 -0
- package/dist/agent/run-local.js +177 -43
- package/dist/{agent-NNGZEKZH.js → agent-RMQTTUEC.js} +37 -16
- package/dist/analytics-broadcaster-O6YBP66L.js +145 -0
- package/dist/chunk-3NE23NYW.js +82 -0
- package/dist/chunk-42AMQAJG.js +404 -0
- package/dist/{chunk-H5N7O5NP.js → chunk-AILXZ2TD.js} +79 -147
- package/dist/{chunk-I2UHVKHS.js → chunk-BRSR26VK.js} +2 -2
- package/dist/chunk-E2EWEYA4.js +1302 -0
- package/dist/chunk-ESWHDHH6.js +102 -0
- package/dist/{chunk-NB44PCD2.js → chunk-FJNH3G2Z.js} +1061 -1138
- package/dist/chunk-MVTGAKQK.js +493 -0
- package/dist/chunk-QQQLP3PL.js +155 -0
- package/dist/chunk-SOBLO4YZ.js +2016 -0
- package/dist/chunk-YRSH2CLW.js +13784 -0
- package/dist/cli.js +335 -44
- package/dist/{issue-state-machine-GPQNZYUZ.js → fsm-issue-YGGF7SIL.js} +9 -5
- package/dist/helpers-L7NYO5XS.js +53 -0
- package/dist/issue-log-broadcaster-WZAHISYB.js +84 -0
- package/dist/{issues-MZLRSXD6.js → issues-3QRR7KM6.js} +10 -8
- package/dist/log-analyzer-K7MXQB4T.js +287 -0
- package/dist/mcp/server.js +109 -137
- package/dist/parallel-executor-6INE6NDO.js +118 -0
- package/dist/pid-manager-UBWXVSMD.js +21 -0
- package/dist/queue-workers-XFZK3TT5.js +32 -0
- package/dist/replan-issue.command-4UCWYHGZ.js +15 -0
- package/dist/scheduler-ZP7GOZDW.js +26 -0
- package/dist/{settings-NGY33WQE.js → settings-ZAWDCFP2.js} +32 -8
- package/dist/settings.resource-5CW456AZ.js +24 -0
- package/dist/store-M6NCKMZY.js +97 -0
- package/dist/{web-push-CRVDJKWR.js → web-push-AX5IIK3P.js} +2 -2
- package/dist/{workspace-D3F3XGSI.js → workspace-CJTWFWTJ.js} +5 -4
- package/package.json +8 -7
- package/app/dist/assets/OnboardingWizard-CijMhJDW.js +0 -1
- package/app/dist/assets/analytics.lazy-Dq90a756.js +0 -1
- package/app/dist/assets/index-Dy_fM427.js +0 -54
- package/app/dist/assets/index-Q9jBP0Pz.css +0 -1
- package/app/dist/assets/vendor-DkWeBvNl.js +0 -9
- package/dist/chunk-2CVTK5F2.js +0 -288
- package/dist/chunk-37N5OFHM.js +0 -125
- package/dist/chunk-JTKUWIQD.js +0 -8406
- package/dist/chunk-RBDBGU2C.js +0 -303
- package/dist/issue-runner-CMZPSVC7.js +0 -16
- package/dist/queue-workers-XZ6DGH4W.js +0 -23
- package/dist/scheduler-NVE6L3P7.js +0 -22
- package/dist/store-4HCGBN4L.js +0 -65
|
@@ -0,0 +1,1302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeMetrics
|
|
3
|
+
} from "./chunk-MVTGAKQK.js";
|
|
4
|
+
import {
|
|
5
|
+
markEventDirty,
|
|
6
|
+
markIssueDirty,
|
|
7
|
+
normalizeAgentProvider
|
|
8
|
+
} from "./chunk-FJNH3G2Z.js";
|
|
9
|
+
import {
|
|
10
|
+
logger
|
|
11
|
+
} from "./chunk-DVU3CXWA.js";
|
|
12
|
+
import {
|
|
13
|
+
COMPLETED_STATES,
|
|
14
|
+
PERSIST_EVENTS_MAX,
|
|
15
|
+
TARGET_ROOT,
|
|
16
|
+
TERMINAL_STATES,
|
|
17
|
+
clamp,
|
|
18
|
+
fail,
|
|
19
|
+
isoWeek,
|
|
20
|
+
normalizeState,
|
|
21
|
+
now,
|
|
22
|
+
parseEnvNumber,
|
|
23
|
+
parseIntArg,
|
|
24
|
+
parseIssueState,
|
|
25
|
+
parsePositiveIntEnv,
|
|
26
|
+
toBooleanValue,
|
|
27
|
+
toNumberValue,
|
|
28
|
+
toStringArray,
|
|
29
|
+
toStringValue,
|
|
30
|
+
withRetryBackoff
|
|
31
|
+
} from "./chunk-42AMQAJG.js";
|
|
32
|
+
|
|
33
|
+
// src/domains/tokens.ts
|
|
34
|
+
var EMPTY = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
35
|
+
var overall = { ...EMPTY };
|
|
36
|
+
var byPhase = /* @__PURE__ */ new Map();
|
|
37
|
+
var byModel = /* @__PURE__ */ new Map();
|
|
38
|
+
var daily = /* @__PURE__ */ new Map();
|
|
39
|
+
var dailyByPhase = /* @__PURE__ */ new Map();
|
|
40
|
+
var dailyByModel = /* @__PURE__ */ new Map();
|
|
41
|
+
var byIssue = /* @__PURE__ */ new Map();
|
|
42
|
+
var dailyEvents = /* @__PURE__ */ new Map();
|
|
43
|
+
var hourly = /* @__PURE__ */ new Map();
|
|
44
|
+
var HOURLY_RETENTION = 48;
|
|
45
|
+
function todayDate() {
|
|
46
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
47
|
+
}
|
|
48
|
+
function currentHour() {
|
|
49
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 13);
|
|
50
|
+
}
|
|
51
|
+
function pruneOldHours() {
|
|
52
|
+
if (hourly.size <= HOURLY_RETENTION) return;
|
|
53
|
+
const cutoff = new Date(Date.now() - HOURLY_RETENTION * 36e5).toISOString().slice(0, 13);
|
|
54
|
+
for (const key of hourly.keys()) {
|
|
55
|
+
if (key < cutoff) hourly.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function addTo(target, usage) {
|
|
59
|
+
target.inputTokens += usage.inputTokens;
|
|
60
|
+
target.outputTokens += usage.outputTokens;
|
|
61
|
+
target.totalTokens += usage.totalTokens;
|
|
62
|
+
}
|
|
63
|
+
function getOrCreate(map, key) {
|
|
64
|
+
let bucket = map.get(key);
|
|
65
|
+
if (!bucket) {
|
|
66
|
+
bucket = { ...EMPTY };
|
|
67
|
+
map.set(key, bucket);
|
|
68
|
+
}
|
|
69
|
+
return bucket;
|
|
70
|
+
}
|
|
71
|
+
function mapToDailyArray(map, prefix) {
|
|
72
|
+
const result = [];
|
|
73
|
+
for (const [key, bucket] of map) {
|
|
74
|
+
if (!key.startsWith(prefix)) continue;
|
|
75
|
+
const date = key.slice(prefix.length);
|
|
76
|
+
result.push({ ...bucket, date });
|
|
77
|
+
}
|
|
78
|
+
result.sort((a, b) => a.date.localeCompare(b.date));
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function record(issue, usage, role) {
|
|
82
|
+
if (!usage || usage.totalTokens === 0) return;
|
|
83
|
+
const date = todayDate();
|
|
84
|
+
const hour = currentHour();
|
|
85
|
+
const model = usage.model || "unknown";
|
|
86
|
+
addTo(overall, usage);
|
|
87
|
+
addTo(getOrCreate(daily, date), usage);
|
|
88
|
+
addTo(getOrCreate(hourly, hour), usage);
|
|
89
|
+
pruneOldHours();
|
|
90
|
+
if (role) {
|
|
91
|
+
addTo(getOrCreate(byPhase, role), usage);
|
|
92
|
+
addTo(getOrCreate(dailyByPhase, `${role}:${date}`), usage);
|
|
93
|
+
}
|
|
94
|
+
addTo(getOrCreate(byModel, model), usage);
|
|
95
|
+
addTo(getOrCreate(dailyByModel, `${model}:${date}`), usage);
|
|
96
|
+
const prev = byIssue.get(issue.id);
|
|
97
|
+
if (prev) {
|
|
98
|
+
prev.inputTokens += usage.inputTokens;
|
|
99
|
+
prev.outputTokens += usage.outputTokens;
|
|
100
|
+
prev.totalTokens += usage.totalTokens;
|
|
101
|
+
if (typeof usage.costUsd === "number") {
|
|
102
|
+
prev.costUsd = (prev.costUsd || 0) + usage.costUsd;
|
|
103
|
+
}
|
|
104
|
+
prev.title = issue.title;
|
|
105
|
+
} else {
|
|
106
|
+
byIssue.set(issue.id, {
|
|
107
|
+
identifier: issue.identifier,
|
|
108
|
+
title: issue.title,
|
|
109
|
+
inputTokens: usage.inputTokens,
|
|
110
|
+
outputTokens: usage.outputTokens,
|
|
111
|
+
totalTokens: usage.totalTokens,
|
|
112
|
+
costUsd: usage.costUsd,
|
|
113
|
+
byPhase: {}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (role) {
|
|
117
|
+
const summary = byIssue.get(issue.id);
|
|
118
|
+
if (summary) {
|
|
119
|
+
const phaseBucket = summary.byPhase[role] ?? { ...EMPTY };
|
|
120
|
+
addTo(phaseBucket, usage);
|
|
121
|
+
summary.byPhase[role] = phaseBucket;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function recordEvent() {
|
|
126
|
+
const date = todayDate();
|
|
127
|
+
dailyEvents.set(date, (dailyEvents.get(date) || 0) + 1);
|
|
128
|
+
}
|
|
129
|
+
function getHourlySnapshot(hours = 24) {
|
|
130
|
+
const now2 = Date.now();
|
|
131
|
+
const tokensPerHour = [];
|
|
132
|
+
for (let i = hours - 1; i >= 0; i--) {
|
|
133
|
+
const h = new Date(now2 - i * 36e5).toISOString().slice(0, 13);
|
|
134
|
+
const tokenBucket = hourly.get(h);
|
|
135
|
+
tokensPerHour.push({
|
|
136
|
+
hour: h,
|
|
137
|
+
inputTokens: tokenBucket?.inputTokens || 0,
|
|
138
|
+
outputTokens: tokenBucket?.outputTokens || 0,
|
|
139
|
+
totalTokens: tokenBucket?.totalTokens || 0
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return { tokensPerHour };
|
|
143
|
+
}
|
|
144
|
+
function hydrate(issues) {
|
|
145
|
+
overall = { ...EMPTY };
|
|
146
|
+
byPhase.clear();
|
|
147
|
+
byModel.clear();
|
|
148
|
+
daily.clear();
|
|
149
|
+
dailyByPhase.clear();
|
|
150
|
+
dailyByModel.clear();
|
|
151
|
+
byIssue.clear();
|
|
152
|
+
dailyEvents.clear();
|
|
153
|
+
for (const issue of issues) {
|
|
154
|
+
const issueSummary = {
|
|
155
|
+
identifier: issue.identifier,
|
|
156
|
+
title: issue.title,
|
|
157
|
+
inputTokens: 0,
|
|
158
|
+
outputTokens: 0,
|
|
159
|
+
totalTokens: 0,
|
|
160
|
+
costUsd: 0,
|
|
161
|
+
byPhase: {}
|
|
162
|
+
};
|
|
163
|
+
if (issue.tokenUsage && issue.tokenUsage.totalTokens > 0) {
|
|
164
|
+
issueSummary.inputTokens = issue.tokenUsage.inputTokens;
|
|
165
|
+
issueSummary.outputTokens = issue.tokenUsage.outputTokens;
|
|
166
|
+
issueSummary.totalTokens = issue.tokenUsage.totalTokens;
|
|
167
|
+
issueSummary.costUsd = issue.tokenUsage.costUsd || 0;
|
|
168
|
+
addTo(overall, issue.tokenUsage);
|
|
169
|
+
}
|
|
170
|
+
if (issue.tokensByPhase) {
|
|
171
|
+
for (const [phase, pu] of Object.entries(issue.tokensByPhase)) {
|
|
172
|
+
if (pu.totalTokens > 0) {
|
|
173
|
+
addTo(getOrCreate(byPhase, phase), pu);
|
|
174
|
+
issueSummary.byPhase[phase] = {
|
|
175
|
+
inputTokens: pu.inputTokens,
|
|
176
|
+
outputTokens: pu.outputTokens,
|
|
177
|
+
totalTokens: pu.totalTokens
|
|
178
|
+
};
|
|
179
|
+
if (issueSummary.totalTokens === 0) {
|
|
180
|
+
issueSummary.inputTokens += pu.inputTokens;
|
|
181
|
+
issueSummary.outputTokens += pu.outputTokens;
|
|
182
|
+
issueSummary.totalTokens += pu.totalTokens;
|
|
183
|
+
issueSummary.costUsd += pu.costUsd || 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (issueSummary.totalTokens > 0) {
|
|
189
|
+
byIssue.set(issue.id, {
|
|
190
|
+
identifier: issueSummary.identifier,
|
|
191
|
+
title: issueSummary.title,
|
|
192
|
+
inputTokens: issueSummary.inputTokens,
|
|
193
|
+
outputTokens: issueSummary.outputTokens,
|
|
194
|
+
totalTokens: issueSummary.totalTokens,
|
|
195
|
+
costUsd: issueSummary.costUsd || void 0,
|
|
196
|
+
byPhase: issueSummary.byPhase
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (issue.tokensByModel) {
|
|
200
|
+
for (const [model, mu] of Object.entries(issue.tokensByModel)) {
|
|
201
|
+
if (mu.totalTokens > 0) addTo(getOrCreate(byModel, model), mu);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const date = issue.completedAt?.slice(0, 10);
|
|
205
|
+
if (date && issue.tokenUsage && issue.tokenUsage.totalTokens > 0) {
|
|
206
|
+
addTo(getOrCreate(daily, date), issue.tokenUsage);
|
|
207
|
+
}
|
|
208
|
+
if (date && issue.tokensByPhase) {
|
|
209
|
+
for (const [phase, pu] of Object.entries(issue.tokensByPhase)) {
|
|
210
|
+
if (pu.totalTokens > 0) addTo(getOrCreate(dailyByPhase, `${phase}:${date}`), pu);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (date && issue.tokensByModel) {
|
|
214
|
+
for (const [model, mu] of Object.entries(issue.tokensByModel)) {
|
|
215
|
+
if (mu.totalTokens > 0) addTo(getOrCreate(dailyByModel, `${model}:${date}`), mu);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function getAnalytics(topN = 20) {
|
|
221
|
+
const allDates = /* @__PURE__ */ new Set([...daily.keys(), ...dailyEvents.keys()]);
|
|
222
|
+
const dailyArray = [];
|
|
223
|
+
for (const date of allDates) {
|
|
224
|
+
const bucket = daily.get(date) ?? { ...EMPTY };
|
|
225
|
+
const events = dailyEvents.get(date) || 0;
|
|
226
|
+
dailyArray.push({ ...bucket, date, ...events > 0 ? { events } : {} });
|
|
227
|
+
}
|
|
228
|
+
dailyArray.sort((a, b) => a.date.localeCompare(b.date));
|
|
229
|
+
const dailyByPhaseResult = {};
|
|
230
|
+
for (const phase of byPhase.keys()) {
|
|
231
|
+
dailyByPhaseResult[phase] = mapToDailyArray(dailyByPhase, `${phase}:`);
|
|
232
|
+
}
|
|
233
|
+
const dailyByModelResult = {};
|
|
234
|
+
for (const model of byModel.keys()) {
|
|
235
|
+
dailyByModelResult[model] = mapToDailyArray(dailyByModel, `${model}:`);
|
|
236
|
+
}
|
|
237
|
+
const topIssues = [...byIssue.entries()].map(([id, data]) => ({
|
|
238
|
+
id,
|
|
239
|
+
...data,
|
|
240
|
+
byPhase: Object.keys(data.byPhase).length > 0 ? Object.fromEntries(
|
|
241
|
+
Object.entries(data.byPhase).map(([phase, bucket]) => [phase, { ...bucket }])
|
|
242
|
+
) : void 0
|
|
243
|
+
})).sort((a, b) => b.totalTokens - a.totalTokens).slice(0, topN);
|
|
244
|
+
const byPhaseResult = {};
|
|
245
|
+
for (const [k, v] of byPhase) byPhaseResult[k] = { ...v };
|
|
246
|
+
const byModelResult = {};
|
|
247
|
+
for (const [k, v] of byModel) byModelResult[k] = { ...v };
|
|
248
|
+
return {
|
|
249
|
+
overall: { ...overall },
|
|
250
|
+
byPhase: byPhaseResult,
|
|
251
|
+
byModel: byModelResult,
|
|
252
|
+
daily: dailyArray,
|
|
253
|
+
dailyByPhase: dailyByPhaseResult,
|
|
254
|
+
dailyByModel: dailyByModelResult,
|
|
255
|
+
topIssues
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/domains/project.ts
|
|
260
|
+
import { execFileSync } from "child_process";
|
|
261
|
+
import {
|
|
262
|
+
existsSync,
|
|
263
|
+
mkdirSync,
|
|
264
|
+
readdirSync,
|
|
265
|
+
readFileSync,
|
|
266
|
+
writeFileSync
|
|
267
|
+
} from "fs";
|
|
268
|
+
import { homedir } from "os";
|
|
269
|
+
import { basename, dirname, join, relative as relativePath, resolve } from "path";
|
|
270
|
+
|
|
271
|
+
// src/shared/project-meta.ts
|
|
272
|
+
var PROJECT_NAME_SETTING_ID = "system.projectName";
|
|
273
|
+
function normalizeProjectName(value) {
|
|
274
|
+
return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
|
|
275
|
+
}
|
|
276
|
+
function detectProjectNameFromPath(path) {
|
|
277
|
+
const normalizedPath = typeof path === "string" ? path.trim().replace(/[\\/]+$/, "") : "";
|
|
278
|
+
if (!normalizedPath) return "";
|
|
279
|
+
const segments = normalizedPath.split(/[\\/]+/).filter(Boolean);
|
|
280
|
+
return normalizeProjectName(segments.at(-1) || "");
|
|
281
|
+
}
|
|
282
|
+
function readSavedProjectName(settings) {
|
|
283
|
+
const list = Array.isArray(settings) ? settings : [];
|
|
284
|
+
return normalizeProjectName(list.find((setting) => setting?.id === PROJECT_NAME_SETTING_ID)?.value);
|
|
285
|
+
}
|
|
286
|
+
function buildQueueTitle(projectName) {
|
|
287
|
+
const normalizedProjectName = normalizeProjectName(projectName);
|
|
288
|
+
return normalizedProjectName ? `fifony: ${normalizedProjectName}` : "fifony";
|
|
289
|
+
}
|
|
290
|
+
function buildProjectDraft({
|
|
291
|
+
savedProjectName = "",
|
|
292
|
+
detectedProjectName = ""
|
|
293
|
+
} = {}) {
|
|
294
|
+
const saved = normalizeProjectName(savedProjectName);
|
|
295
|
+
const detected = normalizeProjectName(detectedProjectName);
|
|
296
|
+
const projectName = saved || detected;
|
|
297
|
+
return {
|
|
298
|
+
projectName,
|
|
299
|
+
detectedProjectName: detected,
|
|
300
|
+
source: saved ? "saved" : detected ? "detected" : "missing",
|
|
301
|
+
requiresManualEntry: !projectName
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/domains/project.ts
|
|
306
|
+
var SETTING_ID_PROJECT_NAME = PROJECT_NAME_SETTING_ID;
|
|
307
|
+
function detectProjectName(targetRoot) {
|
|
308
|
+
return detectProjectNameFromPath(targetRoot);
|
|
309
|
+
}
|
|
310
|
+
function resolveProjectMetadata(settings, targetRoot) {
|
|
311
|
+
const draft = buildProjectDraft({
|
|
312
|
+
savedProjectName: readSavedProjectName(settings),
|
|
313
|
+
detectedProjectName: detectProjectName(targetRoot)
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
projectName: draft.projectName,
|
|
317
|
+
detectedProjectName: draft.detectedProjectName,
|
|
318
|
+
projectNameSource: draft.source,
|
|
319
|
+
queueTitle: buildQueueTitle(draft.projectName)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function scanProjectFiles(targetRoot) {
|
|
323
|
+
const check = (rel) => existsSync(join(targetRoot, rel));
|
|
324
|
+
const files = {
|
|
325
|
+
claudeMd: check("CLAUDE.md"),
|
|
326
|
+
claudeDir: check(".claude"),
|
|
327
|
+
codexDir: check(".codex"),
|
|
328
|
+
readmeMd: check("README.md"),
|
|
329
|
+
packageJson: check("package.json"),
|
|
330
|
+
cargoToml: check("Cargo.toml"),
|
|
331
|
+
pyprojectToml: check("pyproject.toml"),
|
|
332
|
+
goMod: check("go.mod"),
|
|
333
|
+
buildGradle: check("build.gradle") || check("build.gradle.kts"),
|
|
334
|
+
gemfile: check("Gemfile"),
|
|
335
|
+
dockerfile: check("Dockerfile"),
|
|
336
|
+
agentsMd: check("AGENTS.md"),
|
|
337
|
+
claudeAgentsDir: check(".claude/agents"),
|
|
338
|
+
claudeSkillsDir: check(".claude/skills"),
|
|
339
|
+
codexAgentsDir: check(".codex/agents"),
|
|
340
|
+
codexSkillsDir: check(".codex/skills")
|
|
341
|
+
};
|
|
342
|
+
const existingAgents = [];
|
|
343
|
+
for (const agentDir of [".claude/agents", ".codex/agents"]) {
|
|
344
|
+
const fullPath = join(targetRoot, agentDir);
|
|
345
|
+
if (!existsSync(fullPath)) continue;
|
|
346
|
+
try {
|
|
347
|
+
const entries = readdirSync(fullPath);
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
if (entry.endsWith(".md")) {
|
|
350
|
+
existingAgents.push(basename(entry, ".md"));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const existingSkills = [];
|
|
357
|
+
for (const skillDir of [".claude/skills", ".codex/skills"]) {
|
|
358
|
+
const fullPath = join(targetRoot, skillDir);
|
|
359
|
+
if (!existsSync(fullPath)) continue;
|
|
360
|
+
try {
|
|
361
|
+
const entries = readdirSync(fullPath);
|
|
362
|
+
for (const entry of entries) {
|
|
363
|
+
const skillFile = join(fullPath, entry, "SKILL.md");
|
|
364
|
+
if (existsSync(skillFile)) {
|
|
365
|
+
existingSkills.push(entry);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
let readmeExcerpt = "";
|
|
372
|
+
const readmePath = join(targetRoot, "README.md");
|
|
373
|
+
if (existsSync(readmePath)) {
|
|
374
|
+
try {
|
|
375
|
+
const content = readFileSync(readmePath, "utf8");
|
|
376
|
+
readmeExcerpt = content.slice(0, 200).trim();
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let packageName = "";
|
|
381
|
+
let packageDescription = "";
|
|
382
|
+
const pkgPath = join(targetRoot, "package.json");
|
|
383
|
+
if (existsSync(pkgPath)) {
|
|
384
|
+
try {
|
|
385
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
386
|
+
packageName = typeof pkg.name === "string" ? pkg.name : "";
|
|
387
|
+
packageDescription = typeof pkg.description === "string" ? pkg.description : "";
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
root: targetRoot,
|
|
393
|
+
files,
|
|
394
|
+
existingAgents: [...new Set(existingAgents)],
|
|
395
|
+
existingSkills: [...new Set(existingSkills)],
|
|
396
|
+
readmeExcerpt,
|
|
397
|
+
packageName,
|
|
398
|
+
packageDescription
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
var DEFAULT_REFERENCE_REPOSITORIES = [
|
|
402
|
+
{
|
|
403
|
+
id: "ring",
|
|
404
|
+
name: "LerianStudio/ring",
|
|
405
|
+
url: "https://github.com/LerianStudio/ring.git",
|
|
406
|
+
description: "Massive reference library for agents, skills, commands, and engineering standards.",
|
|
407
|
+
fallbackUrls: ["git@github.com:LerianStudio/ring.git"]
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: "agency-agents",
|
|
411
|
+
name: "msitarzewski/agency-agents",
|
|
412
|
+
url: "https://github.com/msitarzewski/agency-agents.git",
|
|
413
|
+
description: "Reference agent set focused on frontend, backend, QA, and review roles."
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: "impeccable",
|
|
417
|
+
name: "pbakaus/impeccable",
|
|
418
|
+
url: "https://github.com/pbakaus/impeccable.git",
|
|
419
|
+
description: "Frontend polish and impeccable-style quality workflows."
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: "everything-claude-code",
|
|
423
|
+
name: "affaan-m/everything-claude-code",
|
|
424
|
+
url: "https://github.com/affaan-m/everything-claude-code.git",
|
|
425
|
+
description: "28 specialized agents, 116 skills, and 59 commands \u2014 agent harness performance system."
|
|
426
|
+
}
|
|
427
|
+
];
|
|
428
|
+
var REPOSITORY_ROOT = resolve(homedir(), ".fifony", "repositories");
|
|
429
|
+
var MAX_SCAN_DEPTH = 8;
|
|
430
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
431
|
+
".git",
|
|
432
|
+
".github",
|
|
433
|
+
"node_modules",
|
|
434
|
+
"dist",
|
|
435
|
+
"build",
|
|
436
|
+
"coverage",
|
|
437
|
+
"tmp",
|
|
438
|
+
"temp"
|
|
439
|
+
]);
|
|
440
|
+
var AGENCY_AGENTS_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
441
|
+
"examples",
|
|
442
|
+
"strategy"
|
|
443
|
+
]);
|
|
444
|
+
var REFERENCE_REPOSITORY_PARSERS = {
|
|
445
|
+
ring: collectStandardArtifacts,
|
|
446
|
+
"agency-agents": collectAgencyArtifacts,
|
|
447
|
+
impeccable: collectImpeccableArtifacts,
|
|
448
|
+
"everything-claude-code": collectStandardArtifacts
|
|
449
|
+
};
|
|
450
|
+
function runGit(args, cwd) {
|
|
451
|
+
return execFileSync("git", args, {
|
|
452
|
+
cwd,
|
|
453
|
+
encoding: "utf8",
|
|
454
|
+
stdio: "pipe",
|
|
455
|
+
timeout: 12e4
|
|
456
|
+
}).toString().trim();
|
|
457
|
+
}
|
|
458
|
+
function slugify(value) {
|
|
459
|
+
const safe = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/(^-|-$)/g, "");
|
|
460
|
+
return safe || "reference-item";
|
|
461
|
+
}
|
|
462
|
+
function uniqueSuffix(base, used) {
|
|
463
|
+
if (!used.has(base)) {
|
|
464
|
+
used.add(base);
|
|
465
|
+
return base;
|
|
466
|
+
}
|
|
467
|
+
let i = 0;
|
|
468
|
+
while (true) {
|
|
469
|
+
const candidate = `${base}-${++i}`;
|
|
470
|
+
if (!used.has(candidate)) {
|
|
471
|
+
used.add(candidate);
|
|
472
|
+
return candidate;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function collectDirectoryEntries(path) {
|
|
477
|
+
try {
|
|
478
|
+
return readdirSync(path, { withFileTypes: true });
|
|
479
|
+
} catch {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function readRepositoryLine(path) {
|
|
484
|
+
try {
|
|
485
|
+
return runGit(["-C", path, "remote", "get-url", "origin"]);
|
|
486
|
+
} catch {
|
|
487
|
+
return void 0;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function readCurrentBranch(path) {
|
|
491
|
+
try {
|
|
492
|
+
return runGit(["-C", path, "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
493
|
+
} catch {
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function isMarkdownFile(value, expectedName) {
|
|
498
|
+
const lower = value.toLowerCase();
|
|
499
|
+
return lower.endsWith(".md") && lower !== expectedName;
|
|
500
|
+
}
|
|
501
|
+
function isReferenceFrontMatterFile(filePath) {
|
|
502
|
+
let source;
|
|
503
|
+
try {
|
|
504
|
+
source = readFileSync(filePath, "utf8");
|
|
505
|
+
} catch {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
509
|
+
if (!match) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
const header = match[1];
|
|
513
|
+
return /^name:\s*.+/im.test(header) && /^description:\s*.+/im.test(header);
|
|
514
|
+
}
|
|
515
|
+
function buildRelativeArtifactName(repoPath, sourcePath) {
|
|
516
|
+
const relative = sourcePath.startsWith(repoPath) ? relativePath(repoPath, sourcePath) : sourcePath;
|
|
517
|
+
const parent = dirname(relative);
|
|
518
|
+
const parentSlug = parent === "." ? "" : parent.split(/[/\\]/).map((segment) => slugify(segment)).filter(Boolean).join("__");
|
|
519
|
+
const baseName = slugify(basename(relative, ".md"));
|
|
520
|
+
return parentSlug ? `${parentSlug}__${baseName}` : baseName;
|
|
521
|
+
}
|
|
522
|
+
function collectAgentArtifacts(agentsDir, usedNames, out) {
|
|
523
|
+
const parent = slugify(basename(dirname(agentsDir)));
|
|
524
|
+
const entries = collectDirectoryEntries(agentsDir);
|
|
525
|
+
for (const entry of entries) {
|
|
526
|
+
const itemPath = join(agentsDir, entry.name);
|
|
527
|
+
if (entry.isDirectory()) {
|
|
528
|
+
const nestedAgentSpec = join(itemPath, "AGENT.md");
|
|
529
|
+
if (existsSync(nestedAgentSpec)) {
|
|
530
|
+
const name2 = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
|
|
531
|
+
out.push({ kind: "agent", sourcePath: nestedAgentSpec, targetName: name2 });
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (!isMarkdownFile(entry.name, "readme.md")) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const baseName = basename(entry.name, ".md");
|
|
539
|
+
if (baseName.trim().length === 0 || baseName.toLowerCase() === "changelog") {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const name = uniqueSuffix(`${parent}__${slugify(baseName)}`, usedNames);
|
|
543
|
+
out.push({ kind: "agent", sourcePath: itemPath, targetName: name });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function collectSkillArtifacts(skillsDir, usedNames, out) {
|
|
547
|
+
const parent = slugify(basename(dirname(skillsDir)));
|
|
548
|
+
const entries = collectDirectoryEntries(skillsDir);
|
|
549
|
+
for (const entry of entries) {
|
|
550
|
+
const itemPath = join(skillsDir, entry.name);
|
|
551
|
+
if (entry.isDirectory()) {
|
|
552
|
+
const skillFile = join(itemPath, "SKILL.md");
|
|
553
|
+
if (existsSync(skillFile)) {
|
|
554
|
+
const name = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
|
|
555
|
+
out.push({ kind: "skill", sourcePath: skillFile, targetName: name });
|
|
556
|
+
}
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (entry.isFile() && entry.name.toLowerCase() === "skill.md") {
|
|
560
|
+
const name = uniqueSuffix(`${parent}__skill`, usedNames);
|
|
561
|
+
out.push({ kind: "skill", sourcePath: itemPath, targetName: name });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function collectStandardArtifacts(repoPath) {
|
|
566
|
+
const agentsUsed = /* @__PURE__ */ new Set();
|
|
567
|
+
const skillsUsed = /* @__PURE__ */ new Set();
|
|
568
|
+
const artifacts = [];
|
|
569
|
+
const queue = [{ path: repoPath, depth: 0 }];
|
|
570
|
+
while (queue.length > 0) {
|
|
571
|
+
const state = queue.shift();
|
|
572
|
+
if (!state) break;
|
|
573
|
+
if (state.depth > MAX_SCAN_DEPTH) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const entries = collectDirectoryEntries(state.path);
|
|
577
|
+
for (const entry of entries) {
|
|
578
|
+
if (!entry.isDirectory()) continue;
|
|
579
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
580
|
+
const childPath = join(state.path, entry.name);
|
|
581
|
+
if (entry.name === "agents") {
|
|
582
|
+
collectAgentArtifacts(childPath, agentsUsed, artifacts);
|
|
583
|
+
}
|
|
584
|
+
if (entry.name === "skills") {
|
|
585
|
+
collectSkillArtifacts(childPath, skillsUsed, artifacts);
|
|
586
|
+
}
|
|
587
|
+
queue.push({ path: childPath, depth: state.depth + 1 });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return artifacts;
|
|
591
|
+
}
|
|
592
|
+
function collectAgencyArtifacts(repoPath) {
|
|
593
|
+
const agentsUsed = /* @__PURE__ */ new Set();
|
|
594
|
+
const artifacts = [];
|
|
595
|
+
const queue = [{ path: repoPath, depth: 0 }];
|
|
596
|
+
while (queue.length > 0) {
|
|
597
|
+
const state = queue.shift();
|
|
598
|
+
if (!state) break;
|
|
599
|
+
if (state.depth > MAX_SCAN_DEPTH) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const entries = collectDirectoryEntries(state.path);
|
|
603
|
+
for (const entry of entries) {
|
|
604
|
+
if (entry.isDirectory()) {
|
|
605
|
+
if (SKIP_DIRS.has(entry.name) || AGENCY_AGENTS_EXCLUDED_DIRS.has(entry.name)) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
queue.push({ path: join(state.path, entry.name), depth: state.depth + 1 });
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(join(state.path, entry.name))) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const itemPath = join(state.path, entry.name);
|
|
615
|
+
const targetName = uniqueSuffix(buildRelativeArtifactName(repoPath, itemPath), agentsUsed);
|
|
616
|
+
artifacts.push({
|
|
617
|
+
kind: "agent",
|
|
618
|
+
sourcePath: itemPath,
|
|
619
|
+
targetName
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return artifacts;
|
|
624
|
+
}
|
|
625
|
+
function collectImpeccableArtifacts(repoPath) {
|
|
626
|
+
const skillsUsed = /* @__PURE__ */ new Set();
|
|
627
|
+
const artifacts = [];
|
|
628
|
+
const sourceSkills = join(repoPath, "source", "skills");
|
|
629
|
+
if (existsSync(sourceSkills)) {
|
|
630
|
+
collectSkillArtifacts(sourceSkills, skillsUsed, artifacts);
|
|
631
|
+
return artifacts;
|
|
632
|
+
}
|
|
633
|
+
const claudeSkills = join(repoPath, ".claude", "skills");
|
|
634
|
+
if (existsSync(claudeSkills)) {
|
|
635
|
+
collectSkillArtifacts(claudeSkills, skillsUsed, artifacts);
|
|
636
|
+
}
|
|
637
|
+
return artifacts;
|
|
638
|
+
}
|
|
639
|
+
function collectArtifacts(repoPath, repositoryId) {
|
|
640
|
+
const parser = repositoryId && REFERENCE_REPOSITORY_PARSERS[repositoryId] ? REFERENCE_REPOSITORY_PARSERS[repositoryId] : collectStandardArtifacts;
|
|
641
|
+
return parser(repoPath);
|
|
642
|
+
}
|
|
643
|
+
function countArtifactKinds(artifacts) {
|
|
644
|
+
let agents = 0;
|
|
645
|
+
let skills = 0;
|
|
646
|
+
for (const artifact of artifacts) {
|
|
647
|
+
if (artifact.kind === "agent") {
|
|
648
|
+
agents += 1;
|
|
649
|
+
} else {
|
|
650
|
+
skills += 1;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return { agents, skills };
|
|
654
|
+
}
|
|
655
|
+
function getReferenceRepositoriesRoot() {
|
|
656
|
+
return REPOSITORY_ROOT;
|
|
657
|
+
}
|
|
658
|
+
function listReferenceRepositories() {
|
|
659
|
+
return DEFAULT_REFERENCE_REPOSITORIES.map((repo) => {
|
|
660
|
+
const path = join(REPOSITORY_ROOT, repo.id);
|
|
661
|
+
const status = {
|
|
662
|
+
id: repo.id,
|
|
663
|
+
name: repo.name,
|
|
664
|
+
url: repo.url,
|
|
665
|
+
path,
|
|
666
|
+
present: existsSync(path),
|
|
667
|
+
synced: false
|
|
668
|
+
};
|
|
669
|
+
if (!status.present) {
|
|
670
|
+
return status;
|
|
671
|
+
}
|
|
672
|
+
if (!existsSync(join(path, ".git"))) {
|
|
673
|
+
status.error = "Path exists but is not a git repo";
|
|
674
|
+
return status;
|
|
675
|
+
}
|
|
676
|
+
status.remote = readRepositoryLine(path);
|
|
677
|
+
status.branch = readCurrentBranch(path);
|
|
678
|
+
status.synced = typeof status.remote === "string";
|
|
679
|
+
if (status.synced) {
|
|
680
|
+
const artifacts = collectArtifacts(path, repo.id);
|
|
681
|
+
status.artifactCounts = countArtifactKinds(artifacts);
|
|
682
|
+
}
|
|
683
|
+
return status;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function resolveReferenceRepository(query) {
|
|
687
|
+
const normalized = query.trim().toLowerCase();
|
|
688
|
+
if (!normalized) return void 0;
|
|
689
|
+
const normalizedWithoutGit = normalized.endsWith(".git") ? normalized.slice(0, -4) : normalized;
|
|
690
|
+
return DEFAULT_REFERENCE_REPOSITORIES.find(
|
|
691
|
+
(repo) => repo.id.toLowerCase() === normalizedWithoutGit || repo.name.toLowerCase() === normalizedWithoutGit || repo.url.toLowerCase() === normalized || repo.url.toLowerCase() === normalizedWithoutGit || repo.url.toLowerCase().endsWith(`/${normalizedWithoutGit}.git`) || repo.url.toLowerCase().endsWith(`/${normalizedWithoutGit}`)
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
function syncReferenceRepositories(repositoryId) {
|
|
695
|
+
const root = REPOSITORY_ROOT;
|
|
696
|
+
mkdirSync(root, { recursive: true });
|
|
697
|
+
const repos = repositoryId ? [resolveReferenceRepository(repositoryId)] : DEFAULT_REFERENCE_REPOSITORIES;
|
|
698
|
+
const selected = repos.filter((repo) => Boolean(repo));
|
|
699
|
+
if (repositoryId && selected.length === 0) {
|
|
700
|
+
throw new Error(`Unknown reference repository: ${repositoryId}`);
|
|
701
|
+
}
|
|
702
|
+
const results = [];
|
|
703
|
+
for (const repo of selected) {
|
|
704
|
+
const target = join(root, repo.id);
|
|
705
|
+
const candidates = [repo.url, ...repo.fallbackUrls ?? []];
|
|
706
|
+
if (!existsSync(target)) {
|
|
707
|
+
let cloneError;
|
|
708
|
+
for (const candidate of candidates) {
|
|
709
|
+
try {
|
|
710
|
+
runGit(["clone", "--depth", "1", candidate, target]);
|
|
711
|
+
results.push({
|
|
712
|
+
id: repo.id,
|
|
713
|
+
path: target,
|
|
714
|
+
action: "cloned",
|
|
715
|
+
message: `Cloned ${candidate}`
|
|
716
|
+
});
|
|
717
|
+
cloneError = void 0;
|
|
718
|
+
break;
|
|
719
|
+
} catch (error) {
|
|
720
|
+
cloneError = error instanceof Error ? error.message : String(error);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (cloneError) {
|
|
724
|
+
results.push({
|
|
725
|
+
id: repo.id,
|
|
726
|
+
path: target,
|
|
727
|
+
action: "failed",
|
|
728
|
+
message: cloneError
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (!existsSync(join(target, ".git"))) {
|
|
734
|
+
results.push({
|
|
735
|
+
id: repo.id,
|
|
736
|
+
path: target,
|
|
737
|
+
action: "failed",
|
|
738
|
+
message: "Path exists but is not a git repository"
|
|
739
|
+
});
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
runGit(["-C", target, "fetch", "--all", "--prune"]);
|
|
744
|
+
runGit(["-C", target, "pull", "--ff-only"]);
|
|
745
|
+
results.push({
|
|
746
|
+
id: repo.id,
|
|
747
|
+
path: target,
|
|
748
|
+
action: "updated",
|
|
749
|
+
message: "Updated from remote"
|
|
750
|
+
});
|
|
751
|
+
} catch (error) {
|
|
752
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
753
|
+
results.push({
|
|
754
|
+
id: repo.id,
|
|
755
|
+
path: target,
|
|
756
|
+
action: "failed",
|
|
757
|
+
message
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return results;
|
|
762
|
+
}
|
|
763
|
+
function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
764
|
+
const repository = resolveReferenceRepository(repositoryId);
|
|
765
|
+
if (!repository) {
|
|
766
|
+
throw new Error(`Unknown reference repository: ${repositoryId}`);
|
|
767
|
+
}
|
|
768
|
+
const localPath = join(REPOSITORY_ROOT, repository.id);
|
|
769
|
+
if (!existsSync(localPath)) {
|
|
770
|
+
throw new Error(`Repository not synced yet: ${repository.id}. Run 'fifony onboarding sync --repository ${repository.id}' first.`);
|
|
771
|
+
}
|
|
772
|
+
const basePath = resolve(workspaceRoot);
|
|
773
|
+
const targetBase = options.importToGlobal ? join(homedir(), ".codex") : join(basePath, ".codex");
|
|
774
|
+
const agentsDir = join(targetBase, "agents");
|
|
775
|
+
const skillsDir = join(targetBase, "skills");
|
|
776
|
+
const artifacts = collectArtifacts(localPath, repository.id);
|
|
777
|
+
const filtered = options.kind === "all" ? artifacts : artifacts.filter((artifact) => artifact.kind === options.kind.slice(0, -1));
|
|
778
|
+
const summary = {
|
|
779
|
+
repositoryId: repository.id,
|
|
780
|
+
localPath,
|
|
781
|
+
requestedKind: options.kind,
|
|
782
|
+
dryRun: options.dryRun,
|
|
783
|
+
importedAgents: [],
|
|
784
|
+
importedSkills: [],
|
|
785
|
+
skippedAgents: [],
|
|
786
|
+
skippedSkills: [],
|
|
787
|
+
errors: []
|
|
788
|
+
};
|
|
789
|
+
if (filtered.length === 0) {
|
|
790
|
+
return summary;
|
|
791
|
+
}
|
|
792
|
+
if (!options.dryRun) {
|
|
793
|
+
mkdirSync(targetBase, { recursive: true });
|
|
794
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
795
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
796
|
+
}
|
|
797
|
+
for (const artifact of filtered) {
|
|
798
|
+
try {
|
|
799
|
+
const source = readFileSync(artifact.sourcePath, "utf8");
|
|
800
|
+
if (artifact.kind === "agent") {
|
|
801
|
+
const target = join(agentsDir, `${artifact.targetName}.md`);
|
|
802
|
+
if (!options.overwrite && existsSync(target)) {
|
|
803
|
+
summary.skippedAgents.push(artifact.targetName);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (options.dryRun) {
|
|
807
|
+
summary.importedAgents.push(artifact.targetName);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
writeFileSync(target, source, "utf8");
|
|
811
|
+
summary.importedAgents.push(artifact.targetName);
|
|
812
|
+
} else {
|
|
813
|
+
const targetDir = join(skillsDir, artifact.targetName);
|
|
814
|
+
const target = join(targetDir, "SKILL.md");
|
|
815
|
+
if (!options.overwrite && existsSync(target)) {
|
|
816
|
+
summary.skippedSkills.push(artifact.targetName);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (options.dryRun) {
|
|
820
|
+
summary.importedSkills.push(artifact.targetName);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
mkdirSync(targetDir, { recursive: true });
|
|
824
|
+
writeFileSync(target, source, "utf8");
|
|
825
|
+
summary.importedSkills.push(artifact.targetName);
|
|
826
|
+
}
|
|
827
|
+
} catch (error) {
|
|
828
|
+
summary.errors.push({
|
|
829
|
+
kind: artifact.kind,
|
|
830
|
+
targetName: artifact.targetName,
|
|
831
|
+
error: error instanceof Error ? error.message : String(error)
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return summary;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/domains/config.ts
|
|
839
|
+
import { env } from "process";
|
|
840
|
+
var VALID_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
|
|
841
|
+
function parseEffortValue(value) {
|
|
842
|
+
const str = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
843
|
+
return VALID_EFFORTS.has(str) ? str : void 0;
|
|
844
|
+
}
|
|
845
|
+
function parseEffortConfig(value) {
|
|
846
|
+
if (!value || typeof value !== "object") {
|
|
847
|
+
const simple = parseEffortValue(value);
|
|
848
|
+
return simple ? { default: simple } : void 0;
|
|
849
|
+
}
|
|
850
|
+
const obj = value;
|
|
851
|
+
const config = {};
|
|
852
|
+
const d = parseEffortValue(obj.default);
|
|
853
|
+
const p = parseEffortValue(obj.planner);
|
|
854
|
+
const e = parseEffortValue(obj.executor);
|
|
855
|
+
const r = parseEffortValue(obj.reviewer);
|
|
856
|
+
if (d) config.default = d;
|
|
857
|
+
if (p) config.planner = p;
|
|
858
|
+
if (e) config.executor = e;
|
|
859
|
+
if (r) config.reviewer = r;
|
|
860
|
+
return Object.keys(config).length > 0 ? config : void 0;
|
|
861
|
+
}
|
|
862
|
+
function deriveConfig(args) {
|
|
863
|
+
const parsedConcurrency = parsePositiveIntEnv("FIFONY_WORKER_CONCURRENCY", 3);
|
|
864
|
+
let pollIntervalMs = parseEnvNumber("FIFONY_POLL_INTERVAL_MS", 1200);
|
|
865
|
+
let workerConcurrency = parsedConcurrency;
|
|
866
|
+
let maxAttemptsDefault = parseEnvNumber("FIFONY_MAX_ATTEMPTS", 3);
|
|
867
|
+
let commandTimeoutMs = parseEnvNumber("FIFONY_AGENT_TIMEOUT_MS", 18e5);
|
|
868
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
869
|
+
const arg = args[i];
|
|
870
|
+
if (arg === "--poll") {
|
|
871
|
+
const value = args[i + 1] ?? "";
|
|
872
|
+
if (!/^\d+$/.test(value)) fail(`Invalid value for --poll: ${value}`);
|
|
873
|
+
pollIntervalMs = parseIntArg(value, pollIntervalMs);
|
|
874
|
+
}
|
|
875
|
+
if (arg === "--concurrency") {
|
|
876
|
+
const value = args[i + 1] ?? "";
|
|
877
|
+
if (!/^\d+$/.test(value)) fail(`Invalid value for --concurrency: ${value}`);
|
|
878
|
+
workerConcurrency = parseIntArg(value, workerConcurrency);
|
|
879
|
+
}
|
|
880
|
+
if (arg === "--attempts") {
|
|
881
|
+
const value = args[i + 1] ?? "";
|
|
882
|
+
if (!/^\d+$/.test(value)) fail(`Invalid value for --attempts: ${value}`);
|
|
883
|
+
maxAttemptsDefault = parseIntArg(value, maxAttemptsDefault);
|
|
884
|
+
}
|
|
885
|
+
if (arg === "--timeout") {
|
|
886
|
+
const value = args[i + 1] ?? "";
|
|
887
|
+
if (!/^\d+$/.test(value)) fail(`Invalid value for --timeout: ${value}`);
|
|
888
|
+
commandTimeoutMs = parseIntArg(value, commandTimeoutMs);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
pollIntervalMs: clamp(pollIntervalMs, 200, 1e4),
|
|
893
|
+
workerConcurrency: clamp(workerConcurrency, 1, 10),
|
|
894
|
+
commandTimeoutMs: clamp(commandTimeoutMs, 1e3, 36e5),
|
|
895
|
+
maxAttemptsDefault: clamp(maxAttemptsDefault, 1, 10),
|
|
896
|
+
maxTurns: clamp(parseEnvNumber("FIFONY_AGENT_MAX_TURNS", 4), 1, 50),
|
|
897
|
+
retryDelayMs: parseEnvNumber("FIFONY_RETRY_DELAY_MS", 3e3),
|
|
898
|
+
staleInProgressTimeoutMs: parseEnvNumber("FIFONY_STALE_IN_PROGRESS_MS", 24e5),
|
|
899
|
+
logLinesTail: parseEnvNumber("FIFONY_LOG_TAIL_CHARS", 12e3),
|
|
900
|
+
maxPreviousOutputChars: parseEnvNumber("FIFONY_PREVIOUS_OUTPUT_CHARS", 2e4),
|
|
901
|
+
agentProvider: normalizeAgentProvider(env.FIFONY_AGENT_PROVIDER ?? "codex"),
|
|
902
|
+
agentCommand: toStringValue(env.FIFONY_AGENT_COMMAND, ""),
|
|
903
|
+
defaultEffort: {
|
|
904
|
+
default: parseEffortValue(env.FIFONY_REASONING_EFFORT),
|
|
905
|
+
planner: parseEffortValue(env.FIFONY_PLANNER_EFFORT),
|
|
906
|
+
executor: parseEffortValue(env.FIFONY_EXECUTOR_EFFORT),
|
|
907
|
+
reviewer: parseEffortValue(env.FIFONY_REVIEWER_EFFORT)
|
|
908
|
+
},
|
|
909
|
+
maxConcurrentByState: {},
|
|
910
|
+
runMode: "filesystem",
|
|
911
|
+
autoReviewApproval: true,
|
|
912
|
+
dockerExecution: false,
|
|
913
|
+
dockerImage: "fifony-agent:latest",
|
|
914
|
+
adaptiveHarnessSelection: toStringValue(env.FIFONY_ADAPTIVE_HARNESS_SELECTION, "true") !== "false",
|
|
915
|
+
adaptiveReviewRouting: toStringValue(env.FIFONY_ADAPTIVE_REVIEW_ROUTING, "true") !== "false",
|
|
916
|
+
adaptivePolicyMinSamples: clamp(parseEnvNumber("FIFONY_ADAPTIVE_POLICY_MIN_SAMPLES", 3), 1, 10),
|
|
917
|
+
afterCreateHook: env.FIFONY_AFTER_CREATE_HOOK ?? "",
|
|
918
|
+
beforeRunHook: env.FIFONY_BEFORE_RUN_HOOK ?? "",
|
|
919
|
+
afterRunHook: env.FIFONY_AFTER_RUN_HOOK ?? "",
|
|
920
|
+
beforeRemoveHook: env.FIFONY_BEFORE_REMOVE_HOOK ?? "",
|
|
921
|
+
serviceEnv: {}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function applyWorkflowConfig(config, port) {
|
|
925
|
+
return {
|
|
926
|
+
...config,
|
|
927
|
+
dashboardPort: port ? String(port) : config.dashboardPort
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function validateConfig(config) {
|
|
931
|
+
const errors = [];
|
|
932
|
+
if (config.pollIntervalMs < 200) errors.push(`pollIntervalMs too low: ${config.pollIntervalMs} (min 200)`);
|
|
933
|
+
if (config.workerConcurrency < 1 || config.workerConcurrency > 10) errors.push(`workerConcurrency out of range: ${config.workerConcurrency} (1-10)`);
|
|
934
|
+
if (config.maxAttemptsDefault < 1 || config.maxAttemptsDefault > 10) errors.push(`maxAttemptsDefault out of range: ${config.maxAttemptsDefault} (1-10)`);
|
|
935
|
+
if (config.maxTurns < 1 || config.maxTurns > 50) errors.push(`maxTurns out of range: ${config.maxTurns} (1-50)`);
|
|
936
|
+
if (config.commandTimeoutMs < 1e3) errors.push(`commandTimeoutMs too low: ${config.commandTimeoutMs} (min 1000)`);
|
|
937
|
+
if (config.retryDelayMs < 0) errors.push(`retryDelayMs negative: ${config.retryDelayMs}`);
|
|
938
|
+
if (config.adaptivePolicyMinSamples !== void 0 && (config.adaptivePolicyMinSamples < 1 || config.adaptivePolicyMinSamples > 10)) {
|
|
939
|
+
errors.push(`adaptivePolicyMinSamples out of range: ${config.adaptivePolicyMinSamples} (1-10)`);
|
|
940
|
+
}
|
|
941
|
+
for (const [stateKey, limit] of Object.entries(config.maxConcurrentByState)) {
|
|
942
|
+
if (limit < 1) errors.push(`maxConcurrentByState[${stateKey}] must be >= 1, got ${limit}`);
|
|
943
|
+
}
|
|
944
|
+
return errors;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/domains/milestones.ts
|
|
948
|
+
function slugifyMilestoneName(value) {
|
|
949
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "milestone";
|
|
950
|
+
}
|
|
951
|
+
function normalizeMilestone(raw) {
|
|
952
|
+
const id = toStringValue(raw.id, "");
|
|
953
|
+
if (!id) return null;
|
|
954
|
+
const createdAt = toStringValue(raw.createdAt, now());
|
|
955
|
+
const updatedAt = toStringValue(raw.updatedAt, createdAt);
|
|
956
|
+
const name = toStringValue(raw.name, "").trim();
|
|
957
|
+
if (!name) return null;
|
|
958
|
+
return {
|
|
959
|
+
id,
|
|
960
|
+
slug: toStringValue(raw.slug, slugifyMilestoneName(name)),
|
|
961
|
+
name,
|
|
962
|
+
description: toStringValue(raw.description) || void 0,
|
|
963
|
+
status: normalizeMilestoneStatus(raw.status),
|
|
964
|
+
createdAt,
|
|
965
|
+
updatedAt,
|
|
966
|
+
progress: { scopeCount: 0, completedCount: 0, progressPercent: 0 },
|
|
967
|
+
issueCount: 0
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function normalizeMilestoneStatus(value) {
|
|
971
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
972
|
+
return normalized === "planned" || normalized === "active" || normalized === "paused" || normalized === "done" || normalized === "cancelled" ? normalized : "planned";
|
|
973
|
+
}
|
|
974
|
+
function createMilestoneFromPayload(payload) {
|
|
975
|
+
const createdAt = now();
|
|
976
|
+
const name = toStringValue(payload.name, "").trim();
|
|
977
|
+
if (!name) {
|
|
978
|
+
throw new Error("Milestone name is required.");
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
id: toStringValue(payload.id, `milestone-${crypto.randomUUID()}`),
|
|
982
|
+
slug: toStringValue(payload.slug, slugifyMilestoneName(name)),
|
|
983
|
+
name,
|
|
984
|
+
description: toStringValue(payload.description) || void 0,
|
|
985
|
+
status: normalizeMilestoneStatus(payload.status),
|
|
986
|
+
createdAt,
|
|
987
|
+
updatedAt: createdAt,
|
|
988
|
+
progress: { scopeCount: 0, completedCount: 0, progressPercent: 0 },
|
|
989
|
+
issueCount: 0
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function findMilestone(state, milestoneId) {
|
|
993
|
+
return state.milestones.find((milestone) => milestone.id === milestoneId || milestone.slug === milestoneId);
|
|
994
|
+
}
|
|
995
|
+
function deriveMilestoneProgressSummary(issues) {
|
|
996
|
+
const scopedIssues = issues.filter((issue) => issue.state !== "Cancelled" && issue.state !== "Archived");
|
|
997
|
+
const completedCount = scopedIssues.filter((issue) => COMPLETED_STATES.has(issue.state) && issue.state !== "Cancelled" && issue.state !== "Archived").length;
|
|
998
|
+
const scopeCount = scopedIssues.length;
|
|
999
|
+
return {
|
|
1000
|
+
scopeCount,
|
|
1001
|
+
completedCount,
|
|
1002
|
+
progressPercent: scopeCount === 0 ? 0 : Math.floor(completedCount / scopeCount * 100)
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function refreshMilestoneSummaries(state) {
|
|
1006
|
+
state.milestones = state.milestones.map((milestone) => {
|
|
1007
|
+
const linkedIssues = state.issues.filter((issue) => issue.milestoneId === milestone.id);
|
|
1008
|
+
return {
|
|
1009
|
+
...milestone,
|
|
1010
|
+
progress: deriveMilestoneProgressSummary(linkedIssues),
|
|
1011
|
+
issueCount: linkedIssues.length
|
|
1012
|
+
};
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/domains/issues.ts
|
|
1017
|
+
var issueTransitionExecutor = null;
|
|
1018
|
+
function setIssueTransitionExecutor(executor) {
|
|
1019
|
+
issueTransitionExecutor = executor;
|
|
1020
|
+
}
|
|
1021
|
+
function getIssueTransitionExecutor() {
|
|
1022
|
+
return issueTransitionExecutor;
|
|
1023
|
+
}
|
|
1024
|
+
function normalizeIssue(raw) {
|
|
1025
|
+
const id = toStringValue(raw.id, "");
|
|
1026
|
+
if (!id) return null;
|
|
1027
|
+
const createdAt = toStringValue(raw.createdAt, now());
|
|
1028
|
+
const updatedAt = toStringValue(raw.updatedAt, createdAt);
|
|
1029
|
+
const milestoneId = toStringValue(raw.milestoneId) || toStringValue(raw["projectId"]) || void 0;
|
|
1030
|
+
const issue = {
|
|
1031
|
+
id,
|
|
1032
|
+
identifier: toStringValue(raw.identifier, id),
|
|
1033
|
+
title: toStringValue(raw.title, `Issue ${id}`),
|
|
1034
|
+
description: toStringValue(raw.description, ""),
|
|
1035
|
+
state: normalizeState(raw.state, raw.plan && typeof raw.plan === "object" ? "PendingApproval" : "Planning"),
|
|
1036
|
+
milestoneId,
|
|
1037
|
+
branchName: toStringValue(raw.branchName),
|
|
1038
|
+
url: toStringValue(raw.url),
|
|
1039
|
+
assigneeId: toStringValue(raw.assigneeId),
|
|
1040
|
+
labels: toStringArray(raw.labels),
|
|
1041
|
+
paths: toStringArray(raw.paths),
|
|
1042
|
+
blockedBy: toStringArray(raw.blockedBy),
|
|
1043
|
+
assignedToWorker: toBooleanValue(raw.assignedToWorker, true),
|
|
1044
|
+
createdAt,
|
|
1045
|
+
updatedAt,
|
|
1046
|
+
history: [],
|
|
1047
|
+
attempts: toNumberValue(raw.attempts, 0),
|
|
1048
|
+
maxAttempts: toNumberValue(raw.maxAttempts, 3),
|
|
1049
|
+
nextRetryAt: toStringValue(raw.nextRetryAt),
|
|
1050
|
+
planVersion: 0,
|
|
1051
|
+
executeAttempt: 0,
|
|
1052
|
+
reviewAttempt: 0,
|
|
1053
|
+
checkpointAttempt: 0,
|
|
1054
|
+
contractNegotiationAttempt: 0,
|
|
1055
|
+
planHistory: [],
|
|
1056
|
+
contractNegotiationRuns: [],
|
|
1057
|
+
reviewRuns: [],
|
|
1058
|
+
reviewFailureHistory: [],
|
|
1059
|
+
policyDecisions: [],
|
|
1060
|
+
contextReportsByRole: {},
|
|
1061
|
+
memoryFlushCount: 0
|
|
1062
|
+
};
|
|
1063
|
+
return issue;
|
|
1064
|
+
}
|
|
1065
|
+
function nextLocalIssueId(issues) {
|
|
1066
|
+
const maxId = issues.reduce((current, issue) => {
|
|
1067
|
+
const match = issue.identifier.match(/^#(\d+)$/);
|
|
1068
|
+
if (!match) return current;
|
|
1069
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
1070
|
+
return Number.isFinite(parsed) ? Math.max(current, parsed) : current;
|
|
1071
|
+
}, 0);
|
|
1072
|
+
return `#${maxId + 1}`;
|
|
1073
|
+
}
|
|
1074
|
+
function createIssueFromPayload(payload, issues, defaultBranch) {
|
|
1075
|
+
const identifier = toStringValue(payload.identifier, nextLocalIssueId(issues));
|
|
1076
|
+
const id = toStringValue(payload.id, identifier.replace(/^#/, "issue-"));
|
|
1077
|
+
logger.info({ id, identifier, title: toStringValue(payload.title, "").slice(0, 80) }, "[Issues] Creating new issue");
|
|
1078
|
+
const createdAt = now();
|
|
1079
|
+
const blockedBy = toStringArray(payload.blockedBy);
|
|
1080
|
+
const paths = toStringArray(payload.paths);
|
|
1081
|
+
const images = toStringArray(payload.images);
|
|
1082
|
+
const initialState = parseIssueState(payload.state) ?? (payload.plan ? "PendingApproval" : "Planning");
|
|
1083
|
+
const issue = {
|
|
1084
|
+
id,
|
|
1085
|
+
identifier,
|
|
1086
|
+
title: toStringValue(payload.title, `Issue ${identifier}`),
|
|
1087
|
+
description: toStringValue(payload.description, ""),
|
|
1088
|
+
state: initialState,
|
|
1089
|
+
milestoneId: toStringValue(payload.milestoneId) || void 0,
|
|
1090
|
+
branchName: toStringValue(payload.branchName),
|
|
1091
|
+
baseBranch: toStringValue(payload.baseBranch) || defaultBranch,
|
|
1092
|
+
url: toStringValue(payload.url),
|
|
1093
|
+
assigneeId: toStringValue(payload.assigneeId),
|
|
1094
|
+
labels: toStringArray(payload.labels),
|
|
1095
|
+
paths,
|
|
1096
|
+
blockedBy,
|
|
1097
|
+
assignedToWorker: true,
|
|
1098
|
+
createdAt,
|
|
1099
|
+
updatedAt: createdAt,
|
|
1100
|
+
history: [`[${createdAt}] Issue created via API.`],
|
|
1101
|
+
attempts: 0,
|
|
1102
|
+
maxAttempts: clamp(toNumberValue(payload.maxAttempts, 3), 1, 10),
|
|
1103
|
+
terminalWeek: "",
|
|
1104
|
+
images: images.length ? images : void 0,
|
|
1105
|
+
issueType: toStringValue(payload.issueType) || void 0,
|
|
1106
|
+
effort: parseEffortConfig(payload.effort),
|
|
1107
|
+
plan: payload.plan && typeof payload.plan === "object" ? payload.plan : void 0,
|
|
1108
|
+
planVersion: payload.plan ? 1 : 0,
|
|
1109
|
+
executeAttempt: 0,
|
|
1110
|
+
reviewAttempt: 0,
|
|
1111
|
+
checkpointAttempt: 0,
|
|
1112
|
+
contractNegotiationAttempt: 0,
|
|
1113
|
+
planHistory: [],
|
|
1114
|
+
contractNegotiationRuns: [],
|
|
1115
|
+
reviewRuns: [],
|
|
1116
|
+
reviewFailureHistory: [],
|
|
1117
|
+
policyDecisions: [],
|
|
1118
|
+
contextReportsByRole: {},
|
|
1119
|
+
memoryFlushCount: 0
|
|
1120
|
+
};
|
|
1121
|
+
if (issue.plan) {
|
|
1122
|
+
if (issue.plan.suggestedPaths?.length && !issue.paths?.length) {
|
|
1123
|
+
issue.paths = issue.plan.suggestedPaths;
|
|
1124
|
+
}
|
|
1125
|
+
if (issue.plan.suggestedEffort && !issue.effort) {
|
|
1126
|
+
issue.effort = issue.plan.suggestedEffort;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return issue;
|
|
1130
|
+
}
|
|
1131
|
+
function dedupHistoryEntries(issues) {
|
|
1132
|
+
for (const issue of issues) {
|
|
1133
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1134
|
+
issue.history = issue.history.filter((entry) => {
|
|
1135
|
+
const key = entry.toLowerCase();
|
|
1136
|
+
if (seen.has(key)) return false;
|
|
1137
|
+
seen.add(key);
|
|
1138
|
+
return true;
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
function buildRuntimeState(previous, config, projectMetadata = resolveProjectMetadata([], TARGET_ROOT), persistedMilestones = []) {
|
|
1143
|
+
const mergedIssues = (previous?.issues ?? []).reduce((issues, rawIssue) => {
|
|
1144
|
+
if (!rawIssue || typeof rawIssue !== "object") return issues;
|
|
1145
|
+
const existing = rawIssue;
|
|
1146
|
+
const existingRecord = rawIssue;
|
|
1147
|
+
issues.push({
|
|
1148
|
+
...existing,
|
|
1149
|
+
id: toStringValue(existing.id, ""),
|
|
1150
|
+
identifier: toStringValue(existing.identifier, existing.id),
|
|
1151
|
+
title: toStringValue(existing.title, `Issue ${toStringValue(existing.identifier, existing.id)}`),
|
|
1152
|
+
description: toStringValue(existing.description, ""),
|
|
1153
|
+
state: normalizeState(existing.state, existing.plan ? "PendingApproval" : "Planning"),
|
|
1154
|
+
milestoneId: toStringValue(existing.milestoneId) || toStringValue(existingRecord["projectId"]) || void 0,
|
|
1155
|
+
paths: toStringArray(existing.paths),
|
|
1156
|
+
labels: toStringArray(existing.labels),
|
|
1157
|
+
blockedBy: toStringArray(existing.blockedBy),
|
|
1158
|
+
history: Array.isArray(existing.history) ? existing.history : [],
|
|
1159
|
+
attempts: clamp(toNumberValue(existing.attempts, 0), 0, config.maxAttemptsDefault),
|
|
1160
|
+
maxAttempts: clamp(toNumberValue(existing.maxAttempts, config.maxAttemptsDefault), 1, config.maxAttemptsDefault),
|
|
1161
|
+
nextRetryAt: toStringValue(existing.nextRetryAt),
|
|
1162
|
+
updatedAt: toStringValue(existing.updatedAt, now()),
|
|
1163
|
+
createdAt: toStringValue(existing.createdAt, now()),
|
|
1164
|
+
planVersion: toNumberValue(existing.planVersion, existing.plan ? 1 : 0),
|
|
1165
|
+
executeAttempt: toNumberValue(existing.executeAttempt, toNumberValue(existing.attempts, 0)),
|
|
1166
|
+
reviewAttempt: toNumberValue(existing.reviewAttempt, toNumberValue(existing.attempts, 0)),
|
|
1167
|
+
checkpointAttempt: toNumberValue(existing.checkpointAttempt, 0),
|
|
1168
|
+
contractNegotiationAttempt: toNumberValue(existing.contractNegotiationAttempt, 0),
|
|
1169
|
+
checkpointStatus: toStringValue(existing.checkpointStatus),
|
|
1170
|
+
checkpointPassedAt: toStringValue(existing.checkpointPassedAt) || void 0,
|
|
1171
|
+
contractNegotiationStatus: toStringValue(existing.contractNegotiationStatus),
|
|
1172
|
+
planHistory: Array.isArray(existing.planHistory) ? existing.planHistory : [],
|
|
1173
|
+
contractNegotiationRuns: Array.isArray(existing.contractNegotiationRuns) ? existing.contractNegotiationRuns : [],
|
|
1174
|
+
reviewRuns: Array.isArray(existing.reviewRuns) ? existing.reviewRuns : [],
|
|
1175
|
+
reviewFailureHistory: Array.isArray(existing.reviewFailureHistory) ? existing.reviewFailureHistory : [],
|
|
1176
|
+
policyDecisions: Array.isArray(existing.policyDecisions) ? existing.policyDecisions : [],
|
|
1177
|
+
contextReportsByRole: existing.contextReportsByRole && typeof existing.contextReportsByRole === "object" ? existing.contextReportsByRole : {},
|
|
1178
|
+
memoryFlushAt: toStringValue(existing.memoryFlushAt) || void 0,
|
|
1179
|
+
memoryFlushCount: toNumberValue(existing.memoryFlushCount, 0)
|
|
1180
|
+
});
|
|
1181
|
+
return issues;
|
|
1182
|
+
}, []).filter((issue) => issue.id);
|
|
1183
|
+
for (const issue of mergedIssues) {
|
|
1184
|
+
if (TERMINAL_STATES.has(issue.state) && !issue.terminalWeek) {
|
|
1185
|
+
issue.terminalWeek = isoWeek(issue.completedAt || issue.updatedAt);
|
|
1186
|
+
} else if (!TERMINAL_STATES.has(issue.state)) {
|
|
1187
|
+
issue.terminalWeek = "";
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
dedupHistoryEntries(mergedIssues);
|
|
1191
|
+
const metrics = computeMetrics(mergedIssues);
|
|
1192
|
+
const mergedMilestones = (previous?.milestones ?? persistedMilestones).map((milestone) => normalizeMilestone(milestone)).filter((milestone) => Boolean(milestone));
|
|
1193
|
+
const state = {
|
|
1194
|
+
startedAt: previous?.startedAt ?? now(),
|
|
1195
|
+
updatedAt: now(),
|
|
1196
|
+
trackerKind: "filesystem",
|
|
1197
|
+
sourceRepoUrl: TARGET_ROOT,
|
|
1198
|
+
sourceRef: "workspace",
|
|
1199
|
+
projectName: projectMetadata.projectName,
|
|
1200
|
+
detectedProjectName: projectMetadata.detectedProjectName,
|
|
1201
|
+
projectNameSource: projectMetadata.projectNameSource,
|
|
1202
|
+
queueTitle: projectMetadata.queueTitle,
|
|
1203
|
+
config: {
|
|
1204
|
+
...config,
|
|
1205
|
+
dashboardPort: previous?.config.dashboardPort
|
|
1206
|
+
},
|
|
1207
|
+
milestones: mergedMilestones,
|
|
1208
|
+
issues: mergedIssues,
|
|
1209
|
+
events: previous?.events ?? [],
|
|
1210
|
+
metrics,
|
|
1211
|
+
notes: previous?.notes ?? [
|
|
1212
|
+
"Local TypeScript runtime bootstrapped.",
|
|
1213
|
+
"Codex-only execution path enabled.",
|
|
1214
|
+
"No external tracker dependency (filesystem-backed local mode)."
|
|
1215
|
+
],
|
|
1216
|
+
variables: previous?.variables ?? []
|
|
1217
|
+
};
|
|
1218
|
+
refreshMilestoneSummaries(state);
|
|
1219
|
+
return state;
|
|
1220
|
+
}
|
|
1221
|
+
function addEvent(state, issueId, kind, message) {
|
|
1222
|
+
const event = {
|
|
1223
|
+
id: `${Date.now()}-${state.events.length + 1}`,
|
|
1224
|
+
issueId,
|
|
1225
|
+
kind,
|
|
1226
|
+
message,
|
|
1227
|
+
at: now()
|
|
1228
|
+
};
|
|
1229
|
+
state.events = [event, ...state.events].slice(0, PERSIST_EVENTS_MAX);
|
|
1230
|
+
markEventDirty(event.id);
|
|
1231
|
+
try {
|
|
1232
|
+
recordEvent();
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
if (issueId) {
|
|
1236
|
+
const issue = state.issues.find((i) => i.id === issueId);
|
|
1237
|
+
if (issue) {
|
|
1238
|
+
issue.eventsCount = (issue.eventsCount || 0) + 1;
|
|
1239
|
+
markIssueDirty(issue.id);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
logger.info({ issueId, kind }, message);
|
|
1243
|
+
}
|
|
1244
|
+
async function transitionIssue(issue, event, context = {}) {
|
|
1245
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, from: issue.state, event, context }, "[State] Issue transition");
|
|
1246
|
+
if (!issueTransitionExecutor) {
|
|
1247
|
+
const { executeTransition } = await import("./fsm-issue-YGGF7SIL.js");
|
|
1248
|
+
await executeTransition(issue, event, { ...context, issue });
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
await issueTransitionExecutor(issue, event, { ...context, issue });
|
|
1252
|
+
}
|
|
1253
|
+
function issueDependenciesResolved(issue, allIssues) {
|
|
1254
|
+
if (issue.blockedBy.length === 0) return true;
|
|
1255
|
+
const map = new Map(allIssues.map((entry) => [entry.id, entry]));
|
|
1256
|
+
return issue.blockedBy.every((dependencyId) => {
|
|
1257
|
+
const dep = map.get(dependencyId);
|
|
1258
|
+
return dep?.state === "Approved" || dep?.state === "Merged";
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
function getNextRetryAt(issue, baseMs) {
|
|
1262
|
+
const nextAttempt = issue.attempts + 1;
|
|
1263
|
+
const nextDelay = withRetryBackoff(nextAttempt, baseMs);
|
|
1264
|
+
return new Date(Date.now() + nextDelay).toISOString();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
export {
|
|
1268
|
+
buildQueueTitle,
|
|
1269
|
+
SETTING_ID_PROJECT_NAME,
|
|
1270
|
+
detectProjectName,
|
|
1271
|
+
resolveProjectMetadata,
|
|
1272
|
+
scanProjectFiles,
|
|
1273
|
+
collectArtifacts,
|
|
1274
|
+
getReferenceRepositoriesRoot,
|
|
1275
|
+
listReferenceRepositories,
|
|
1276
|
+
syncReferenceRepositories,
|
|
1277
|
+
importReferenceArtifacts,
|
|
1278
|
+
record,
|
|
1279
|
+
getHourlySnapshot,
|
|
1280
|
+
hydrate,
|
|
1281
|
+
getAnalytics,
|
|
1282
|
+
deriveConfig,
|
|
1283
|
+
applyWorkflowConfig,
|
|
1284
|
+
validateConfig,
|
|
1285
|
+
normalizeMilestone,
|
|
1286
|
+
normalizeMilestoneStatus,
|
|
1287
|
+
createMilestoneFromPayload,
|
|
1288
|
+
findMilestone,
|
|
1289
|
+
refreshMilestoneSummaries,
|
|
1290
|
+
setIssueTransitionExecutor,
|
|
1291
|
+
getIssueTransitionExecutor,
|
|
1292
|
+
normalizeIssue,
|
|
1293
|
+
nextLocalIssueId,
|
|
1294
|
+
createIssueFromPayload,
|
|
1295
|
+
dedupHistoryEntries,
|
|
1296
|
+
buildRuntimeState,
|
|
1297
|
+
addEvent,
|
|
1298
|
+
transitionIssue,
|
|
1299
|
+
issueDependenciesResolved,
|
|
1300
|
+
getNextRetryAt
|
|
1301
|
+
};
|
|
1302
|
+
//# sourceMappingURL=chunk-E2EWEYA4.js.map
|