issue-flow 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{analyze-7B5TORNK.js → analyze-OEET7KGE.js} +17 -11
- package/dist/analyze-OEET7KGE.js.map +1 -0
- package/dist/{apply-WUJHDD7A.js → apply-ZIA3C52S.js} +6 -4
- package/dist/{apply-WUJHDD7A.js.map → apply-ZIA3C52S.js.map} +1 -1
- package/dist/{chunk-6N7LZZ5U.js → chunk-3QHK4YQA.js} +204 -107
- package/dist/chunk-3QHK4YQA.js.map +1 -0
- package/dist/chunk-42PJFMAR.js +605 -0
- package/dist/chunk-42PJFMAR.js.map +1 -0
- package/dist/{chunk-PL6CNMVS.js → chunk-4JSOYE4G.js} +105 -19
- package/dist/chunk-4JSOYE4G.js.map +1 -0
- package/dist/chunk-56KS3TFQ.js +703 -0
- package/dist/chunk-56KS3TFQ.js.map +1 -0
- package/dist/{chunk-AAY6KM7N.js → chunk-73ATPIYX.js} +160 -154
- package/dist/chunk-73ATPIYX.js.map +1 -0
- package/dist/{chunk-MKD2D7WE.js → chunk-A4SLHOW7.js} +70 -13
- package/dist/chunk-A4SLHOW7.js.map +1 -0
- package/dist/chunk-BAUPQXPE.js +173 -0
- package/dist/chunk-BAUPQXPE.js.map +1 -0
- package/dist/{chunk-HC2MT7BX.js → chunk-BYZDPR7N.js} +14 -581
- package/dist/chunk-BYZDPR7N.js.map +1 -0
- package/dist/chunk-CD5ULRVC.js +615 -0
- package/dist/chunk-CD5ULRVC.js.map +1 -0
- package/dist/chunk-DIDUVL6O.js +1365 -0
- package/dist/chunk-DIDUVL6O.js.map +1 -0
- package/dist/{chunk-47VIN33V.js → chunk-EZATO3DW.js} +7 -5
- package/dist/{chunk-47VIN33V.js.map → chunk-EZATO3DW.js.map} +1 -1
- package/dist/{chunk-JW7WLSZ6.js → chunk-FADTCQGM.js} +48 -12
- package/dist/chunk-FADTCQGM.js.map +1 -0
- package/dist/{chunk-GANSDJV7.js → chunk-FJP3UQGD.js} +15 -13
- package/dist/chunk-FJP3UQGD.js.map +1 -0
- package/dist/chunk-KMMASOA6.js +134 -0
- package/dist/chunk-KMMASOA6.js.map +1 -0
- package/dist/{chunk-HQYXVIH6.js → chunk-LAFNLLQP.js} +3 -3
- package/dist/chunk-LWRSMTTZ.js +18 -0
- package/dist/chunk-LWRSMTTZ.js.map +1 -0
- package/dist/{chunk-MSPUB635.js → chunk-M5UCXTIT.js} +15 -12
- package/dist/chunk-M5UCXTIT.js.map +1 -0
- package/dist/chunk-M66SJHQQ.js +206 -0
- package/dist/chunk-M66SJHQQ.js.map +1 -0
- package/dist/{chunk-OBLVLIUS.js → chunk-MJULIKQU.js} +42 -6
- package/dist/chunk-MJULIKQU.js.map +1 -0
- package/dist/{chunk-RKTEV6EG.js → chunk-NS6R37II.js} +4 -4
- package/dist/{run-H75MD5BQ.js → chunk-NTMMF5UD.js} +515 -544
- package/dist/chunk-NTMMF5UD.js.map +1 -0
- package/dist/{chunk-UBIQXCVE.js → chunk-QCPGHBSZ.js} +16 -13
- package/dist/chunk-QCPGHBSZ.js.map +1 -0
- package/dist/{chunk-LVKQ2AKT.js → chunk-TSOLWK7Y.js} +8 -6
- package/dist/chunk-TSOLWK7Y.js.map +1 -0
- package/dist/{chunk-VCKX262Z.js → chunk-XQUSIEKA.js} +13 -10
- package/dist/chunk-XQUSIEKA.js.map +1 -0
- package/dist/chunk-Y5ZDJ4PJ.js +76 -0
- package/dist/chunk-Y5ZDJ4PJ.js.map +1 -0
- package/dist/cli.js +156 -26
- package/dist/cli.js.map +1 -1
- package/dist/{config-4THHOXNN.js → config-PRJGWW7J.js} +15 -3
- package/dist/execute-XBNVL753.js +18 -0
- package/dist/{generate-6RA2MFJB.js → generate-EGB4KV2B.js} +18 -11
- package/dist/generate-EGB4KV2B.js.map +1 -0
- package/dist/init-AJHCXJBP.js +10 -0
- package/dist/operations-BYT626KQ.js +365 -0
- package/dist/operations-BYT626KQ.js.map +1 -0
- package/dist/plan-QOZ3RIO6.js +21 -0
- package/dist/{policy-XQW5NUIE.js → policy-6S4GFC76.js} +6 -4
- package/dist/{policy-XQW5NUIE.js.map → policy-6S4GFC76.js.map} +1 -1
- package/dist/pr-EBBHD2ZX.js +26 -0
- package/dist/pr-review-VVMW5PO4.js +20 -0
- package/dist/prd-JQSZF7PR.js +21 -0
- package/dist/resume-PPCNQVVV.js +221 -0
- package/dist/resume-PPCNQVVV.js.map +1 -0
- package/dist/review-3F6MARWH.js +19 -0
- package/dist/run-RXE5GDQO.js +36 -0
- package/dist/run-RXE5GDQO.js.map +1 -0
- package/dist/state-manager-2EDVDBAR.js +34 -0
- package/dist/state-manager-2EDVDBAR.js.map +1 -0
- package/dist/{web-ANQQTTRD.js → web-EF5WIIGG.js} +13 -8
- package/dist/{web-ANQQTTRD.js.map → web-EF5WIIGG.js.map} +1 -1
- package/package.json +1 -1
- package/prompts/pr.md +11 -4
- package/web/public/app.js +12 -0
- package/dist/analyze-7B5TORNK.js.map +0 -1
- package/dist/chunk-3BOHTH4V.js +0 -58
- package/dist/chunk-3BOHTH4V.js.map +0 -1
- package/dist/chunk-6N7LZZ5U.js.map +0 -1
- package/dist/chunk-AAY6KM7N.js.map +0 -1
- package/dist/chunk-GANSDJV7.js.map +0 -1
- package/dist/chunk-HC2MT7BX.js.map +0 -1
- package/dist/chunk-HDV3YDTG.js +0 -52
- package/dist/chunk-HDV3YDTG.js.map +0 -1
- package/dist/chunk-JW7WLSZ6.js.map +0 -1
- package/dist/chunk-LVKQ2AKT.js.map +0 -1
- package/dist/chunk-MKD2D7WE.js.map +0 -1
- package/dist/chunk-MSPUB635.js.map +0 -1
- package/dist/chunk-OBLVLIUS.js.map +0 -1
- package/dist/chunk-PL6CNMVS.js.map +0 -1
- package/dist/chunk-UBIQXCVE.js.map +0 -1
- package/dist/chunk-VCKX262Z.js.map +0 -1
- package/dist/chunk-VQ5VN7DF.js +0 -1018
- package/dist/chunk-VQ5VN7DF.js.map +0 -1
- package/dist/execute-5OEKLM5X.js +0 -15
- package/dist/generate-6RA2MFJB.js.map +0 -1
- package/dist/init-PRMOTQ7N.js +0 -9
- package/dist/plan-KENR6MJ3.js +0 -17
- package/dist/pr-KDGKG4BP.js +0 -21
- package/dist/pr-review-CAMAB7KO.js +0 -17
- package/dist/prd-V3MWDIHK.js +0 -17
- package/dist/review-7LN47SAN.js +0 -15
- package/dist/run-H75MD5BQ.js.map +0 -1
- /package/dist/{chunk-HQYXVIH6.js.map → chunk-LAFNLLQP.js.map} +0 -0
- /package/dist/{chunk-RKTEV6EG.js.map → chunk-NS6R37II.js.map} +0 -0
- /package/dist/{config-4THHOXNN.js.map → config-PRJGWW7J.js.map} +0 -0
- /package/dist/{execute-5OEKLM5X.js.map → execute-XBNVL753.js.map} +0 -0
- /package/dist/{init-PRMOTQ7N.js.map → init-AJHCXJBP.js.map} +0 -0
- /package/dist/{plan-KENR6MJ3.js.map → plan-QOZ3RIO6.js.map} +0 -0
- /package/dist/{pr-KDGKG4BP.js.map → pr-EBBHD2ZX.js.map} +0 -0
- /package/dist/{pr-review-CAMAB7KO.js.map → pr-review-VVMW5PO4.js.map} +0 -0
- /package/dist/{prd-V3MWDIHK.js.map → prd-JQSZF7PR.js.map} +0 -0
- /package/dist/{review-7LN47SAN.js.map → review-3F6MARWH.js.map} +0 -0
|
@@ -0,0 +1,1365 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
printInfo
|
|
4
|
+
} from "./chunk-BYZDPR7N.js";
|
|
5
|
+
import {
|
|
6
|
+
isoNow,
|
|
7
|
+
pullRequestRefSchema,
|
|
8
|
+
webConfigSchema,
|
|
9
|
+
writeFileAtomic
|
|
10
|
+
} from "./chunk-CD5ULRVC.js";
|
|
11
|
+
|
|
12
|
+
// src/resilience/errors.ts
|
|
13
|
+
var RETRYABLE_KINDS = /* @__PURE__ */ new Set([
|
|
14
|
+
"network",
|
|
15
|
+
"timeout",
|
|
16
|
+
"stalled",
|
|
17
|
+
"rate_limit",
|
|
18
|
+
"provider_down",
|
|
19
|
+
"provider_crash"
|
|
20
|
+
]);
|
|
21
|
+
var HUMAN_ACTION_KINDS = /* @__PURE__ */ new Set([
|
|
22
|
+
"authentication",
|
|
23
|
+
"configuration",
|
|
24
|
+
"repository_state",
|
|
25
|
+
"task_execution"
|
|
26
|
+
]);
|
|
27
|
+
function isRetryableKind(kind) {
|
|
28
|
+
return RETRYABLE_KINDS.has(kind);
|
|
29
|
+
}
|
|
30
|
+
function requiresHumanAction(kind) {
|
|
31
|
+
return HUMAN_ACTION_KINDS.has(kind);
|
|
32
|
+
}
|
|
33
|
+
var ClassifiedError = class extends Error {
|
|
34
|
+
failure;
|
|
35
|
+
action;
|
|
36
|
+
constructor(message, failure, action) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "ClassifiedError";
|
|
39
|
+
this.failure = failure;
|
|
40
|
+
this.action = action;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function failureOf(error) {
|
|
44
|
+
return error instanceof ClassifiedError ? error.failure : null;
|
|
45
|
+
}
|
|
46
|
+
function actionOf(error) {
|
|
47
|
+
return error instanceof ClassifiedError ? error.action ?? null : null;
|
|
48
|
+
}
|
|
49
|
+
var ERRNO_KINDS = {
|
|
50
|
+
ENOTFOUND: "network",
|
|
51
|
+
EAI_AGAIN: "network",
|
|
52
|
+
ECONNRESET: "network",
|
|
53
|
+
ECONNREFUSED: "network",
|
|
54
|
+
ECONNABORTED: "network",
|
|
55
|
+
EHOSTUNREACH: "network",
|
|
56
|
+
ENETUNREACH: "network",
|
|
57
|
+
ENETDOWN: "network",
|
|
58
|
+
EPIPE: "network",
|
|
59
|
+
ETIMEDOUT: "timeout",
|
|
60
|
+
ENOENT: "configuration",
|
|
61
|
+
EACCES: "configuration",
|
|
62
|
+
EPERM: "configuration"
|
|
63
|
+
};
|
|
64
|
+
function kindFromErrno(errno) {
|
|
65
|
+
if (!errno) return void 0;
|
|
66
|
+
return ERRNO_KINDS[errno.toUpperCase()];
|
|
67
|
+
}
|
|
68
|
+
function kindFromHttpStatus(status, lowered) {
|
|
69
|
+
if (status === void 0 || status < 400) return void 0;
|
|
70
|
+
if (status === 429) return "rate_limit";
|
|
71
|
+
if (status === 401) return "authentication";
|
|
72
|
+
if (status === 403)
|
|
73
|
+
return matchesAny(lowered, RATE_LIMIT_PATTERNS) ? "rate_limit" : "authentication";
|
|
74
|
+
if (status === 408) return "timeout";
|
|
75
|
+
if (status >= 500) return "provider_down";
|
|
76
|
+
return "configuration";
|
|
77
|
+
}
|
|
78
|
+
var CRASH_SIGNALS = /* @__PURE__ */ new Set(["SIGKILL", "SIGABRT", "SIGSEGV", "SIGBUS"]);
|
|
79
|
+
function kindFromTermination(signal) {
|
|
80
|
+
if (signal.stalled === true) return "stalled";
|
|
81
|
+
if (signal.timedOut === true) return "timeout";
|
|
82
|
+
const name = signal.signal?.toUpperCase();
|
|
83
|
+
if (!name) return void 0;
|
|
84
|
+
if (name === "SIGTERM") return "timeout";
|
|
85
|
+
if (CRASH_SIGNALS.has(name)) return "provider_crash";
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
var EXIT_CODE_KINDS = {
|
|
89
|
+
75: "provider_down",
|
|
90
|
+
// EX_TEMPFAIL
|
|
91
|
+
126: "configuration",
|
|
92
|
+
// found, not executable
|
|
93
|
+
127: "configuration",
|
|
94
|
+
// command not found
|
|
95
|
+
137: "provider_crash",
|
|
96
|
+
// 128 + SIGKILL, typically the OOM killer
|
|
97
|
+
139: "provider_crash"
|
|
98
|
+
// 128 + SIGSEGV
|
|
99
|
+
};
|
|
100
|
+
function kindFromExitCode(exitCode) {
|
|
101
|
+
if (exitCode === null || exitCode === void 0) return void 0;
|
|
102
|
+
return EXIT_CODE_KINDS[exitCode];
|
|
103
|
+
}
|
|
104
|
+
var RATE_LIMIT_PATTERNS = [
|
|
105
|
+
"rate limit",
|
|
106
|
+
"rate-limit",
|
|
107
|
+
"ratelimit",
|
|
108
|
+
"too many requests",
|
|
109
|
+
"http 429"
|
|
110
|
+
];
|
|
111
|
+
var TEXT_RULES = [
|
|
112
|
+
{ kind: "rate_limit", patterns: RATE_LIMIT_PATTERNS },
|
|
113
|
+
{
|
|
114
|
+
kind: "authentication",
|
|
115
|
+
patterns: [
|
|
116
|
+
"authentication failed",
|
|
117
|
+
"authentication_error",
|
|
118
|
+
"gh auth login",
|
|
119
|
+
"not logged into",
|
|
120
|
+
"bad credentials",
|
|
121
|
+
"invalid api key",
|
|
122
|
+
"invalid_api_key",
|
|
123
|
+
"token expired",
|
|
124
|
+
"expired token",
|
|
125
|
+
"unauthorized",
|
|
126
|
+
"http 401",
|
|
127
|
+
"http 403"
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
kind: "network",
|
|
132
|
+
patterns: [
|
|
133
|
+
"connection reset",
|
|
134
|
+
"connection refused",
|
|
135
|
+
"connection aborted",
|
|
136
|
+
"network error",
|
|
137
|
+
"network unavailable",
|
|
138
|
+
"temporary failure",
|
|
139
|
+
"econnreset",
|
|
140
|
+
"econnrefused",
|
|
141
|
+
"enotfound",
|
|
142
|
+
"eai_again",
|
|
143
|
+
"socket hang up",
|
|
144
|
+
// What Go prints, and therefore what `gh` prints: its network errors
|
|
145
|
+
// never mention "connection reset", they mention the resolver.
|
|
146
|
+
"no such host",
|
|
147
|
+
"dial tcp",
|
|
148
|
+
"i/o timeout",
|
|
149
|
+
"tls handshake timeout",
|
|
150
|
+
"server misbehaving",
|
|
151
|
+
"error connecting to"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
// Before the timeout rule: a stall *is* a kind of timeout, and the specific
|
|
155
|
+
// reading is the useful one — it has its own budget and its own escalation.
|
|
156
|
+
{ kind: "stalled", patterns: ["produced no output for", "(stalled)"] },
|
|
157
|
+
{ kind: "timeout", patterns: ["timed out", "timeout", "etimedout"] },
|
|
158
|
+
{
|
|
159
|
+
kind: "provider_down",
|
|
160
|
+
patterns: [
|
|
161
|
+
"service unavailable",
|
|
162
|
+
"temporarily unavailable",
|
|
163
|
+
"overloaded",
|
|
164
|
+
"bad gateway",
|
|
165
|
+
"gateway timeout",
|
|
166
|
+
"internal server error",
|
|
167
|
+
"http 500",
|
|
168
|
+
"http 502",
|
|
169
|
+
"http 503",
|
|
170
|
+
"http 504"
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
kind: "provider_crash",
|
|
175
|
+
patterns: ["segmentation fault", "core dumped", "out of memory", "killed by signal"]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
kind: "repository_state",
|
|
179
|
+
patterns: [
|
|
180
|
+
"unmerged paths",
|
|
181
|
+
"needs merge",
|
|
182
|
+
"automatic merge failed",
|
|
183
|
+
"rebase in progress",
|
|
184
|
+
"you are in the middle of",
|
|
185
|
+
"cherry-pick is in progress",
|
|
186
|
+
"not a git repository"
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
kind: "configuration",
|
|
191
|
+
patterns: ["command not found", "unknown flag", "unknown option", "no such file or directory"]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
kind: "task_execution",
|
|
195
|
+
patterns: [
|
|
196
|
+
"test failed",
|
|
197
|
+
"tests failed",
|
|
198
|
+
"failed tests",
|
|
199
|
+
"failing test",
|
|
200
|
+
"assertion failed",
|
|
201
|
+
"assertionerror",
|
|
202
|
+
"expect(received)",
|
|
203
|
+
"typecheck failed",
|
|
204
|
+
"npm err! test failed",
|
|
205
|
+
"lint failed",
|
|
206
|
+
"build failed",
|
|
207
|
+
"compilation failed",
|
|
208
|
+
"syntaxerror",
|
|
209
|
+
"typeerror",
|
|
210
|
+
"referenceerror",
|
|
211
|
+
// What a runner actually prints: "Tests 3 failed | 41 passed",
|
|
212
|
+
// "Test Files 1 failed (12)", "Tests: 2 failed, 8 total".
|
|
213
|
+
/\b\d+\s+failed\b/
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
];
|
|
217
|
+
function matchesAny(lowered, patterns) {
|
|
218
|
+
return patterns.some(
|
|
219
|
+
(pattern) => typeof pattern === "string" ? lowered.includes(pattern) : pattern.test(lowered)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
function kindFromText(lowered) {
|
|
223
|
+
if (!lowered) return void 0;
|
|
224
|
+
for (const rule of TEXT_RULES) {
|
|
225
|
+
if (matchesAny(lowered, rule.patterns)) return rule.kind;
|
|
226
|
+
}
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
var RETRY_AFTER_IN_TEXT = /retry[\s-]?after:?\s*(\d+)/i;
|
|
230
|
+
function parseRetryAfter(value, now) {
|
|
231
|
+
if (value === void 0) return void 0;
|
|
232
|
+
if (typeof value === "number")
|
|
233
|
+
return Number.isFinite(value) && value >= 0 ? value * 1e3 : void 0;
|
|
234
|
+
const trimmed = value.trim();
|
|
235
|
+
if (trimmed === "") return void 0;
|
|
236
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
237
|
+
const at = Date.parse(trimmed);
|
|
238
|
+
if (Number.isNaN(at)) return void 0;
|
|
239
|
+
return at > now ? at - now : void 0;
|
|
240
|
+
}
|
|
241
|
+
function resolveRetryAfterMs(signal, text, now) {
|
|
242
|
+
const explicit = parseRetryAfter(signal.retryAfter, now);
|
|
243
|
+
if (explicit !== void 0) return explicit;
|
|
244
|
+
const inText = RETRY_AFTER_IN_TEXT.exec(text);
|
|
245
|
+
return inText ? Number(inText[1]) * 1e3 : void 0;
|
|
246
|
+
}
|
|
247
|
+
function combineOutput(signal) {
|
|
248
|
+
return [signal.stderr, signal.stdout].filter((part) => part && part.trim() !== "").join("\n");
|
|
249
|
+
}
|
|
250
|
+
function describe(signal, text) {
|
|
251
|
+
const trimmed = text.trim();
|
|
252
|
+
if (trimmed !== "") return trimmed;
|
|
253
|
+
if (signal.errno) return `${signal.source} failed with ${signal.errno}`;
|
|
254
|
+
if (signal.httpStatus !== void 0)
|
|
255
|
+
return `${signal.source} responded HTTP ${signal.httpStatus}`;
|
|
256
|
+
if (signal.timedOut === true) return `${signal.source} timed out`;
|
|
257
|
+
if (signal.signal) return `${signal.source} was terminated by ${signal.signal}`;
|
|
258
|
+
if (signal.exitCode !== null && signal.exitCode !== void 0) {
|
|
259
|
+
return `${signal.source} exited with code ${signal.exitCode}`;
|
|
260
|
+
}
|
|
261
|
+
return `${signal.source} failed for an unreported reason`;
|
|
262
|
+
}
|
|
263
|
+
function classify(signal, options = {}) {
|
|
264
|
+
const now = options.now?.() ?? Date.now();
|
|
265
|
+
const text = combineOutput(signal);
|
|
266
|
+
const lowered = text.toLowerCase();
|
|
267
|
+
const kind = kindFromErrno(signal.errno) ?? kindFromHttpStatus(signal.httpStatus, lowered) ?? kindFromTermination(signal) ?? kindFromExitCode(signal.exitCode) ?? kindFromText(lowered) ?? "unknown";
|
|
268
|
+
const retryAfterMs = resolveRetryAfterMs(signal, text, now);
|
|
269
|
+
return {
|
|
270
|
+
kind,
|
|
271
|
+
message: describe(signal, text),
|
|
272
|
+
retryable: isRetryableKind(kind),
|
|
273
|
+
source: signal.source,
|
|
274
|
+
...retryAfterMs === void 0 ? {} : { retryAfterMs }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/resilience/policy.ts
|
|
279
|
+
var CONFIG_KEYS = {
|
|
280
|
+
network: "network",
|
|
281
|
+
timeout: "timeout",
|
|
282
|
+
stalled: "stalled",
|
|
283
|
+
rate_limit: "rateLimit",
|
|
284
|
+
provider_down: "providerDown",
|
|
285
|
+
provider_crash: "providerCrash",
|
|
286
|
+
authentication: "authentication",
|
|
287
|
+
configuration: "configuration",
|
|
288
|
+
repository_state: "repositoryState",
|
|
289
|
+
task_execution: "taskExecution",
|
|
290
|
+
internal: "internal",
|
|
291
|
+
unknown: "unknown"
|
|
292
|
+
};
|
|
293
|
+
function retryConfigKey(kind) {
|
|
294
|
+
return CONFIG_KEYS[kind];
|
|
295
|
+
}
|
|
296
|
+
var RETRY_CONFIG_KEYS = Object.values(CONFIG_KEYS);
|
|
297
|
+
var SECOND = 1e3;
|
|
298
|
+
function policy(overrides) {
|
|
299
|
+
return {
|
|
300
|
+
maxAttempts: 0,
|
|
301
|
+
initialDelayMs: 0,
|
|
302
|
+
maxDelayMs: 0,
|
|
303
|
+
backoffFactor: 2,
|
|
304
|
+
jitter: "full",
|
|
305
|
+
retryForever: false,
|
|
306
|
+
failover: "never",
|
|
307
|
+
failoverAfterAttempts: 2,
|
|
308
|
+
onExhausted: "fail",
|
|
309
|
+
...overrides
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
var BASE_POLICIES = {
|
|
313
|
+
network: policy({ maxAttempts: 8, initialDelayMs: 2 * SECOND, maxDelayMs: 120 * SECOND }),
|
|
314
|
+
timeout: policy({
|
|
315
|
+
maxAttempts: 2,
|
|
316
|
+
initialDelayMs: 30 * SECOND,
|
|
317
|
+
maxDelayMs: 120 * SECOND,
|
|
318
|
+
failover: "after_attempts"
|
|
319
|
+
}),
|
|
320
|
+
stalled: policy({
|
|
321
|
+
maxAttempts: 2,
|
|
322
|
+
initialDelayMs: 15 * SECOND,
|
|
323
|
+
maxDelayMs: 15 * SECOND,
|
|
324
|
+
failover: "after_attempts"
|
|
325
|
+
}),
|
|
326
|
+
rate_limit: policy({
|
|
327
|
+
maxAttempts: 6,
|
|
328
|
+
initialDelayMs: 60 * SECOND,
|
|
329
|
+
maxDelayMs: 900 * SECOND,
|
|
330
|
+
failover: "after_attempts"
|
|
331
|
+
}),
|
|
332
|
+
provider_down: policy({
|
|
333
|
+
maxAttempts: 4,
|
|
334
|
+
initialDelayMs: 10 * SECOND,
|
|
335
|
+
maxDelayMs: 300 * SECOND,
|
|
336
|
+
failover: "after_attempts"
|
|
337
|
+
}),
|
|
338
|
+
provider_crash: policy({
|
|
339
|
+
maxAttempts: 3,
|
|
340
|
+
initialDelayMs: 5 * SECOND,
|
|
341
|
+
maxDelayMs: 60 * SECOND,
|
|
342
|
+
failover: "after_attempts"
|
|
343
|
+
}),
|
|
344
|
+
authentication: policy({ maxAttempts: 0, onExhausted: "block" }),
|
|
345
|
+
configuration: policy({ maxAttempts: 0 }),
|
|
346
|
+
repository_state: policy({ maxAttempts: 0, onExhausted: "block" }),
|
|
347
|
+
task_execution: policy({ maxAttempts: 0 }),
|
|
348
|
+
internal: policy({ maxAttempts: 2, initialDelayMs: 5 * SECOND, maxDelayMs: 5 * SECOND }),
|
|
349
|
+
unknown: policy({ maxAttempts: 2, initialDelayMs: 5 * SECOND, maxDelayMs: 5 * SECOND })
|
|
350
|
+
};
|
|
351
|
+
var CONTINUOUS_OVERRIDES = {
|
|
352
|
+
network: { retryForever: true },
|
|
353
|
+
timeout: { maxAttempts: 3 },
|
|
354
|
+
stalled: { maxAttempts: 3 },
|
|
355
|
+
rate_limit: { retryForever: true },
|
|
356
|
+
provider_down: { retryForever: true, failover: "after_attempts" },
|
|
357
|
+
provider_crash: { maxAttempts: 5 }
|
|
358
|
+
};
|
|
359
|
+
function resolvePolicy(kind, config = {}) {
|
|
360
|
+
const base = BASE_POLICIES[kind];
|
|
361
|
+
const profile = config.profile === "continuous" ? CONTINUOUS_OVERRIDES[kind] : void 0;
|
|
362
|
+
const user = config.retry?.[retryConfigKey(kind)];
|
|
363
|
+
const resolved = { ...base, ...profile, ...user };
|
|
364
|
+
if (!requiresHumanAction(kind)) return sanitize(resolved);
|
|
365
|
+
return sanitize({
|
|
366
|
+
...resolved,
|
|
367
|
+
maxAttempts: 0,
|
|
368
|
+
retryForever: false,
|
|
369
|
+
failover: kind === "authentication" && config.failoverOnAuth === true ? resolved.failover : "never"
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function sanitize(value) {
|
|
373
|
+
return {
|
|
374
|
+
...value,
|
|
375
|
+
maxAttempts: Math.max(0, Math.trunc(value.maxAttempts)),
|
|
376
|
+
initialDelayMs: Math.max(0, value.initialDelayMs),
|
|
377
|
+
maxDelayMs: Math.max(0, value.maxDelayMs),
|
|
378
|
+
backoffFactor: value.backoffFactor >= 1 ? value.backoffFactor : 1,
|
|
379
|
+
failoverAfterAttempts: Math.max(1, Math.trunc(value.failoverAfterAttempts))
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function shouldRetry(policy2, attemptsMade) {
|
|
383
|
+
if (policy2.maxAttempts <= 0) return false;
|
|
384
|
+
if (policy2.retryForever) return true;
|
|
385
|
+
return attemptsMade < policy2.maxAttempts;
|
|
386
|
+
}
|
|
387
|
+
function computeDelayMs(policy2, attemptsMade, options = {}) {
|
|
388
|
+
if (options.retryAfterMs !== void 0 && options.retryAfterMs >= 0) {
|
|
389
|
+
return Math.round(options.retryAfterMs);
|
|
390
|
+
}
|
|
391
|
+
const exponent = Math.max(0, attemptsMade - 1);
|
|
392
|
+
const growth = policy2.initialDelayMs * policy2.backoffFactor ** exponent;
|
|
393
|
+
const ceiling = Math.min(policy2.maxDelayMs, growth);
|
|
394
|
+
if (ceiling <= 0) return 0;
|
|
395
|
+
if (policy2.jitter === "none") return Math.round(ceiling);
|
|
396
|
+
const random = options.random ?? Math.random;
|
|
397
|
+
return Math.round(random() * ceiling);
|
|
398
|
+
}
|
|
399
|
+
function abortableDelay(ms, options = {}) {
|
|
400
|
+
const { signal } = options;
|
|
401
|
+
if (signal?.aborted === true) return Promise.resolve(false);
|
|
402
|
+
if (ms <= 0) return Promise.resolve(true);
|
|
403
|
+
return new Promise((resolve4) => {
|
|
404
|
+
const onAbort = () => {
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
resolve4(false);
|
|
407
|
+
};
|
|
408
|
+
const timer = setTimeout(() => {
|
|
409
|
+
signal?.removeEventListener("abort", onAbort);
|
|
410
|
+
resolve4(true);
|
|
411
|
+
}, ms);
|
|
412
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/utils/shell.ts
|
|
417
|
+
import { basename } from "path";
|
|
418
|
+
import { execa } from "execa";
|
|
419
|
+
|
|
420
|
+
// src/resilience/retry.ts
|
|
421
|
+
async function withRetry(fn, options) {
|
|
422
|
+
const { policy: policy2, evaluate, signal, onAttempt, random } = options;
|
|
423
|
+
const delay = options.delay ?? abortableDelay;
|
|
424
|
+
const policyFor = typeof policy2 === "function" ? policy2 : () => policy2;
|
|
425
|
+
for (let attempt = 1; ; attempt++) {
|
|
426
|
+
const value = await fn(attempt);
|
|
427
|
+
const failure = evaluate(value, attempt);
|
|
428
|
+
if (failure === null) {
|
|
429
|
+
await onAttempt?.({ attempt, value, failure, willRetry: false, delayMs: 0 });
|
|
430
|
+
return { value, attempts: attempt, failure: null, exhausted: false, aborted: false };
|
|
431
|
+
}
|
|
432
|
+
const active = policyFor(failure);
|
|
433
|
+
const eligible = failure.retryable && !requiresHumanAction(failure.kind);
|
|
434
|
+
const willRetry = eligible && shouldRetry(active, attempt);
|
|
435
|
+
const delayMs = willRetry ? computeDelayMs(active, attempt, { random, retryAfterMs: failure.retryAfterMs }) : 0;
|
|
436
|
+
await onAttempt?.({ attempt, value, failure, willRetry, delayMs });
|
|
437
|
+
if (!willRetry) {
|
|
438
|
+
return { value, attempts: attempt, failure, exhausted: eligible, aborted: false };
|
|
439
|
+
}
|
|
440
|
+
if (!await delay(delayMs, { signal })) {
|
|
441
|
+
return { value, attempts: attempt, failure, exhausted: false, aborted: true };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function fixedBackoffPolicy(maxAttempts, baseSeconds, maxSeconds, overrides = {}) {
|
|
446
|
+
return {
|
|
447
|
+
maxAttempts,
|
|
448
|
+
initialDelayMs: baseSeconds * 1e3,
|
|
449
|
+
maxDelayMs: maxSeconds * 1e3,
|
|
450
|
+
backoffFactor: 2,
|
|
451
|
+
jitter: "none",
|
|
452
|
+
retryForever: false,
|
|
453
|
+
failover: "never",
|
|
454
|
+
failoverAfterAttempts: 2,
|
|
455
|
+
onExhausted: "fail",
|
|
456
|
+
...overrides
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/utils/shell.ts
|
|
461
|
+
var SOURCE_BY_COMMAND = {
|
|
462
|
+
git: "git",
|
|
463
|
+
gh: "github"
|
|
464
|
+
};
|
|
465
|
+
function inferSource(command) {
|
|
466
|
+
return SOURCE_BY_COMMAND[basename(command)] ?? "internal";
|
|
467
|
+
}
|
|
468
|
+
var GIT_GLOBAL_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
469
|
+
"-C",
|
|
470
|
+
"-c",
|
|
471
|
+
"--git-dir",
|
|
472
|
+
"--work-tree",
|
|
473
|
+
"--namespace",
|
|
474
|
+
"--exec-path",
|
|
475
|
+
"--super-prefix"
|
|
476
|
+
]);
|
|
477
|
+
var DESTRUCTIVE_GIT_COMMANDS = {
|
|
478
|
+
rebase: true,
|
|
479
|
+
"cherry-pick": true,
|
|
480
|
+
revert: true,
|
|
481
|
+
merge: true,
|
|
482
|
+
am: true,
|
|
483
|
+
restore: true,
|
|
484
|
+
"filter-branch": true,
|
|
485
|
+
gc: true,
|
|
486
|
+
prune: true,
|
|
487
|
+
"update-ref": true,
|
|
488
|
+
push: ["-f", "--force", "--force-with-lease", "--force-if-includes", "--delete", "--mirror"],
|
|
489
|
+
reset: ["--hard", "--merge", "--keep"],
|
|
490
|
+
clean: ["-f", "--force"],
|
|
491
|
+
checkout: ["-f", "--force", "-B"],
|
|
492
|
+
switch: ["-f", "--force", "--discard-changes"],
|
|
493
|
+
branch: ["-d", "-D", "--delete", "-M"],
|
|
494
|
+
tag: ["-d", "--delete", "-f", "--force"],
|
|
495
|
+
stash: ["drop", "clear", "pop"],
|
|
496
|
+
worktree: ["remove", "prune"],
|
|
497
|
+
submodule: ["deinit"]
|
|
498
|
+
};
|
|
499
|
+
function gitSubcommand(args) {
|
|
500
|
+
for (let index = 0; index < args.length; index++) {
|
|
501
|
+
const arg = args[index];
|
|
502
|
+
if (arg === void 0) continue;
|
|
503
|
+
if (GIT_GLOBAL_OPTIONS_WITH_VALUE.has(arg)) {
|
|
504
|
+
index++;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (arg.startsWith("-")) continue;
|
|
508
|
+
return { name: arg, rest: args.slice(index + 1) };
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
function hasToken(rest, token) {
|
|
513
|
+
if (token.startsWith("--")) {
|
|
514
|
+
return rest.some((arg) => arg === token || arg.startsWith(`${token}=`));
|
|
515
|
+
}
|
|
516
|
+
if (token.startsWith("-")) {
|
|
517
|
+
const letter = token.slice(1);
|
|
518
|
+
return rest.some((arg) => /^-[A-Za-z]+$/.test(arg) && arg.slice(1).includes(letter));
|
|
519
|
+
}
|
|
520
|
+
return rest.includes(token);
|
|
521
|
+
}
|
|
522
|
+
function isRetryableInvocation(command, args) {
|
|
523
|
+
if (basename(command) !== "git") return true;
|
|
524
|
+
const subcommand = gitSubcommand(args);
|
|
525
|
+
if (subcommand === null) return true;
|
|
526
|
+
const rule = DESTRUCTIVE_GIT_COMMANDS[subcommand.name];
|
|
527
|
+
if (rule === void 0) return true;
|
|
528
|
+
if (rule === true) return false;
|
|
529
|
+
return !rule.some((token) => hasToken(subcommand.rest, token));
|
|
530
|
+
}
|
|
531
|
+
function asString(value) {
|
|
532
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
533
|
+
}
|
|
534
|
+
function toFailureSignal(raw, result, source) {
|
|
535
|
+
const fields = raw ?? {};
|
|
536
|
+
const errno = asString(fields.code);
|
|
537
|
+
const signal = asString(fields.signal);
|
|
538
|
+
const stderr = result.stderr.trim() !== "" ? result.stderr : asString(fields.originalMessage) ?? asString(fields.shortMessage) ?? "";
|
|
539
|
+
return {
|
|
540
|
+
source,
|
|
541
|
+
exitCode: result.exitCode,
|
|
542
|
+
stdout: result.stdout,
|
|
543
|
+
stderr,
|
|
544
|
+
...errno === void 0 ? {} : { errno },
|
|
545
|
+
...signal === void 0 ? {} : { signal },
|
|
546
|
+
...fields.timedOut === true ? { timedOut: true } : {}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
async function execOnce(command, args, options) {
|
|
550
|
+
const raw = await execa(command, args, {
|
|
551
|
+
reject: false,
|
|
552
|
+
...options
|
|
553
|
+
});
|
|
554
|
+
return {
|
|
555
|
+
raw,
|
|
556
|
+
result: {
|
|
557
|
+
stdout: raw.stdout?.toString() ?? "",
|
|
558
|
+
stderr: raw.stderr?.toString() ?? "",
|
|
559
|
+
exitCode: raw.exitCode ?? 1
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
async function run(command, args = [], options) {
|
|
564
|
+
const { retry, source, retrySignal, onRetryAttempt, ...execaOptions } = options ?? {};
|
|
565
|
+
if (retry === void 0 || !isRetryableInvocation(command, args)) {
|
|
566
|
+
return (await execOnce(command, args, execaOptions)).result;
|
|
567
|
+
}
|
|
568
|
+
const failureSource = source ?? inferSource(command);
|
|
569
|
+
const outcome = await withRetry(
|
|
570
|
+
async () => {
|
|
571
|
+
const { result, raw } = await execOnce(command, args, execaOptions);
|
|
572
|
+
const failure = result.exitCode === 0 ? null : classify(toFailureSignal(raw, result, failureSource));
|
|
573
|
+
return { result, failure };
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
policy: retry,
|
|
577
|
+
evaluate: (attempt) => attempt.failure,
|
|
578
|
+
...retrySignal === void 0 ? {} : { signal: retrySignal },
|
|
579
|
+
...onRetryAttempt === void 0 ? {} : {
|
|
580
|
+
onAttempt: (info) => onRetryAttempt({
|
|
581
|
+
attempt: info.attempt,
|
|
582
|
+
result: info.value.result,
|
|
583
|
+
failure: info.value.failure,
|
|
584
|
+
willRetry: info.willRetry,
|
|
585
|
+
delayMs: info.delayMs
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
return {
|
|
591
|
+
...outcome.value.result,
|
|
592
|
+
attempts: outcome.attempts,
|
|
593
|
+
...outcome.failure === null ? {} : { failure: outcome.failure }
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/utils/git.ts
|
|
598
|
+
async function getProjectRoot() {
|
|
599
|
+
const result = await run("git", ["rev-parse", "--show-toplevel"]);
|
|
600
|
+
if (result.exitCode !== 0) {
|
|
601
|
+
throw new Error(
|
|
602
|
+
"Not inside a git repository. Please run issue-flow from within a git project."
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
return result.stdout.trim();
|
|
606
|
+
}
|
|
607
|
+
async function getCurrentBranch() {
|
|
608
|
+
const result = await run("git", ["branch", "--show-current"]);
|
|
609
|
+
if (result.exitCode !== 0) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
"Failed to detect git branch. Ensure git is installed and you are inside a repository."
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return result.stdout.trim();
|
|
615
|
+
}
|
|
616
|
+
async function getBaseBranch() {
|
|
617
|
+
const remoteHead = await run("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
|
618
|
+
if (remoteHead.exitCode === 0) {
|
|
619
|
+
const name = remoteHead.stdout.trim().replace(/^origin\//, "");
|
|
620
|
+
if (name) return name;
|
|
621
|
+
}
|
|
622
|
+
for (const candidate of ["main", "master"]) {
|
|
623
|
+
const check = await run("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`]);
|
|
624
|
+
if (check.exitCode === 0) return candidate;
|
|
625
|
+
}
|
|
626
|
+
return "main";
|
|
627
|
+
}
|
|
628
|
+
async function getRemoteUrl(cwd) {
|
|
629
|
+
let result;
|
|
630
|
+
try {
|
|
631
|
+
result = await run("git", ["remote", "get-url", "origin"], { cwd });
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
if (result.exitCode !== 0) return null;
|
|
636
|
+
const url = result.stdout.trim();
|
|
637
|
+
return url === "" ? null : url;
|
|
638
|
+
}
|
|
639
|
+
async function getHeadCommit(cwd) {
|
|
640
|
+
let result;
|
|
641
|
+
try {
|
|
642
|
+
result = await run("git", ["rev-parse", "--short", "HEAD"], { cwd });
|
|
643
|
+
} catch {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
if (result.exitCode !== 0) return null;
|
|
647
|
+
const hash = result.stdout.trim();
|
|
648
|
+
return hash === "" ? null : hash;
|
|
649
|
+
}
|
|
650
|
+
function normalizeRemoteUrl(url) {
|
|
651
|
+
if (typeof url !== "string") return null;
|
|
652
|
+
const trimmed = url.trim();
|
|
653
|
+
if (trimmed === "") return null;
|
|
654
|
+
let authority;
|
|
655
|
+
let path;
|
|
656
|
+
const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(trimmed);
|
|
657
|
+
if (scheme) {
|
|
658
|
+
const rest = trimmed.slice(scheme[0].length);
|
|
659
|
+
const slash = rest.indexOf("/");
|
|
660
|
+
if (slash === -1) return null;
|
|
661
|
+
authority = rest.slice(0, slash);
|
|
662
|
+
path = rest.slice(slash + 1);
|
|
663
|
+
} else {
|
|
664
|
+
const colon = trimmed.indexOf(":");
|
|
665
|
+
if (colon === -1) return null;
|
|
666
|
+
authority = trimmed.slice(0, colon);
|
|
667
|
+
path = trimmed.slice(colon + 1);
|
|
668
|
+
}
|
|
669
|
+
const at = authority.lastIndexOf("@");
|
|
670
|
+
if (at !== -1) authority = authority.slice(at + 1);
|
|
671
|
+
const host = authority.replace(/:\d+$/, "").toLowerCase();
|
|
672
|
+
if (host === "") return null;
|
|
673
|
+
const normalizedPath = path.replace(/[?#].*$/, "").replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
|
|
674
|
+
if (normalizedPath === "") return null;
|
|
675
|
+
return `${host}/${normalizedPath}`;
|
|
676
|
+
}
|
|
677
|
+
function stripRemoteUrlCredentials(url) {
|
|
678
|
+
if (typeof url !== "string") return null;
|
|
679
|
+
const trimmed = url.trim();
|
|
680
|
+
if (trimmed === "") return null;
|
|
681
|
+
const httpScheme = /^https?:\/\//i.exec(trimmed);
|
|
682
|
+
if (!httpScheme) return trimmed;
|
|
683
|
+
const rest = trimmed.slice(httpScheme[0].length);
|
|
684
|
+
const slash = rest.indexOf("/");
|
|
685
|
+
const authority = slash === -1 ? rest : rest.slice(0, slash);
|
|
686
|
+
const remainder = slash === -1 ? "" : rest.slice(slash);
|
|
687
|
+
const at = authority.lastIndexOf("@");
|
|
688
|
+
if (at === -1) return trimmed;
|
|
689
|
+
return `${httpScheme[0]}${authority.slice(at + 1)}${remainder}`;
|
|
690
|
+
}
|
|
691
|
+
async function getCommitsSince(base) {
|
|
692
|
+
const result = await run("git", ["log", "--pretty=format:%h%x09%s", `${base}..HEAD`]);
|
|
693
|
+
if (result.exitCode !== 0) return [];
|
|
694
|
+
return result.stdout.split("\n").filter((line) => line.trim() !== "").map((line) => {
|
|
695
|
+
const tab = line.indexOf(" ");
|
|
696
|
+
return tab === -1 ? { hash: line.trim(), subject: "" } : { hash: line.slice(0, tab), subject: line.slice(tab + 1) };
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
async function refExists(ref, cwd) {
|
|
700
|
+
const result = await run("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
701
|
+
...cwd === void 0 ? {} : { cwd }
|
|
702
|
+
});
|
|
703
|
+
return result.exitCode === 0;
|
|
704
|
+
}
|
|
705
|
+
var SEQUENCER_REFS = [
|
|
706
|
+
{
|
|
707
|
+
ref: "REBASE_HEAD",
|
|
708
|
+
kind: "rebase_in_progress",
|
|
709
|
+
name: "a rebase",
|
|
710
|
+
suggestion: "git rebase --continue (or git rebase --abort)"
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
ref: "MERGE_HEAD",
|
|
714
|
+
kind: "merge_in_progress",
|
|
715
|
+
name: "a merge",
|
|
716
|
+
suggestion: "git merge --continue (or git merge --abort)"
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
ref: "CHERRY_PICK_HEAD",
|
|
720
|
+
kind: "cherry_pick_in_progress",
|
|
721
|
+
name: "a cherry-pick",
|
|
722
|
+
suggestion: "git cherry-pick --continue (or git cherry-pick --abort)"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
ref: "REVERT_HEAD",
|
|
726
|
+
kind: "revert_in_progress",
|
|
727
|
+
name: "a revert",
|
|
728
|
+
suggestion: "git revert --continue (or git revert --abort)"
|
|
729
|
+
}
|
|
730
|
+
];
|
|
731
|
+
async function preflightRepository(options = {}) {
|
|
732
|
+
const { cwd, expectedBranch, intent = "new-phase" } = options;
|
|
733
|
+
const at = cwd === void 0 ? {} : { cwd };
|
|
734
|
+
const blocks = [];
|
|
735
|
+
for (const sequencer of SEQUENCER_REFS) {
|
|
736
|
+
if (await refExists(sequencer.ref, cwd)) {
|
|
737
|
+
blocks.push({
|
|
738
|
+
kind: sequencer.kind,
|
|
739
|
+
message: `The repository is in the middle of ${sequencer.name} (${sequencer.ref} is present).`,
|
|
740
|
+
suggestion: sequencer.suggestion
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const unmerged = await run("git", ["diff", "--name-only", "--diff-filter=U"], at);
|
|
745
|
+
const conflicted = unmerged.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
746
|
+
if (conflicted.length > 0) {
|
|
747
|
+
blocks.push({
|
|
748
|
+
kind: "unmerged_paths",
|
|
749
|
+
message: `${conflicted.length} file(s) still have unresolved conflicts: ${conflicted.slice(0, 5).join(", ")}${conflicted.length > 5 ? ", \u2026" : ""}.`,
|
|
750
|
+
suggestion: "Resolve them, then git add the files"
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
const symbolic = await run("git", ["symbolic-ref", "-q", "HEAD"], at);
|
|
754
|
+
const detached = symbolic.exitCode !== 0;
|
|
755
|
+
const branch = detached ? null : symbolic.stdout.trim().replace(/^refs\/heads\//, "");
|
|
756
|
+
if (detached) {
|
|
757
|
+
blocks.push({
|
|
758
|
+
kind: "detached_head",
|
|
759
|
+
message: "HEAD is detached, so a commit would belong to no branch.",
|
|
760
|
+
suggestion: `git switch ${expectedBranch ?? "<branch>"}`
|
|
761
|
+
});
|
|
762
|
+
} else if (expectedBranch !== void 0 && expectedBranch !== null && expectedBranch !== "" && branch !== expectedBranch) {
|
|
763
|
+
blocks.push({
|
|
764
|
+
kind: "branch_mismatch",
|
|
765
|
+
message: `The plan works on '${expectedBranch}' but the repository is on '${branch}'.`,
|
|
766
|
+
suggestion: `git switch ${expectedBranch}`
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
const status = await run("git", ["status", "--porcelain"], at);
|
|
770
|
+
const dirty = status.stdout.trim() !== "";
|
|
771
|
+
if (dirty && intent !== "resume-same-phase") {
|
|
772
|
+
blocks.push({
|
|
773
|
+
kind: "dirty_tree",
|
|
774
|
+
message: "The working tree has uncommitted changes that do not belong to this phase.",
|
|
775
|
+
suggestion: "Commit or stash them yourself, then run again"
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
return { ok: blocks.length === 0, blocks, branch, dirty };
|
|
779
|
+
}
|
|
780
|
+
function describePreflight(result) {
|
|
781
|
+
return result.blocks.map((block) => `${block.message} Suggested: ${block.suggestion}`);
|
|
782
|
+
}
|
|
783
|
+
var STORY_ID_IN_SUBJECT = /\bUS-\d+\b/g;
|
|
784
|
+
async function committedStoryIds(baseBranch, cwd) {
|
|
785
|
+
const at = cwd === void 0 ? {} : { cwd };
|
|
786
|
+
const range = baseBranch === "" ? "HEAD" : `${baseBranch}..HEAD`;
|
|
787
|
+
const result = await run("git", ["log", range, "--format=%s"], at);
|
|
788
|
+
if (result.exitCode !== 0) return /* @__PURE__ */ new Set();
|
|
789
|
+
const ids = /* @__PURE__ */ new Set();
|
|
790
|
+
for (const subject of result.stdout.split("\n")) {
|
|
791
|
+
for (const match of subject.matchAll(STORY_ID_IN_SUBJECT)) {
|
|
792
|
+
ids.add(match[0]);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return ids;
|
|
796
|
+
}
|
|
797
|
+
async function isWorkingTreeClean(cwd) {
|
|
798
|
+
const at = cwd === void 0 ? {} : { cwd };
|
|
799
|
+
const result = await run("git", ["status", "--porcelain"], at);
|
|
800
|
+
if (result.exitCode !== 0) return false;
|
|
801
|
+
return result.stdout.trim() === "";
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/storage/paths.ts
|
|
805
|
+
import { createHash } from "crypto";
|
|
806
|
+
import { homedir } from "os";
|
|
807
|
+
import { basename as basename2, join, resolve } from "path";
|
|
808
|
+
var GLOBAL_DIR_NAME = ".issue-flow";
|
|
809
|
+
var GLOBAL_ROOT_ENV = "ISSUE_FLOW_HOME";
|
|
810
|
+
var PROJECTS_DIR_NAME = "projects";
|
|
811
|
+
var ISSUES_DIR_NAME = "issues";
|
|
812
|
+
var QUEUES_DIR_NAME = "queues";
|
|
813
|
+
var RUN_LOCK_FILENAME = "run.lock";
|
|
814
|
+
function getGlobalRoot(options = {}) {
|
|
815
|
+
const env = options.env ?? process.env;
|
|
816
|
+
const override = env[GLOBAL_ROOT_ENV]?.trim();
|
|
817
|
+
if (override) {
|
|
818
|
+
return resolve(override);
|
|
819
|
+
}
|
|
820
|
+
let home;
|
|
821
|
+
try {
|
|
822
|
+
home = homedir();
|
|
823
|
+
} catch {
|
|
824
|
+
home = "";
|
|
825
|
+
}
|
|
826
|
+
if (home.trim() === "") {
|
|
827
|
+
throw new Error(
|
|
828
|
+
`Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
return join(home, GLOBAL_DIR_NAME);
|
|
832
|
+
}
|
|
833
|
+
var SLUG_MAX_LENGTH = 32;
|
|
834
|
+
var HASH_LENGTH = 12;
|
|
835
|
+
var FALLBACK_SLUG = "project";
|
|
836
|
+
function slugify(name) {
|
|
837
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
838
|
+
return slug === "" ? FALLBACK_SLUG : slug;
|
|
839
|
+
}
|
|
840
|
+
function projectIdFromRemote(remote, projectRoot) {
|
|
841
|
+
let seed;
|
|
842
|
+
let name;
|
|
843
|
+
if (remote) {
|
|
844
|
+
seed = `remote:${remote}`;
|
|
845
|
+
name = remote.split("/").pop() ?? "";
|
|
846
|
+
} else {
|
|
847
|
+
const absolute = resolve(projectRoot);
|
|
848
|
+
seed = `path:${absolute}`;
|
|
849
|
+
name = basename2(absolute);
|
|
850
|
+
}
|
|
851
|
+
const hash = createHash("sha256").update(seed).digest("hex").slice(0, HASH_LENGTH);
|
|
852
|
+
return `${slugify(name)}-${hash}`;
|
|
853
|
+
}
|
|
854
|
+
function getProjectDir(projectId, options = {}) {
|
|
855
|
+
return join(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);
|
|
856
|
+
}
|
|
857
|
+
function getQueuePaths(projectId, queueId, options = {}) {
|
|
858
|
+
const queueDir = join(
|
|
859
|
+
getProjectDir(projectId, options),
|
|
860
|
+
QUEUES_DIR_NAME,
|
|
861
|
+
normalizeIssueNumber(queueId)
|
|
862
|
+
);
|
|
863
|
+
return { queueDir, planFile: join(queueDir, "execution-plan.json") };
|
|
864
|
+
}
|
|
865
|
+
function normalizeIssueNumber(issueNumber) {
|
|
866
|
+
const normalized = String(issueNumber).trim().replace(/^#/, "");
|
|
867
|
+
if (normalized.length === 0) {
|
|
868
|
+
throw new Error("Issue identifier cannot be empty");
|
|
869
|
+
}
|
|
870
|
+
if (/[/\\]/.test(normalized) || normalized === "." || normalized === "..") {
|
|
871
|
+
throw new Error(`Invalid issue identifier: '${issueNumber}'`);
|
|
872
|
+
}
|
|
873
|
+
return normalized;
|
|
874
|
+
}
|
|
875
|
+
function getIssuePaths(projectId, issueNumber, options = {}) {
|
|
876
|
+
const issueDir = join(
|
|
877
|
+
getProjectDir(projectId, options),
|
|
878
|
+
ISSUES_DIR_NAME,
|
|
879
|
+
normalizeIssueNumber(issueNumber)
|
|
880
|
+
);
|
|
881
|
+
return {
|
|
882
|
+
issueDir,
|
|
883
|
+
issueFile: join(issueDir, "issue.md"),
|
|
884
|
+
metadataFile: join(issueDir, "metadata.json"),
|
|
885
|
+
prdFile: join(issueDir, "prd.md"),
|
|
886
|
+
tasksFile: join(issueDir, "tasks.json"),
|
|
887
|
+
progressFile: join(issueDir, "progress.txt"),
|
|
888
|
+
analysisFile: join(issueDir, "analysis.md"),
|
|
889
|
+
sessionFile: join(issueDir, "session.json"),
|
|
890
|
+
// The journal, and the single generation kept when it rotates. The
|
|
891
|
+
// snapshot above is the projection; this pair is the history.
|
|
892
|
+
eventsFile: join(issueDir, "events.jsonl"),
|
|
893
|
+
rotatedEventsFile: join(issueDir, "events.1.jsonl"),
|
|
894
|
+
// "This issue looks larger than one run": written only when the signals
|
|
895
|
+
// agree, and read by a person rather than by the pipeline.
|
|
896
|
+
decompositionFile: join(issueDir, "decomposition.md"),
|
|
897
|
+
lastBranchFile: join(issueDir, ".last-branch"),
|
|
898
|
+
archiveDir: join(issueDir, "archive"),
|
|
899
|
+
prReviewDir: join(issueDir, "pr-review")
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/storage/schemas.ts
|
|
904
|
+
import { z } from "zod";
|
|
905
|
+
var STORAGE_SCHEMA_VERSION = 1;
|
|
906
|
+
var userStoryNumberingSourceSchema = z.enum(["history", "start-us", "none"]);
|
|
907
|
+
var userStoryNumberingDecisionSchema = z.object({
|
|
908
|
+
/** The `US-NNN` number the `plan` prompt was told to continue from. */
|
|
909
|
+
nextNumber: z.number().int().positive(),
|
|
910
|
+
source: userStoryNumberingSourceSchema,
|
|
911
|
+
/** Issue the decision was made for. */
|
|
912
|
+
issueNumber: z.string().min(1),
|
|
913
|
+
decidedAt: z.string().min(1),
|
|
914
|
+
/** Human-readable origin, e.g. the previous story id and issue it came from. */
|
|
915
|
+
detail: z.string().optional()
|
|
916
|
+
});
|
|
917
|
+
var projectMetadataSchema = z.object({
|
|
918
|
+
schemaVersion: z.number().int().positive(),
|
|
919
|
+
projectId: z.string().min(1),
|
|
920
|
+
root: z.string().min(1),
|
|
921
|
+
/** Normalized remote (`host/org/repo`), or null when the project has none. */
|
|
922
|
+
remoteUrl: z.string().nullable(),
|
|
923
|
+
createdAt: z.string().min(1),
|
|
924
|
+
updatedAt: z.string().min(1),
|
|
925
|
+
/** Null until the project is used by a pipeline run. */
|
|
926
|
+
lastAttemptAt: z.string().nullable(),
|
|
927
|
+
/**
|
|
928
|
+
* Most recent User Story numbering decision made by `plan` (issue #36).
|
|
929
|
+
* Absent on a project whose `plan` never ran through the numbering
|
|
930
|
+
* resolver, or on a `metadata.json` written before the feature existed.
|
|
931
|
+
*/
|
|
932
|
+
userStoryNumbering: userStoryNumberingDecisionSchema.optional()
|
|
933
|
+
});
|
|
934
|
+
var webShape = webConfigSchema.shape;
|
|
935
|
+
var globalWebConfigSchema = z.object({
|
|
936
|
+
port: webShape.port.unwrap(),
|
|
937
|
+
host: webShape.host.unwrap(),
|
|
938
|
+
refreshSeconds: webShape.refreshSeconds.unwrap(),
|
|
939
|
+
logLimit: webShape.logLimit.unwrap()
|
|
940
|
+
}).partial();
|
|
941
|
+
var globalRetryConfigSchema = z.object({
|
|
942
|
+
retryLimit: z.number().int().nonnegative(),
|
|
943
|
+
retryForever: z.boolean(),
|
|
944
|
+
backoffBaseSeconds: z.number().nonnegative(),
|
|
945
|
+
backoffMaxSeconds: z.number().nonnegative()
|
|
946
|
+
}).partial();
|
|
947
|
+
var resilienceProfileSchema = z.enum([
|
|
948
|
+
"default",
|
|
949
|
+
"continuous"
|
|
950
|
+
]);
|
|
951
|
+
var jitterSchema = z.enum(["none", "full"]);
|
|
952
|
+
var failoverModeSchema = z.enum([
|
|
953
|
+
"never",
|
|
954
|
+
"after_attempts",
|
|
955
|
+
"immediate"
|
|
956
|
+
]);
|
|
957
|
+
var exhaustedActionSchema = z.enum(["fail", "block"]);
|
|
958
|
+
var retryPolicyOverrideSchema = z.object({
|
|
959
|
+
maxAttempts: z.number().int().nonnegative(),
|
|
960
|
+
initialDelayMs: z.number().nonnegative(),
|
|
961
|
+
maxDelayMs: z.number().nonnegative(),
|
|
962
|
+
backoffFactor: z.number().min(1),
|
|
963
|
+
jitter: jitterSchema,
|
|
964
|
+
retryForever: z.boolean(),
|
|
965
|
+
failover: failoverModeSchema,
|
|
966
|
+
failoverAfterAttempts: z.number().int().positive(),
|
|
967
|
+
onExhausted: exhaustedActionSchema
|
|
968
|
+
}).partial();
|
|
969
|
+
var retryOverridesByKind = {
|
|
970
|
+
network: retryPolicyOverrideSchema,
|
|
971
|
+
timeout: retryPolicyOverrideSchema,
|
|
972
|
+
stalled: retryPolicyOverrideSchema,
|
|
973
|
+
rateLimit: retryPolicyOverrideSchema,
|
|
974
|
+
providerDown: retryPolicyOverrideSchema,
|
|
975
|
+
providerCrash: retryPolicyOverrideSchema,
|
|
976
|
+
authentication: retryPolicyOverrideSchema,
|
|
977
|
+
configuration: retryPolicyOverrideSchema,
|
|
978
|
+
repositoryState: retryPolicyOverrideSchema,
|
|
979
|
+
taskExecution: retryPolicyOverrideSchema,
|
|
980
|
+
internal: retryPolicyOverrideSchema,
|
|
981
|
+
unknown: retryPolicyOverrideSchema
|
|
982
|
+
};
|
|
983
|
+
var resilienceRetryConfigSchema = z.object(retryOverridesByKind).partial();
|
|
984
|
+
var resilienceProvidersConfigSchema = z.object({
|
|
985
|
+
failover: z.boolean(),
|
|
986
|
+
chain: z.array(z.string().min(1)),
|
|
987
|
+
cooldownMs: z.number().nonnegative()
|
|
988
|
+
}).partial();
|
|
989
|
+
var resilienceQueueConfigSchema = z.object({
|
|
990
|
+
onIssueFailure: z.enum(["stop", "skip", "block"]),
|
|
991
|
+
maxIssueAttempts: z.number().int().positive()
|
|
992
|
+
}).partial();
|
|
993
|
+
var resilienceWatchdogConfigSchema = z.object({
|
|
994
|
+
inactivityTimeoutMs: z.number().int().nonnegative()
|
|
995
|
+
}).partial();
|
|
996
|
+
var resilienceJournalConfigSchema = z.object({
|
|
997
|
+
enabled: z.boolean(),
|
|
998
|
+
maxFileBytes: z.number().int().positive()
|
|
999
|
+
}).partial();
|
|
1000
|
+
var resilienceDecomposeConfigSchema = z.object({
|
|
1001
|
+
auto: z.boolean()
|
|
1002
|
+
}).partial();
|
|
1003
|
+
var resilienceConfigSchema = z.object({
|
|
1004
|
+
profile: resilienceProfileSchema,
|
|
1005
|
+
/** Whether a credential failure may migrate to another provider. */
|
|
1006
|
+
failoverOnAuth: z.boolean(),
|
|
1007
|
+
retry: resilienceRetryConfigSchema,
|
|
1008
|
+
providers: resilienceProvidersConfigSchema,
|
|
1009
|
+
queue: resilienceQueueConfigSchema,
|
|
1010
|
+
watchdog: resilienceWatchdogConfigSchema,
|
|
1011
|
+
journal: resilienceJournalConfigSchema,
|
|
1012
|
+
decompose: resilienceDecomposeConfigSchema
|
|
1013
|
+
}).partial();
|
|
1014
|
+
var globalCommitConfigSchema = z.object({
|
|
1015
|
+
signoff: z.boolean(),
|
|
1016
|
+
conventional: z.boolean()
|
|
1017
|
+
}).partial();
|
|
1018
|
+
var globalConfigSchema = z.object({
|
|
1019
|
+
schemaVersion: z.number().int().positive(),
|
|
1020
|
+
/** Override for the directory holding `projects/`. */
|
|
1021
|
+
storageDir: z.string().min(1),
|
|
1022
|
+
web: globalWebConfigSchema,
|
|
1023
|
+
retry: globalRetryConfigSchema,
|
|
1024
|
+
commit: globalCommitConfigSchema,
|
|
1025
|
+
resilience: resilienceConfigSchema
|
|
1026
|
+
}).partial();
|
|
1027
|
+
var webLockSchema = z.object({
|
|
1028
|
+
pid: z.number().int().positive(),
|
|
1029
|
+
port: z.number().int().positive(),
|
|
1030
|
+
host: z.string().min(1),
|
|
1031
|
+
startedAt: z.string().min(1)
|
|
1032
|
+
});
|
|
1033
|
+
var runLockSchema = z.object({
|
|
1034
|
+
pid: z.number().int().positive(),
|
|
1035
|
+
/** `os.hostname()`. A pid only means something on the host that wrote it. */
|
|
1036
|
+
host: z.string().min(1),
|
|
1037
|
+
/** The issue (or queue) identifier the owner is running. */
|
|
1038
|
+
target: z.string().min(1),
|
|
1039
|
+
startedAt: z.string().min(1),
|
|
1040
|
+
lastHeartbeatAt: z.string().min(1)
|
|
1041
|
+
});
|
|
1042
|
+
var executionPlanIssueSchema = z.object({
|
|
1043
|
+
id: z.string().min(1),
|
|
1044
|
+
number: z.number().int().positive().nullable(),
|
|
1045
|
+
title: z.string(),
|
|
1046
|
+
url: z.string().nullable(),
|
|
1047
|
+
source: z.string().min(1),
|
|
1048
|
+
position: z.number().int().positive(),
|
|
1049
|
+
// `blocked` and `skipped` are additive: no plan written before them can
|
|
1050
|
+
// carry one, and a reader that does not know them was never given one.
|
|
1051
|
+
status: z.enum(["pending", "in_progress", "completed", "failed", "blocked", "skipped"]),
|
|
1052
|
+
origin: z.enum(["requested", "discovered"]),
|
|
1053
|
+
dependsOn: z.array(z.string()).default([]),
|
|
1054
|
+
parent: z.string().nullable().default(null),
|
|
1055
|
+
priority: z.enum(["high", "medium", "low"]).nullable().default(null),
|
|
1056
|
+
heuristic: z.boolean().default(false),
|
|
1057
|
+
failedPhase: z.string().nullable().default(null),
|
|
1058
|
+
lastError: z.object({ category: z.string(), message: z.string(), at: z.string() }).nullable().default(null),
|
|
1059
|
+
startedAt: z.string().nullable().default(null),
|
|
1060
|
+
completedAt: z.string().nullable().default(null),
|
|
1061
|
+
// Both default, so an execution-plan.json written by an earlier release
|
|
1062
|
+
// parses unchanged and reads as "never attempted, not blocked".
|
|
1063
|
+
attempts: z.number().int().min(0).default(0),
|
|
1064
|
+
blockedReason: z.string().nullable().default(null)
|
|
1065
|
+
});
|
|
1066
|
+
var executionPlanSchema = z.object({
|
|
1067
|
+
schemaVersion: z.literal(1),
|
|
1068
|
+
id: z.string().min(1),
|
|
1069
|
+
project: z.string().min(1),
|
|
1070
|
+
requested: z.array(z.string().min(1)),
|
|
1071
|
+
branchName: z.string().nullable().default(null),
|
|
1072
|
+
noBranch: z.boolean().default(false),
|
|
1073
|
+
prReview: z.boolean().default(false),
|
|
1074
|
+
status: z.enum(["pending", "in_progress", "completed", "failed"]),
|
|
1075
|
+
createdAt: z.string().min(1),
|
|
1076
|
+
updatedAt: z.string().min(1),
|
|
1077
|
+
truncated: z.boolean().default(false),
|
|
1078
|
+
issues: z.array(executionPlanIssueSchema),
|
|
1079
|
+
excluded: z.array(
|
|
1080
|
+
z.object({
|
|
1081
|
+
id: z.string().min(1),
|
|
1082
|
+
number: z.number().int().positive().nullable(),
|
|
1083
|
+
title: z.string(),
|
|
1084
|
+
url: z.string().nullable(),
|
|
1085
|
+
reason: z.string()
|
|
1086
|
+
})
|
|
1087
|
+
).default([]),
|
|
1088
|
+
pullRequest: pullRequestRefSchema.optional()
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// src/storage/resolve.ts
|
|
1092
|
+
import { stat as stat2 } from "fs/promises";
|
|
1093
|
+
import { basename as basename3, join as join3, resolve as resolve3 } from "path";
|
|
1094
|
+
|
|
1095
|
+
// src/storage/compat.ts
|
|
1096
|
+
import { copyFile, mkdir, readdir, readFile, stat } from "fs/promises";
|
|
1097
|
+
import { join as join2, resolve as resolve2, sep } from "path";
|
|
1098
|
+
var LEGACY_ISSUES_DIR_NAME = "issues";
|
|
1099
|
+
var METADATA_FILENAME = "metadata.json";
|
|
1100
|
+
async function directoryExists(path) {
|
|
1101
|
+
try {
|
|
1102
|
+
return (await stat(path)).isDirectory();
|
|
1103
|
+
} catch {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function pathExists(path) {
|
|
1108
|
+
try {
|
|
1109
|
+
await stat(path);
|
|
1110
|
+
return true;
|
|
1111
|
+
} catch {
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function resolveStorageMode(projectRoot, options = {}) {
|
|
1116
|
+
const remoteUrl = normalizeRemoteUrl(await getRemoteUrl(projectRoot));
|
|
1117
|
+
const projectId = projectIdFromRemote(remoteUrl, projectRoot);
|
|
1118
|
+
const globalDir = getProjectDir(projectId, options);
|
|
1119
|
+
const legacyDir = join2(resolve2(projectRoot), LEGACY_ISSUES_DIR_NAME);
|
|
1120
|
+
const [globalExists, legacyExists] = await Promise.all([
|
|
1121
|
+
directoryExists(globalDir),
|
|
1122
|
+
directoryExists(legacyDir)
|
|
1123
|
+
]);
|
|
1124
|
+
return {
|
|
1125
|
+
// Only one state calls for work: legacy content that has nowhere to be read
|
|
1126
|
+
// from yet. Everything else — including "neither exists" — is already the
|
|
1127
|
+
// global layout.
|
|
1128
|
+
mode: legacyExists && !globalExists ? "needs-migration" : "global",
|
|
1129
|
+
projectId,
|
|
1130
|
+
globalDir,
|
|
1131
|
+
legacyDir,
|
|
1132
|
+
globalExists,
|
|
1133
|
+
legacyExists,
|
|
1134
|
+
remoteUrl
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
function migrationFailure(issue, relativePath, err) {
|
|
1138
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1139
|
+
return new Error(
|
|
1140
|
+
`Failed to migrate '${relativePath}' (issue '${issue}'): ${reason}. The legacy directory was left untouched \u2014 fix the cause and run the migration again.`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
function issueOf(relativePath) {
|
|
1144
|
+
return relativePath.split(sep)[0] || ".";
|
|
1145
|
+
}
|
|
1146
|
+
async function copyTree(sourceRoot, targetRoot, relativeDir, result) {
|
|
1147
|
+
const source = join2(sourceRoot, relativeDir);
|
|
1148
|
+
const target = join2(targetRoot, relativeDir);
|
|
1149
|
+
let entries;
|
|
1150
|
+
try {
|
|
1151
|
+
entries = await readdir(source, { withFileTypes: true });
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
throw migrationFailure(issueOf(relativeDir), relativeDir || ".", err);
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
await mkdir(target, { recursive: true });
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
throw migrationFailure(issueOf(relativeDir), relativeDir || ".", err);
|
|
1159
|
+
}
|
|
1160
|
+
for (const entry of entries) {
|
|
1161
|
+
const relativePath = join2(relativeDir, entry.name);
|
|
1162
|
+
if (entry.isDirectory()) {
|
|
1163
|
+
await copyTree(sourceRoot, targetRoot, relativePath, result);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
if (!entry.isFile()) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const to = join2(targetRoot, relativePath);
|
|
1170
|
+
try {
|
|
1171
|
+
if (await pathExists(to)) {
|
|
1172
|
+
result.skipped.push(relativePath);
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
await copyFile(join2(sourceRoot, relativePath), to);
|
|
1176
|
+
result.copied.push(relativePath);
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
throw migrationFailure(issueOf(relativePath), relativePath, err);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async function readExistingMetadata(file) {
|
|
1183
|
+
let raw;
|
|
1184
|
+
try {
|
|
1185
|
+
raw = await readFile(file, "utf-8");
|
|
1186
|
+
} catch {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
const parsed = projectMetadataSchema.safeParse(JSON.parse(raw));
|
|
1191
|
+
return parsed.success ? parsed.data : null;
|
|
1192
|
+
} catch {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
async function migrateLegacyStorage(projectRoot, options = {}) {
|
|
1197
|
+
const status = await resolveStorageMode(projectRoot, { env: options.env });
|
|
1198
|
+
const result = { copied: [], skipped: [] };
|
|
1199
|
+
try {
|
|
1200
|
+
await mkdir(status.globalDir, { recursive: true });
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
throw migrationFailure(".", status.globalDir, err);
|
|
1203
|
+
}
|
|
1204
|
+
if (status.legacyExists) {
|
|
1205
|
+
await copyTree(status.legacyDir, join2(status.globalDir, ISSUES_DIR_NAME), "", result);
|
|
1206
|
+
}
|
|
1207
|
+
const now = (options.now ?? isoNow)();
|
|
1208
|
+
const metadataFile = join2(status.globalDir, METADATA_FILENAME);
|
|
1209
|
+
const existing = await readExistingMetadata(metadataFile);
|
|
1210
|
+
const metadata = {
|
|
1211
|
+
schemaVersion: STORAGE_SCHEMA_VERSION,
|
|
1212
|
+
projectId: status.projectId,
|
|
1213
|
+
root: resolve2(projectRoot),
|
|
1214
|
+
remoteUrl: status.remoteUrl,
|
|
1215
|
+
// The project was first seen whenever it was first written, not now.
|
|
1216
|
+
createdAt: existing?.createdAt ?? now,
|
|
1217
|
+
updatedAt: now,
|
|
1218
|
+
lastAttemptAt: existing?.lastAttemptAt ?? null
|
|
1219
|
+
};
|
|
1220
|
+
await writeFileAtomic(metadataFile, `${JSON.stringify(metadata, null, 2)}
|
|
1221
|
+
`);
|
|
1222
|
+
return {
|
|
1223
|
+
projectId: status.projectId,
|
|
1224
|
+
globalDir: status.globalDir,
|
|
1225
|
+
legacyDir: status.legacyDir,
|
|
1226
|
+
previousMode: status.mode,
|
|
1227
|
+
...result,
|
|
1228
|
+
metadataFile,
|
|
1229
|
+
metadata
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
async function recordUserStoryNumbering(projectRoot, decision, options = {}) {
|
|
1233
|
+
const status = await resolveStorageMode(projectRoot, { env: options.env });
|
|
1234
|
+
const metadataFile = join2(status.globalDir, METADATA_FILENAME);
|
|
1235
|
+
const existing = await readExistingMetadata(metadataFile);
|
|
1236
|
+
const now = (options.now ?? isoNow)();
|
|
1237
|
+
const metadata = {
|
|
1238
|
+
schemaVersion: STORAGE_SCHEMA_VERSION,
|
|
1239
|
+
projectId: status.projectId,
|
|
1240
|
+
root: resolve2(projectRoot),
|
|
1241
|
+
remoteUrl: status.remoteUrl,
|
|
1242
|
+
createdAt: existing?.createdAt ?? now,
|
|
1243
|
+
updatedAt: now,
|
|
1244
|
+
lastAttemptAt: existing?.lastAttemptAt ?? null,
|
|
1245
|
+
userStoryNumbering: decision
|
|
1246
|
+
};
|
|
1247
|
+
await mkdir(status.globalDir, { recursive: true });
|
|
1248
|
+
await writeFileAtomic(metadataFile, `${JSON.stringify(metadata, null, 2)}
|
|
1249
|
+
`);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/storage/resolve.ts
|
|
1253
|
+
var projectCache = /* @__PURE__ */ new Map();
|
|
1254
|
+
var checkedIssues = /* @__PURE__ */ new Set();
|
|
1255
|
+
async function directoryExists2(path) {
|
|
1256
|
+
try {
|
|
1257
|
+
return (await stat2(path)).isDirectory();
|
|
1258
|
+
} catch {
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function announceMigration(result) {
|
|
1263
|
+
if (result.copied.length === 0) return;
|
|
1264
|
+
const count = result.copied.length;
|
|
1265
|
+
const target = join3(result.globalDir, ISSUES_DIR_NAME);
|
|
1266
|
+
printInfo(
|
|
1267
|
+
`Migrated ${count} file${count === 1 ? "" : "s"} from ${result.legacyDir} to ${target}`
|
|
1268
|
+
);
|
|
1269
|
+
printInfo(
|
|
1270
|
+
`The legacy directory was not modified or removed \u2014 ${result.legacyDir} is kept as-is, read-only, for compatibility.`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
async function resolveProject(projectRoot, options) {
|
|
1274
|
+
const status = await resolveStorageMode(projectRoot, options);
|
|
1275
|
+
if (status.mode === "needs-migration") {
|
|
1276
|
+
announceMigration(await migrateLegacyStorage(projectRoot, options));
|
|
1277
|
+
}
|
|
1278
|
+
return {
|
|
1279
|
+
projectId: status.projectId,
|
|
1280
|
+
legacyDir: status.legacyDir
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function getProjectResolution(projectRoot, options) {
|
|
1284
|
+
const key = `${getGlobalRoot(options)}::${projectRoot}`;
|
|
1285
|
+
const cached = projectCache.get(key);
|
|
1286
|
+
if (cached) return cached;
|
|
1287
|
+
const pending = resolveProject(projectRoot, options).catch((err) => {
|
|
1288
|
+
projectCache.delete(key);
|
|
1289
|
+
throw err;
|
|
1290
|
+
});
|
|
1291
|
+
projectCache.set(key, pending);
|
|
1292
|
+
return pending;
|
|
1293
|
+
}
|
|
1294
|
+
async function resolveProjectPaths(options = {}) {
|
|
1295
|
+
const { projectRoot: projectRootOption, ...rootOptions } = options;
|
|
1296
|
+
const projectRoot = resolve3(projectRootOption ?? await getProjectRoot());
|
|
1297
|
+
const project = await getProjectResolution(projectRoot, rootOptions);
|
|
1298
|
+
const projectDir = getProjectDir(project.projectId, rootOptions);
|
|
1299
|
+
return {
|
|
1300
|
+
projectId: project.projectId,
|
|
1301
|
+
projectDir,
|
|
1302
|
+
issuesDir: join3(projectDir, ISSUES_DIR_NAME),
|
|
1303
|
+
runLockFile: join3(projectDir, RUN_LOCK_FILENAME)
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
async function resolveQueuePaths(queueId, options = {}) {
|
|
1307
|
+
const { projectRoot: projectRootOption, ...rootOptions } = options;
|
|
1308
|
+
const projectRoot = resolve3(projectRootOption ?? await getProjectRoot());
|
|
1309
|
+
const project = await getProjectResolution(projectRoot, rootOptions);
|
|
1310
|
+
return getQueuePaths(project.projectId, queueId, rootOptions);
|
|
1311
|
+
}
|
|
1312
|
+
async function resolveIssuePaths(issueNumber, options = {}) {
|
|
1313
|
+
const { projectRoot: projectRootOption, ...rootOptions } = options;
|
|
1314
|
+
const projectRoot = resolve3(projectRootOption ?? await getProjectRoot());
|
|
1315
|
+
const project = await getProjectResolution(projectRoot, rootOptions);
|
|
1316
|
+
const paths = getIssuePaths(project.projectId, issueNumber, rootOptions);
|
|
1317
|
+
if (!checkedIssues.has(paths.issueDir)) {
|
|
1318
|
+
const legacyIssueDir = join3(project.legacyDir, basename3(paths.issueDir));
|
|
1319
|
+
if (!await directoryExists2(paths.issueDir) && await directoryExists2(legacyIssueDir)) {
|
|
1320
|
+
announceMigration(await migrateLegacyStorage(projectRoot, rootOptions));
|
|
1321
|
+
}
|
|
1322
|
+
checkedIssues.add(paths.issueDir);
|
|
1323
|
+
}
|
|
1324
|
+
return paths;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
export {
|
|
1328
|
+
requiresHumanAction,
|
|
1329
|
+
ClassifiedError,
|
|
1330
|
+
failureOf,
|
|
1331
|
+
actionOf,
|
|
1332
|
+
classify,
|
|
1333
|
+
RETRY_CONFIG_KEYS,
|
|
1334
|
+
resolvePolicy,
|
|
1335
|
+
withRetry,
|
|
1336
|
+
fixedBackoffPolicy,
|
|
1337
|
+
run,
|
|
1338
|
+
getProjectRoot,
|
|
1339
|
+
getCurrentBranch,
|
|
1340
|
+
getBaseBranch,
|
|
1341
|
+
getRemoteUrl,
|
|
1342
|
+
getHeadCommit,
|
|
1343
|
+
normalizeRemoteUrl,
|
|
1344
|
+
stripRemoteUrlCredentials,
|
|
1345
|
+
getCommitsSince,
|
|
1346
|
+
preflightRepository,
|
|
1347
|
+
describePreflight,
|
|
1348
|
+
committedStoryIds,
|
|
1349
|
+
isWorkingTreeClean,
|
|
1350
|
+
PROJECTS_DIR_NAME,
|
|
1351
|
+
ISSUES_DIR_NAME,
|
|
1352
|
+
QUEUES_DIR_NAME,
|
|
1353
|
+
getGlobalRoot,
|
|
1354
|
+
getIssuePaths,
|
|
1355
|
+
resilienceConfigSchema,
|
|
1356
|
+
globalConfigSchema,
|
|
1357
|
+
webLockSchema,
|
|
1358
|
+
runLockSchema,
|
|
1359
|
+
executionPlanSchema,
|
|
1360
|
+
recordUserStoryNumbering,
|
|
1361
|
+
resolveProjectPaths,
|
|
1362
|
+
resolveQueuePaths,
|
|
1363
|
+
resolveIssuePaths
|
|
1364
|
+
};
|
|
1365
|
+
//# sourceMappingURL=chunk-DIDUVL6O.js.map
|