issue-flow 0.11.1 → 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/dist/{analyze-XV3K25K3.js → analyze-OEET7KGE.js} +15 -10
- package/dist/{analyze-XV3K25K3.js.map → analyze-OEET7KGE.js.map} +1 -1
- 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-EWDJENFW.js → chunk-4JSOYE4G.js} +77 -12
- 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-KQ4JQN7T.js → chunk-A4SLHOW7.js} +68 -12
- 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-BXCPXRBR.js → chunk-FADTCQGM.js} +46 -11
- 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-IVHZEYAX.js → chunk-M5UCXTIT.js} +13 -11
- package/dist/{chunk-IVHZEYAX.js.map → chunk-M5UCXTIT.js.map} +1 -1
- 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-DBF2VWUN.js → chunk-NTMMF5UD.js} +515 -544
- package/dist/chunk-NTMMF5UD.js.map +1 -0
- package/dist/{chunk-EY6WMHV2.js → chunk-QCPGHBSZ.js} +14 -12
- package/dist/{chunk-EY6WMHV2.js.map → chunk-QCPGHBSZ.js.map} +1 -1
- package/dist/{chunk-LVKQ2AKT.js → chunk-TSOLWK7Y.js} +8 -6
- package/dist/chunk-TSOLWK7Y.js.map +1 -0
- package/dist/{chunk-34WNTK3K.js → chunk-XQUSIEKA.js} +11 -9
- package/dist/{chunk-34WNTK3K.js.map → chunk-XQUSIEKA.js.map} +1 -1
- 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-TBVTLMOP.js → generate-EGB4KV2B.js} +16 -10
- package/dist/{generate-TBVTLMOP.js.map → generate-EGB4KV2B.js.map} +1 -1
- 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/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-BXCPXRBR.js.map +0 -1
- package/dist/chunk-EWDJENFW.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-KQ4JQN7T.js.map +0 -1
- package/dist/chunk-LVKQ2AKT.js.map +0 -1
- package/dist/chunk-OBLVLIUS.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/init-PRMOTQ7N.js +0 -9
- package/dist/plan-Y2BYZD24.js +0 -17
- package/dist/pr-H6GLOSXF.js +0 -21
- package/dist/pr-review-CK5FR6WX.js +0 -17
- package/dist/prd-4PWLCGH5.js +0 -17
- package/dist/review-QGVAHEZF.js +0 -15
- package/dist/run-DBF2VWUN.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-Y2BYZD24.js.map → plan-QOZ3RIO6.js.map} +0 -0
- /package/dist/{pr-H6GLOSXF.js.map → pr-EBBHD2ZX.js.map} +0 -0
- /package/dist/{pr-review-CK5FR6WX.js.map → pr-review-VVMW5PO4.js.map} +0 -0
- /package/dist/{prd-4PWLCGH5.js.map → prd-JQSZF7PR.js.map} +0 -0
- /package/dist/{review-QGVAHEZF.js.map → review-3F6MARWH.js.map} +0 -0
|
@@ -1,579 +1,285 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensureWebMonitor
|
|
4
|
+
} from "./chunk-FJP3UQGD.js";
|
|
5
|
+
import {
|
|
6
|
+
runPr
|
|
7
|
+
} from "./chunk-FADTCQGM.js";
|
|
2
8
|
import {
|
|
3
9
|
mostRecent,
|
|
4
10
|
prReviewDir,
|
|
5
11
|
readPrReviewIndex,
|
|
6
12
|
runPrReview
|
|
7
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-A4SLHOW7.js";
|
|
8
14
|
import {
|
|
9
15
|
runPrd
|
|
10
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-M5UCXTIT.js";
|
|
11
17
|
import {
|
|
12
18
|
runReview
|
|
13
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-XQUSIEKA.js";
|
|
14
20
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
JournalPublisher,
|
|
22
|
+
MultiPublisher,
|
|
23
|
+
PIPELINE_PHASES,
|
|
24
|
+
PIPELINE_PHASES_NO_BRANCH,
|
|
25
|
+
PIPELINE_PHASES_WITH_PR_REVIEW,
|
|
26
|
+
PipelineManager,
|
|
27
|
+
buildDependencyGraph,
|
|
28
|
+
buildExecutionPlan,
|
|
29
|
+
computeExecutionOrder,
|
|
30
|
+
describeCycles,
|
|
31
|
+
isQueueComplete,
|
|
32
|
+
loadExecutionPlan,
|
|
33
|
+
markQueueIssueBlocked,
|
|
34
|
+
markQueueIssueCompleted,
|
|
35
|
+
markQueueIssueFailed,
|
|
36
|
+
markQueueIssueInProgress,
|
|
37
|
+
markQueueIssueSkipped,
|
|
38
|
+
nextQueueIssue,
|
|
39
|
+
parseJournal,
|
|
40
|
+
saveExecutionPlan,
|
|
41
|
+
setQueueBranch,
|
|
42
|
+
setQueuePullRequest
|
|
43
|
+
} from "./chunk-56KS3TFQ.js";
|
|
44
|
+
import {
|
|
45
|
+
acquireRunLock,
|
|
46
|
+
describeRunLockOwner
|
|
47
|
+
} from "./chunk-BAUPQXPE.js";
|
|
17
48
|
import {
|
|
18
49
|
runInit
|
|
19
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-NS6R37II.js";
|
|
20
51
|
import {
|
|
21
52
|
printQueueSummary,
|
|
22
53
|
printRunSummary,
|
|
23
54
|
runExecute
|
|
24
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-3QHK4YQA.js";
|
|
25
56
|
import {
|
|
26
57
|
listPullRequests,
|
|
27
58
|
publishGitState
|
|
28
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-TSOLWK7Y.js";
|
|
29
60
|
import {
|
|
30
61
|
runPlan
|
|
31
|
-
} from "./chunk-
|
|
32
|
-
import {
|
|
33
|
-
runPr
|
|
34
|
-
} from "./chunk-BXCPXRBR.js";
|
|
35
|
-
import "./chunk-3BOHTH4V.js";
|
|
62
|
+
} from "./chunk-QCPGHBSZ.js";
|
|
36
63
|
import {
|
|
37
|
-
emptyRelations,
|
|
38
64
|
getProvider,
|
|
39
|
-
resolveCommandIssue
|
|
40
|
-
|
|
41
|
-
} from "./chunk-AAY6KM7N.js";
|
|
42
|
-
import "./chunk-EWDJENFW.js";
|
|
43
|
-
import "./chunk-HDV3YDTG.js";
|
|
65
|
+
resolveCommandIssue
|
|
66
|
+
} from "./chunk-73ATPIYX.js";
|
|
44
67
|
import {
|
|
45
68
|
beginUsageScope,
|
|
46
69
|
getRunUsageTotals
|
|
47
|
-
} from "./chunk-
|
|
48
|
-
import
|
|
49
|
-
|
|
70
|
+
} from "./chunk-MJULIKQU.js";
|
|
71
|
+
import {
|
|
72
|
+
onShutdown
|
|
73
|
+
} from "./chunk-M66SJHQQ.js";
|
|
50
74
|
import {
|
|
51
|
-
|
|
75
|
+
getActiveResilienceConfig,
|
|
76
|
+
initResilienceConfig,
|
|
52
77
|
loadIssuesConfig,
|
|
53
|
-
loadWebConfig
|
|
78
|
+
loadWebConfig
|
|
79
|
+
} from "./chunk-42PJFMAR.js";
|
|
80
|
+
import {
|
|
81
|
+
committedStoryIds,
|
|
82
|
+
describePreflight,
|
|
83
|
+
getBaseBranch,
|
|
84
|
+
preflightRepository,
|
|
54
85
|
resolveIssuePaths,
|
|
55
86
|
resolveProjectPaths,
|
|
56
|
-
resolveQueuePaths
|
|
57
|
-
|
|
87
|
+
resolveQueuePaths,
|
|
88
|
+
run
|
|
89
|
+
} from "./chunk-DIDUVL6O.js";
|
|
58
90
|
import {
|
|
59
91
|
FilePublisher,
|
|
60
92
|
NullPublisher,
|
|
93
|
+
getInactivityTimeout,
|
|
61
94
|
isVerbose,
|
|
62
|
-
isoNow,
|
|
63
|
-
loadTaskPlan,
|
|
64
95
|
printError,
|
|
65
96
|
printInfo,
|
|
66
97
|
printSuccess,
|
|
67
98
|
printWarning,
|
|
68
|
-
|
|
99
|
+
setInactivityTimeout,
|
|
69
100
|
setOutputCallback,
|
|
70
101
|
setSessionPublisher,
|
|
71
102
|
setStoryStageCallback,
|
|
72
|
-
setStoryUpdateCallback
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
setStoryUpdateCallback
|
|
104
|
+
} from "./chunk-BYZDPR7N.js";
|
|
105
|
+
import {
|
|
106
|
+
isoNow,
|
|
107
|
+
loadTaskPlan,
|
|
108
|
+
saveTaskPlan
|
|
109
|
+
} from "./chunk-CD5ULRVC.js";
|
|
75
110
|
|
|
76
111
|
// src/commands/run.ts
|
|
77
112
|
import { randomUUID } from "crypto";
|
|
78
|
-
import { mkdir
|
|
113
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
79
114
|
import { join } from "path";
|
|
80
115
|
import { execa } from "execa";
|
|
81
116
|
|
|
82
|
-
// src/core/
|
|
83
|
-
var
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
pr: "prCreated",
|
|
95
|
-
"pr-review": "prReviewCompleted"
|
|
117
|
+
// src/core/decompose.ts
|
|
118
|
+
var DECOMPOSITION_THRESHOLDS = {
|
|
119
|
+
/** Timeouts on the same phase. Two is already a pattern, not bad luck. */
|
|
120
|
+
timeoutsPerPhase: 2,
|
|
121
|
+
/** Stories in the plan. Past this, the plan is a backlog. */
|
|
122
|
+
stories: 15,
|
|
123
|
+
/** Iterations in a row that completed no story. */
|
|
124
|
+
barrenIterations: 5,
|
|
125
|
+
/** Files touched on the branch. */
|
|
126
|
+
filesTouched: 40,
|
|
127
|
+
/** Characters in the issue body. */
|
|
128
|
+
issueBodyChars: 2e4
|
|
96
129
|
};
|
|
97
|
-
var
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Reload state from disk.
|
|
108
|
-
*/
|
|
109
|
-
async reload() {
|
|
110
|
-
this.plan = await loadTaskPlan(this.tasksJsonPath);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Check if a phase is complete.
|
|
114
|
-
*/
|
|
115
|
-
isPhaseComplete(phase) {
|
|
116
|
-
const field = PHASE_TO_FIELD[phase];
|
|
117
|
-
if (field === null) return true;
|
|
118
|
-
return this.plan.pipeline?.[field] ?? false;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Get the first incomplete phase in the pipeline.
|
|
122
|
-
* Returns null if all phases are complete.
|
|
123
|
-
*/
|
|
124
|
-
getNextPhase() {
|
|
125
|
-
for (const phase of this.activePhases) {
|
|
126
|
-
if (!this.isPhaseComplete(phase)) {
|
|
127
|
-
return phase;
|
|
128
|
-
}
|
|
130
|
+
var DECOMPOSITION_MIN_SIGNALS = 2;
|
|
131
|
+
function timeoutsByPhase(journal) {
|
|
132
|
+
const counts = /* @__PURE__ */ new Map();
|
|
133
|
+
let phase = "(unknown)";
|
|
134
|
+
for (const { event } of parseJournal(journal)) {
|
|
135
|
+
if (event.type === "phase:start") {
|
|
136
|
+
phase = event.phase;
|
|
137
|
+
continue;
|
|
129
138
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Mark a phase as complete and persist to disk.
|
|
154
|
-
*/
|
|
155
|
-
async markPhaseComplete(phase) {
|
|
156
|
-
const field = PHASE_TO_FIELD[phase];
|
|
157
|
-
if (field === null) return;
|
|
158
|
-
this.plan = {
|
|
159
|
-
...this.plan,
|
|
160
|
-
pipeline: {
|
|
161
|
-
...this.plan.pipeline ?? {
|
|
162
|
-
prdCompleted: false,
|
|
163
|
-
jsonCompleted: false,
|
|
164
|
-
executionCompleted: false,
|
|
165
|
-
reviewCompleted: false,
|
|
166
|
-
prCreated: false
|
|
167
|
-
},
|
|
168
|
-
[field]: true
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
await saveTaskPlan(this.tasksJsonPath, this.plan);
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// src/execution/plan.ts
|
|
176
|
-
import { mkdir, readFile } from "fs/promises";
|
|
177
|
-
import { dirname } from "path";
|
|
178
|
-
import { ZodError } from "zod";
|
|
179
|
-
|
|
180
|
-
// src/issues/graph.ts
|
|
181
|
-
var DEFAULT_MAX_NODES = 25;
|
|
182
|
-
var DEFAULT_MAX_DEPTH = 3;
|
|
183
|
-
function dependencyEdges(graph) {
|
|
184
|
-
const edges = [];
|
|
185
|
-
const seen = /* @__PURE__ */ new Set();
|
|
186
|
-
const add = (from, to) => {
|
|
187
|
-
if (from === to || !graph.nodes.has(from) || !graph.nodes.has(to)) return;
|
|
188
|
-
const key = `${from}->${to}`;
|
|
189
|
-
if (seen.has(key)) return;
|
|
190
|
-
seen.add(key);
|
|
191
|
-
edges.push({ from, to });
|
|
192
|
-
};
|
|
193
|
-
for (const node of graph.nodes.values()) {
|
|
194
|
-
for (const blocker of node.relations.blockedBy) {
|
|
195
|
-
add(blocker, node.id);
|
|
196
|
-
}
|
|
197
|
-
for (const blocked of node.relations.blocking) {
|
|
198
|
-
add(node.id, blocked);
|
|
139
|
+
if (event.type !== "retry") continue;
|
|
140
|
+
const kind = event.kind;
|
|
141
|
+
const reason = event.reason ?? "";
|
|
142
|
+
const timedOut = kind === "timeout" || kind === "stalled" || /timed out|stalled/i.test(reason);
|
|
143
|
+
if (!timedOut) continue;
|
|
144
|
+
counts.set(phase, (counts.get(phase) ?? 0) + 1);
|
|
145
|
+
}
|
|
146
|
+
return counts;
|
|
147
|
+
}
|
|
148
|
+
function longestBarrenRun(journal) {
|
|
149
|
+
let passing = 0;
|
|
150
|
+
let current = 0;
|
|
151
|
+
let longest = 0;
|
|
152
|
+
for (const { event } of parseJournal(journal)) {
|
|
153
|
+
if (event.type === "iteration:start") {
|
|
154
|
+
current++;
|
|
155
|
+
longest = Math.max(longest, current);
|
|
156
|
+
continue;
|
|
199
157
|
}
|
|
158
|
+
if (event.type !== "stories:update") continue;
|
|
159
|
+
const nowPassing = event.stories.filter((story) => story.passes).length;
|
|
160
|
+
if (nowPassing > passing) current = 0;
|
|
161
|
+
passing = Math.max(passing, nowPassing);
|
|
200
162
|
}
|
|
201
|
-
return
|
|
163
|
+
return longest;
|
|
202
164
|
}
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
165
|
+
function assessDecomposition(input) {
|
|
166
|
+
const signals = [];
|
|
167
|
+
const journal = input.journal ?? "";
|
|
168
|
+
for (const [phase, count] of timeoutsByPhase(journal)) {
|
|
169
|
+
if (count >= DECOMPOSITION_THRESHOLDS.timeoutsPerPhase) {
|
|
170
|
+
signals.push({
|
|
171
|
+
id: "repeated-timeouts",
|
|
172
|
+
detail: `The '${phase}' phase timed out ${count} times (threshold: ${DECOMPOSITION_THRESHOLDS.timeoutsPerPhase}).`
|
|
173
|
+
});
|
|
211
174
|
}
|
|
212
175
|
}
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for (let i = 1; i < cycle.length; i++) {
|
|
220
|
-
if (compareIds(cycle[i], cycle[pivot]) < 0) pivot = i;
|
|
221
|
-
}
|
|
222
|
-
const normalized = [...cycle.slice(pivot), ...cycle.slice(0, pivot)];
|
|
223
|
-
const key = normalized.join("->");
|
|
224
|
-
if (reported.has(key)) return;
|
|
225
|
-
reported.add(key);
|
|
226
|
-
cycles.push(normalized);
|
|
227
|
-
};
|
|
228
|
-
const visit = (id) => {
|
|
229
|
-
state.set(id, "visiting");
|
|
230
|
-
stack.push(id);
|
|
231
|
-
for (const next of adjacency.get(id) ?? []) {
|
|
232
|
-
const status = state.get(next);
|
|
233
|
-
if (status === "visiting") {
|
|
234
|
-
record(stack.slice(stack.indexOf(next)));
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (status === void 0) {
|
|
238
|
-
visit(next);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
stack.pop();
|
|
242
|
-
state.set(id, "done");
|
|
243
|
-
};
|
|
244
|
-
for (const id of graph.nodes.keys()) {
|
|
245
|
-
if (!state.has(id)) visit(id);
|
|
176
|
+
const stories = input.plan?.userStories.length ?? 0;
|
|
177
|
+
if (stories > DECOMPOSITION_THRESHOLDS.stories) {
|
|
178
|
+
signals.push({
|
|
179
|
+
id: "story-count",
|
|
180
|
+
detail: `The plan has ${stories} user stories (threshold: ${DECOMPOSITION_THRESHOLDS.stories}).`
|
|
181
|
+
});
|
|
246
182
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return na - nb;
|
|
183
|
+
const barren = longestBarrenRun(journal);
|
|
184
|
+
if (barren >= DECOMPOSITION_THRESHOLDS.barrenIterations) {
|
|
185
|
+
signals.push({
|
|
186
|
+
id: "barren-iterations",
|
|
187
|
+
detail: `${barren} iterations in a row completed no story (threshold: ${DECOMPOSITION_THRESHOLDS.barrenIterations}).`
|
|
188
|
+
});
|
|
254
189
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
...relations.children,
|
|
262
|
-
...relations.blockedBy,
|
|
263
|
-
...relations.blocking
|
|
264
|
-
],
|
|
265
|
-
relations.id
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
async function buildDependencyGraph(roots, fetchRelations, options = {}) {
|
|
269
|
-
const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
270
|
-
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
271
|
-
const warn = options.warn ?? (() => {
|
|
272
|
-
});
|
|
273
|
-
const known = /* @__PURE__ */ new Map();
|
|
274
|
-
for (const issue of options.known ?? []) {
|
|
275
|
-
known.set(issue.id, issue);
|
|
276
|
-
}
|
|
277
|
-
const rootIds = uniqueIds(roots);
|
|
278
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
279
|
-
const queued = new Set(rootIds);
|
|
280
|
-
let truncated = false;
|
|
281
|
-
let frontier = rootIds.map((id) => ({ id, depth: 0 }));
|
|
282
|
-
while (frontier.length > 0) {
|
|
283
|
-
const next = [];
|
|
284
|
-
for (const { id, depth } of frontier) {
|
|
285
|
-
if (nodes.size >= maxNodes) {
|
|
286
|
-
truncated = true;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
let relations = null;
|
|
290
|
-
try {
|
|
291
|
-
relations = await fetchRelations(id);
|
|
292
|
-
} catch (err) {
|
|
293
|
-
warn(
|
|
294
|
-
`Could not read the relations of issue '${id}': ${err instanceof Error ? err.message : String(err)}`
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
let issue = known.get(id) ?? null;
|
|
298
|
-
if (issue === null && options.fetchIssue !== void 0) {
|
|
299
|
-
try {
|
|
300
|
-
issue = await options.fetchIssue(id);
|
|
301
|
-
} catch (err) {
|
|
302
|
-
warn(`Could not read issue '${id}': ${err instanceof Error ? err.message : String(err)}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
const resolved = relations ?? emptyRelations(id);
|
|
306
|
-
nodes.set(id, { id, relations: resolved, depth, issue, root: rootIds.includes(id) });
|
|
307
|
-
if (depth >= maxDepth) {
|
|
308
|
-
if (neighbours(resolved).some((neighbour) => !queued.has(neighbour))) {
|
|
309
|
-
truncated = true;
|
|
310
|
-
}
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
for (const neighbour of neighbours(resolved)) {
|
|
314
|
-
if (queued.has(neighbour)) continue;
|
|
315
|
-
queued.add(neighbour);
|
|
316
|
-
next.push({ id: neighbour, depth: depth + 1 });
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (nodes.size >= maxNodes && next.length > 0) {
|
|
320
|
-
truncated = true;
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
frontier = next;
|
|
324
|
-
}
|
|
325
|
-
const graph = {
|
|
326
|
-
roots: rootIds,
|
|
327
|
-
nodes,
|
|
328
|
-
cycles: [],
|
|
329
|
-
truncated,
|
|
330
|
-
limits: { maxNodes, maxDepth }
|
|
331
|
-
};
|
|
332
|
-
linkReferencedBy(graph);
|
|
333
|
-
graph.cycles = findCycles(graph);
|
|
334
|
-
return graph;
|
|
335
|
-
}
|
|
336
|
-
function linkReferencedBy(graph) {
|
|
337
|
-
for (const node of graph.nodes.values()) {
|
|
338
|
-
for (const cited of node.relations.references) {
|
|
339
|
-
const target = graph.nodes.get(cited);
|
|
340
|
-
if (target === void 0 || target.id === node.id) continue;
|
|
341
|
-
target.relations.referencedBy = uniqueIds(
|
|
342
|
-
[...target.relations.referencedBy, node.id],
|
|
343
|
-
target.id
|
|
344
|
-
);
|
|
345
|
-
}
|
|
190
|
+
const files = input.filesTouched ?? 0;
|
|
191
|
+
if (files > DECOMPOSITION_THRESHOLDS.filesTouched) {
|
|
192
|
+
signals.push({
|
|
193
|
+
id: "files-touched",
|
|
194
|
+
detail: `${files} files were touched on this branch (threshold: ${DECOMPOSITION_THRESHOLDS.filesTouched}).`
|
|
195
|
+
});
|
|
346
196
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
low: "low"
|
|
354
|
-
};
|
|
355
|
-
var PRIORITY_RANK = { high: 0, medium: 1, low: 2 };
|
|
356
|
-
var NO_PRIORITY_RANK = 3;
|
|
357
|
-
function priorityOf(labels) {
|
|
358
|
-
for (const label of labels) {
|
|
359
|
-
const normalized = label.trim().toLowerCase();
|
|
360
|
-
const direct = PRIORITY_LABELS[normalized];
|
|
361
|
-
if (direct !== void 0) return direct;
|
|
362
|
-
const qualified = /^(?:priority|prio|p)\s*[:/-]\s*(high|medium|low)$/.exec(normalized);
|
|
363
|
-
const matched = qualified?.[1];
|
|
364
|
-
if (matched !== void 0) return PRIORITY_LABELS[matched];
|
|
365
|
-
}
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
function hierarchyDepth(id, parentOf, selected) {
|
|
369
|
-
let depth = 0;
|
|
370
|
-
let current = parentOf.get(id) ?? null;
|
|
371
|
-
const seen = /* @__PURE__ */ new Set([id]);
|
|
372
|
-
while (current !== null && selected.has(current) && !seen.has(current)) {
|
|
373
|
-
depth++;
|
|
374
|
-
seen.add(current);
|
|
375
|
-
current = parentOf.get(current) ?? null;
|
|
376
|
-
}
|
|
377
|
-
return depth;
|
|
378
|
-
}
|
|
379
|
-
function computeExecutionOrder(graph, options = {}) {
|
|
380
|
-
const selected = new Set(
|
|
381
|
-
(options.include ?? [...graph.nodes.keys()]).filter((id) => graph.nodes.has(id))
|
|
382
|
-
);
|
|
383
|
-
const parentOf = /* @__PURE__ */ new Map();
|
|
384
|
-
const priority = /* @__PURE__ */ new Map();
|
|
385
|
-
for (const id of selected) {
|
|
386
|
-
const node = graph.nodes.get(id);
|
|
387
|
-
parentOf.set(id, node?.relations.parent ?? null);
|
|
388
|
-
priority.set(id, priorityOf(node?.issue?.labels ?? []));
|
|
389
|
-
}
|
|
390
|
-
const incoming = /* @__PURE__ */ new Map();
|
|
391
|
-
const outgoing = /* @__PURE__ */ new Map();
|
|
392
|
-
for (const id of selected) {
|
|
393
|
-
incoming.set(id, /* @__PURE__ */ new Set());
|
|
394
|
-
outgoing.set(id, []);
|
|
395
|
-
}
|
|
396
|
-
for (const { from, to } of dependencyEdges(graph)) {
|
|
397
|
-
if (!selected.has(from) || !selected.has(to) || from === to) continue;
|
|
398
|
-
incoming.get(to)?.add(from);
|
|
399
|
-
outgoing.get(from)?.push(to);
|
|
400
|
-
}
|
|
401
|
-
const depth = /* @__PURE__ */ new Map();
|
|
402
|
-
for (const id of selected) {
|
|
403
|
-
depth.set(id, hierarchyDepth(id, parentOf, selected));
|
|
404
|
-
}
|
|
405
|
-
const rank = (id) => {
|
|
406
|
-
const value = priority.get(id) ?? null;
|
|
407
|
-
return value === null ? NO_PRIORITY_RANK : PRIORITY_RANK[value];
|
|
408
|
-
};
|
|
409
|
-
const better = (a, b) => {
|
|
410
|
-
const byHierarchy = (depth.get(a) ?? 0) - (depth.get(b) ?? 0);
|
|
411
|
-
if (byHierarchy !== 0) return byHierarchy;
|
|
412
|
-
const byPriority = rank(a) - rank(b);
|
|
413
|
-
if (byPriority !== 0) return byPriority;
|
|
414
|
-
return compareIds(a, b);
|
|
415
|
-
};
|
|
416
|
-
const ready = [...selected].filter((id) => (incoming.get(id)?.size ?? 0) === 0);
|
|
417
|
-
const order = [];
|
|
418
|
-
while (ready.length > 0) {
|
|
419
|
-
ready.sort(better);
|
|
420
|
-
const next = ready.shift();
|
|
421
|
-
order.push(next);
|
|
422
|
-
for (const dependent of outgoing.get(next) ?? []) {
|
|
423
|
-
const pending = incoming.get(dependent);
|
|
424
|
-
if (pending === void 0) continue;
|
|
425
|
-
pending.delete(next);
|
|
426
|
-
if (pending.size === 0) {
|
|
427
|
-
ready.push(dependent);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
197
|
+
const body = input.issueBody?.length ?? 0;
|
|
198
|
+
if (body > DECOMPOSITION_THRESHOLDS.issueBodyChars) {
|
|
199
|
+
signals.push({
|
|
200
|
+
id: "issue-size",
|
|
201
|
+
detail: `The issue body is ${body} characters (threshold: ${DECOMPOSITION_THRESHOLDS.issueBodyChars}).`
|
|
202
|
+
});
|
|
430
203
|
}
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
204
|
+
if (input.hitMaxIterations === true) {
|
|
205
|
+
signals.push({
|
|
206
|
+
id: "max-iterations",
|
|
207
|
+
detail: "The execute loop stopped because it ran out of iterations, not because it finished."
|
|
208
|
+
});
|
|
435
209
|
}
|
|
436
|
-
return {
|
|
437
|
-
}
|
|
438
|
-
function describeCycles(cycles) {
|
|
439
|
-
return cycles.map((cycle) => [...cycle, cycle[0]].map((id) => `#${id}`).join(" \u2192 ")).join("; ");
|
|
210
|
+
return { signals, oversized: signals.length >= DECOMPOSITION_MIN_SIGNALS };
|
|
440
211
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const scheduled = new Set(input.order);
|
|
456
|
-
const requested = new Set(input.requested);
|
|
457
|
-
const issues = input.order.map((id, index) => {
|
|
458
|
-
const node = input.graph.nodes.get(id);
|
|
459
|
-
const relations = node?.relations;
|
|
460
|
-
const described = describeIssue(node?.issue ?? null, id);
|
|
461
|
-
return {
|
|
462
|
-
id,
|
|
463
|
-
number: described.number,
|
|
464
|
-
title: described.title,
|
|
465
|
-
url: described.url,
|
|
466
|
-
source: described.source,
|
|
467
|
-
position: index + 1,
|
|
468
|
-
status: "pending",
|
|
469
|
-
origin: requested.has(id) ? "requested" : "discovered",
|
|
470
|
-
// Only the constraints that survive inside the queue: a blocker left out
|
|
471
|
-
// of it is reported in `excluded`, not enforced as a dependency.
|
|
472
|
-
dependsOn: (relations?.blockedBy ?? []).filter((blocker) => scheduled.has(blocker)),
|
|
473
|
-
parent: relations?.parent ?? null,
|
|
474
|
-
priority: priorityOf(node?.issue?.labels ?? []),
|
|
475
|
-
heuristic: (relations?.heuristic.length ?? 0) > 0,
|
|
476
|
-
failedPhase: null,
|
|
477
|
-
lastError: null,
|
|
478
|
-
startedAt: null,
|
|
479
|
-
completedAt: null
|
|
480
|
-
};
|
|
481
|
-
});
|
|
482
|
-
const excluded = [];
|
|
483
|
-
for (const node of input.graph.nodes.values()) {
|
|
484
|
-
if (scheduled.has(node.id)) continue;
|
|
485
|
-
const described = describeIssue(node.issue, node.id);
|
|
486
|
-
excluded.push({
|
|
487
|
-
id: node.id,
|
|
488
|
-
number: described.number,
|
|
489
|
-
title: described.title,
|
|
490
|
-
url: described.url,
|
|
491
|
-
reason: "Discovered in the hierarchy but not selected for this run"
|
|
212
|
+
var STORIES_PER_SUBISSUE = 5;
|
|
213
|
+
function proposeSubIssues(plan) {
|
|
214
|
+
const pending = [...plan?.userStories ?? []].filter((story) => !story.passes).sort((a, b) => a.priority - b.priority);
|
|
215
|
+
if (pending.length === 0) return [];
|
|
216
|
+
const chunks = [];
|
|
217
|
+
for (let index = 0; index < pending.length; index += STORIES_PER_SUBISSUE) {
|
|
218
|
+
const stories = pending.slice(index, index + STORIES_PER_SUBISSUE);
|
|
219
|
+
const first = stories[0];
|
|
220
|
+
const last = stories[stories.length - 1];
|
|
221
|
+
const range = first === void 0 || last === void 0 || first.id === last.id ? first?.id ?? "" : `${first.id}\u2013${last.id}`;
|
|
222
|
+
chunks.push({
|
|
223
|
+
title: `${plan?.description ?? "Issue"} \u2014 part ${chunks.length + 1} (${range})`,
|
|
224
|
+
stories,
|
|
225
|
+
dependsOn: chunks.length === 0 ? [] : [chunks[chunks.length - 1]?.title ?? ""]
|
|
492
226
|
});
|
|
493
227
|
}
|
|
494
|
-
return
|
|
495
|
-
schemaVersion: 1,
|
|
496
|
-
id: input.requested[0] ?? input.order[0],
|
|
497
|
-
project: input.projectId,
|
|
498
|
-
requested: [...input.requested],
|
|
499
|
-
branchName: null,
|
|
500
|
-
noBranch: input.noBranch ?? false,
|
|
501
|
-
prReview: input.prReview ?? false,
|
|
502
|
-
status: "pending",
|
|
503
|
-
createdAt: at,
|
|
504
|
-
updatedAt: at,
|
|
505
|
-
truncated: input.graph.truncated,
|
|
506
|
-
issues,
|
|
507
|
-
excluded
|
|
508
|
-
};
|
|
228
|
+
return chunks;
|
|
509
229
|
}
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
230
|
+
function buildDecompositionReport(input) {
|
|
231
|
+
const { issueNumber, assessment, plan, at } = input;
|
|
232
|
+
const proposals = proposeSubIssues(plan);
|
|
233
|
+
const lines = [];
|
|
234
|
+
lines.push(`# Issue #${issueNumber} looks larger than one run`, "");
|
|
235
|
+
lines.push(`Detected at ${at}.`, "");
|
|
236
|
+
lines.push(
|
|
237
|
+
"This is a report, not an action. Nothing has been split, and nothing will be",
|
|
238
|
+
"without you asking for it.",
|
|
239
|
+
""
|
|
240
|
+
);
|
|
241
|
+
lines.push("## What was detected", "");
|
|
242
|
+
for (const signal of assessment.signals) {
|
|
243
|
+
lines.push(`- **${signal.id}** \u2014 ${signal.detail}`);
|
|
521
244
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return
|
|
559
|
-
|
|
560
|
-
completedAt: now(),
|
|
561
|
-
failedPhase: null,
|
|
562
|
-
lastError: null
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
function markQueueIssueFailed(plan, id, failure2) {
|
|
566
|
-
return updateIssue(plan, id, {
|
|
567
|
-
status: "failed",
|
|
568
|
-
failedPhase: failure2.phase,
|
|
569
|
-
lastError: failure2.error
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
function setQueueBranch(plan, branchName) {
|
|
573
|
-
return { ...plan, branchName };
|
|
574
|
-
}
|
|
575
|
-
function setQueuePullRequest(plan, pullRequest) {
|
|
576
|
-
return { ...plan, pullRequest };
|
|
245
|
+
lines.push("");
|
|
246
|
+
if (proposals.length === 0) {
|
|
247
|
+
lines.push("## Proposed split", "");
|
|
248
|
+
lines.push(
|
|
249
|
+
"No pending user stories were found, so there is nothing to cut here.",
|
|
250
|
+
"The signals above are about the run rather than about the plan.",
|
|
251
|
+
""
|
|
252
|
+
);
|
|
253
|
+
return `${lines.join("\n")}
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
lines.push("## Proposed split", "");
|
|
257
|
+
lines.push(
|
|
258
|
+
`The ${proposals.reduce((total, proposal) => total + proposal.stories.length, 0)} pending stories, in priority order, cut into ${proposals.length} sub-issues of at most ${STORIES_PER_SUBISSUE}. Each depends on the one before it, which is the only`,
|
|
259
|
+
"dependency shape that can be derived from the plan alone \u2014 read it as a starting",
|
|
260
|
+
"point, not as an answer.",
|
|
261
|
+
""
|
|
262
|
+
);
|
|
263
|
+
for (const proposal of proposals) {
|
|
264
|
+
lines.push(`### ${proposal.title}`, "");
|
|
265
|
+
if (proposal.dependsOn.length > 0) {
|
|
266
|
+
lines.push(`Depends on: ${proposal.dependsOn.join(", ")}`, "");
|
|
267
|
+
}
|
|
268
|
+
for (const story of proposal.stories) {
|
|
269
|
+
lines.push(`- \`${story.id}\` ${story.title}`);
|
|
270
|
+
}
|
|
271
|
+
lines.push("");
|
|
272
|
+
}
|
|
273
|
+
lines.push("## What to do next", "");
|
|
274
|
+
lines.push(
|
|
275
|
+
"- Adjust the cut above until it matches how you would actually split the work;",
|
|
276
|
+
'- create the sub-issues (`issue-flow generate "\u2026"`) and run them as a queue;',
|
|
277
|
+
"- or decide the issue is fine as it is and re-run it \u2014 nothing here blocks that,",
|
|
278
|
+
" beyond the report you are reading.",
|
|
279
|
+
""
|
|
280
|
+
);
|
|
281
|
+
return `${lines.join("\n")}
|
|
282
|
+
`;
|
|
577
283
|
}
|
|
578
284
|
|
|
579
285
|
// src/execution/confirm.ts
|
|
@@ -1162,6 +868,7 @@ async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
|
|
|
1162
868
|
}
|
|
1163
869
|
return outcome;
|
|
1164
870
|
}
|
|
871
|
+
var DEFAULT_MAX_ISSUE_ATTEMPTS = 2;
|
|
1165
872
|
async function runPipeline(issue, mode, from, noBranch, prReview, options = {}) {
|
|
1166
873
|
let requested;
|
|
1167
874
|
try {
|
|
@@ -1170,34 +877,102 @@ async function runPipeline(issue, mode, from, noBranch, prReview, options = {})
|
|
|
1170
877
|
printError(err instanceof Error ? err.message : String(err));
|
|
1171
878
|
return 1;
|
|
1172
879
|
}
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (first.queue === void 0) {
|
|
1181
|
-
return first.code;
|
|
880
|
+
const ownership = await claimRunOwnership(requested[0]);
|
|
881
|
+
if (!ownership.ok) {
|
|
882
|
+
printError(
|
|
883
|
+
`Another issue-flow run owns this project: ${describeRunLockOwner(ownership.owner)}.`
|
|
884
|
+
);
|
|
885
|
+
printInfo("Wait for it to finish, or stop that process before running again.");
|
|
886
|
+
return 1;
|
|
1182
887
|
}
|
|
1183
|
-
|
|
1184
|
-
first
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
888
|
+
try {
|
|
889
|
+
const first = await runIssueSession(requested[0], mode, {
|
|
890
|
+
from,
|
|
891
|
+
noBranch,
|
|
892
|
+
prReview,
|
|
893
|
+
requested,
|
|
894
|
+
runOptions: options,
|
|
895
|
+
...ownership.interruptedBy === null ? {} : { interruptedBy: ownership.interruptedBy }
|
|
896
|
+
});
|
|
897
|
+
if (first.queue === void 0) {
|
|
898
|
+
return first.code;
|
|
899
|
+
}
|
|
900
|
+
return runQueue(
|
|
901
|
+
first.queue.plan,
|
|
902
|
+
{ mode, from, noBranch, prReview, runOptions: options },
|
|
903
|
+
first.queue.resolved
|
|
904
|
+
);
|
|
905
|
+
} finally {
|
|
906
|
+
await ownership.release();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
var WRITING_PHASES = /* @__PURE__ */ new Set(["plan", "execute", "pr"]);
|
|
910
|
+
async function ensureRepositoryWritable(phase) {
|
|
911
|
+
if (!WRITING_PHASES.has(phase)) return;
|
|
912
|
+
const preflight = await preflightRepository({ intent: "resume-same-phase" });
|
|
913
|
+
if (preflight.ok) return;
|
|
914
|
+
for (const line of describePreflight(preflight)) {
|
|
915
|
+
printError(line);
|
|
916
|
+
}
|
|
917
|
+
throw new Error(`The repository is not in a state the ${phase} phase can write to`);
|
|
918
|
+
}
|
|
919
|
+
async function claimRunOwnership(target) {
|
|
920
|
+
let lockFile;
|
|
921
|
+
try {
|
|
922
|
+
lockFile = (await resolveProjectPaths()).runLockFile;
|
|
923
|
+
} catch {
|
|
924
|
+
return { ok: true, interruptedBy: null, release: async () => {
|
|
925
|
+
} };
|
|
926
|
+
}
|
|
927
|
+
const result = await acquireRunLock(lockFile, { target });
|
|
928
|
+
if (!result.ok) return result;
|
|
929
|
+
return {
|
|
930
|
+
ok: true,
|
|
931
|
+
interruptedBy: result.handle.reclaimedFrom,
|
|
932
|
+
release: () => result.handle.release()
|
|
933
|
+
};
|
|
1188
934
|
}
|
|
1189
935
|
async function runIssueSession(issueNumber, mode, input) {
|
|
1190
936
|
const paths = await resolveIssuePaths(issueNumber);
|
|
937
|
+
const resilience = await initResilienceConfig();
|
|
938
|
+
const configuredInactivity = resilience.watchdog?.inactivityTimeoutMs;
|
|
939
|
+
if (configuredInactivity !== void 0 && getInactivityTimeout() === void 0) {
|
|
940
|
+
setInactivityTimeout(configuredInactivity);
|
|
941
|
+
}
|
|
1191
942
|
const webConfig = await loadWebConfig();
|
|
1192
|
-
|
|
943
|
+
const surfaces = [];
|
|
944
|
+
const journalEnabled = resilience.journal?.enabled === true;
|
|
945
|
+
if (webConfig.enabled || journalEnabled) {
|
|
946
|
+
await mkdir(paths.issueDir, { recursive: true });
|
|
947
|
+
}
|
|
1193
948
|
if (webConfig.enabled) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
949
|
+
surfaces.push(
|
|
950
|
+
new FilePublisher(paths.sessionFile, {
|
|
951
|
+
logLimit: webConfig.logLimit,
|
|
952
|
+
includeLogs: webConfig.includeLogs
|
|
953
|
+
})
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (journalEnabled) {
|
|
957
|
+
surfaces.push(
|
|
958
|
+
new JournalPublisher(paths.eventsFile, paths.rotatedEventsFile, {
|
|
959
|
+
logLimit: webConfig.logLimit,
|
|
960
|
+
includeLogs: webConfig.includeLogs,
|
|
961
|
+
...resilience.journal?.maxFileBytes === void 0 ? {} : { maxFileBytes: resilience.journal.maxFileBytes }
|
|
962
|
+
})
|
|
963
|
+
);
|
|
1199
964
|
}
|
|
965
|
+
const publisher = surfaces.length === 0 ? new NullPublisher() : surfaces.length === 1 ? surfaces[0] : new MultiPublisher(surfaces);
|
|
1200
966
|
setSessionPublisher(publisher);
|
|
967
|
+
if (input.interruptedBy !== void 0) {
|
|
968
|
+
const previous = input.interruptedBy;
|
|
969
|
+
publisher.publish({
|
|
970
|
+
type: "log",
|
|
971
|
+
at: isoNow(),
|
|
972
|
+
level: "warn",
|
|
973
|
+
message: `Previous run interrupted: ${describeRunLockOwner(previous)}. Its lock was stale and has been taken over.`
|
|
974
|
+
});
|
|
975
|
+
}
|
|
1201
976
|
if (webConfig.enabled) {
|
|
1202
977
|
await ensureWebMonitor({
|
|
1203
978
|
publisher,
|
|
@@ -1213,10 +988,34 @@ async function runIssueSession(issueNumber, mode, input) {
|
|
|
1213
988
|
storyCount: 0,
|
|
1214
989
|
elapsedSeconds: 0
|
|
1215
990
|
};
|
|
991
|
+
const releaseCheckpoint = onShutdown({
|
|
992
|
+
phase: "checkpoint",
|
|
993
|
+
run: async () => {
|
|
994
|
+
await pauseIssue(paths.tasksFile, issueNumber);
|
|
995
|
+
publisher.publish({
|
|
996
|
+
type: "log",
|
|
997
|
+
at: isoNow(),
|
|
998
|
+
level: "warn",
|
|
999
|
+
message: `Interrupted during issue #${issueNumber}. A checkpoint was saved; resume with \`issue-flow resume ${issueNumber}\`.`
|
|
1000
|
+
});
|
|
1001
|
+
publisher.publish({ type: "session:end", at: isoNow(), status: "failed" });
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
const releaseClose = onShutdown({
|
|
1005
|
+
phase: "close",
|
|
1006
|
+
run: async () => {
|
|
1007
|
+
await publisher.close();
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1216
1010
|
try {
|
|
1217
1011
|
result = await runPipelinePhases(issueNumber, paths, mode, publisher, input);
|
|
1012
|
+
if (result.code !== 0) {
|
|
1013
|
+
await reportIfOversized(issueNumber, paths, result);
|
|
1014
|
+
}
|
|
1218
1015
|
return result;
|
|
1219
1016
|
} finally {
|
|
1017
|
+
releaseCheckpoint();
|
|
1018
|
+
releaseClose();
|
|
1220
1019
|
if (result.queue === void 0) {
|
|
1221
1020
|
publisher.publish({
|
|
1222
1021
|
type: "session:end",
|
|
@@ -1228,10 +1027,127 @@ async function runIssueSession(issueNumber, mode, input) {
|
|
|
1228
1027
|
setSessionPublisher(void 0);
|
|
1229
1028
|
}
|
|
1230
1029
|
}
|
|
1030
|
+
async function reportIfOversized(issueNumber, paths, result) {
|
|
1031
|
+
try {
|
|
1032
|
+
const journal = `${await readFileIfPresent(paths.rotatedEventsFile)}${await readFileIfPresent(paths.eventsFile)}`;
|
|
1033
|
+
const plan = await loadTaskPlan(paths.tasksFile).catch(() => null);
|
|
1034
|
+
const assessment = assessDecomposition({
|
|
1035
|
+
journal,
|
|
1036
|
+
plan,
|
|
1037
|
+
filesTouched: await countChangedFiles(),
|
|
1038
|
+
hitMaxIterations: result.failedPhase === "execute" && plan?.issueStatus !== "completed"
|
|
1039
|
+
});
|
|
1040
|
+
if (!assessment.oversized) return;
|
|
1041
|
+
const report = buildDecompositionReport({
|
|
1042
|
+
issueNumber,
|
|
1043
|
+
assessment,
|
|
1044
|
+
plan,
|
|
1045
|
+
at: isoNow()
|
|
1046
|
+
});
|
|
1047
|
+
const reportFile = paths.decompositionFile;
|
|
1048
|
+
await mkdir(paths.issueDir, { recursive: true });
|
|
1049
|
+
await writeFile(reportFile, report, "utf-8");
|
|
1050
|
+
printWarning(`Issue #${issueNumber} looks larger than one run. Report: ${reportFile}`);
|
|
1051
|
+
for (const signal of assessment.signals) {
|
|
1052
|
+
printWarning(` ${signal.detail}`);
|
|
1053
|
+
}
|
|
1054
|
+
await maybeCreateSubIssues(issueNumber, plan, reportFile);
|
|
1055
|
+
if (plan !== null) {
|
|
1056
|
+
await saveTaskPlan(paths.tasksFile, {
|
|
1057
|
+
...plan,
|
|
1058
|
+
runState: {
|
|
1059
|
+
currentPhase: plan.runState?.currentPhase ?? result.failedPhase,
|
|
1060
|
+
attempt: plan.runState?.attempt ?? 0,
|
|
1061
|
+
owner: null,
|
|
1062
|
+
status: "blocked",
|
|
1063
|
+
blockedReason: `Looks larger than one run; see ${reportFile}`,
|
|
1064
|
+
lastHeartbeatAt: isoNow()
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function maybeCreateSubIssues(issueNumber, plan, reportFile) {
|
|
1072
|
+
if (getActiveResilienceConfig().decompose?.auto !== true) {
|
|
1073
|
+
printInfo(`Nothing was split. Read ${reportFile} and decide.`);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const committed = await committedStoryIds(await getBaseBranch()).catch(() => /* @__PURE__ */ new Set());
|
|
1077
|
+
if (committed.size > 0) {
|
|
1078
|
+
printWarning(
|
|
1079
|
+
`--auto-decompose did not run: ${committed.size} story(ies) are already committed on this branch. Splitting on top of committed work needs a person.`
|
|
1080
|
+
);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const proposals = proposeSubIssues(plan);
|
|
1084
|
+
if (proposals.length === 0) {
|
|
1085
|
+
printInfo("Nothing left to split: no pending story.");
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const { runGenerate } = await import("./generate-EGB4KV2B.js");
|
|
1089
|
+
for (const proposal of proposals) {
|
|
1090
|
+
const instruction = [
|
|
1091
|
+
`Create a sub-issue of #${issueNumber}: ${proposal.title}.`,
|
|
1092
|
+
proposal.dependsOn.length === 0 ? "" : `It depends on: ${proposal.dependsOn.join(", ")}.`,
|
|
1093
|
+
"It covers exactly these user stories, and nothing else:",
|
|
1094
|
+
...proposal.stories.map((story) => `- ${story.id} ${story.title}: ${story.description}`)
|
|
1095
|
+
].filter((line) => line !== "").join("\n");
|
|
1096
|
+
const code = await runGenerate(instruction);
|
|
1097
|
+
if (code !== 0) {
|
|
1098
|
+
printWarning(
|
|
1099
|
+
`Could not create "${proposal.title}". The report at ${reportFile} still stands.`
|
|
1100
|
+
);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
async function countChangedFiles() {
|
|
1106
|
+
try {
|
|
1107
|
+
const base = await getBaseBranch();
|
|
1108
|
+
const result = await run("git", ["diff", "--name-only", `${base}...HEAD`]);
|
|
1109
|
+
if (result.exitCode !== 0) return 0;
|
|
1110
|
+
return result.stdout.split("\n").filter((line) => line.trim() !== "").length;
|
|
1111
|
+
} catch {
|
|
1112
|
+
return 0;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function readFileIfPresent(path) {
|
|
1116
|
+
try {
|
|
1117
|
+
return await readFile(path, "utf-8");
|
|
1118
|
+
} catch {
|
|
1119
|
+
return "";
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async function pauseIssue(tasksFile, issueNumber) {
|
|
1123
|
+
try {
|
|
1124
|
+
const plan = await loadTaskPlan(tasksFile);
|
|
1125
|
+
await saveTaskPlan(tasksFile, {
|
|
1126
|
+
...plan,
|
|
1127
|
+
runState: {
|
|
1128
|
+
...plan.runState ?? {
|
|
1129
|
+
currentPhase: null,
|
|
1130
|
+
attempt: 0,
|
|
1131
|
+
lastHeartbeatAt: null,
|
|
1132
|
+
blockedReason: null,
|
|
1133
|
+
owner: null
|
|
1134
|
+
},
|
|
1135
|
+
status: "paused",
|
|
1136
|
+
lastHeartbeatAt: isoNow()
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
} catch {
|
|
1140
|
+
printWarning(`Could not write a checkpoint for issue #${issueNumber}.`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1231
1143
|
async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
1232
1144
|
const { from, noBranch, prReview, queue } = input;
|
|
1233
1145
|
const continueNumbering = input.runOptions?.continueNumbering;
|
|
1234
1146
|
const startUs = input.runOptions?.startUs;
|
|
1147
|
+
const executeRetry = {
|
|
1148
|
+
retryLimit: input.runOptions?.retryLimit,
|
|
1149
|
+
retryForever: input.runOptions?.retryForever
|
|
1150
|
+
};
|
|
1235
1151
|
const tasksPath = paths.tasksFile;
|
|
1236
1152
|
const sessionId = randomUUID();
|
|
1237
1153
|
let publishedIssueNumber = toIssueNumber(issueNumber);
|
|
@@ -1399,6 +1315,7 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1399
1315
|
}
|
|
1400
1316
|
const startIdx = Math.max(phaseOrder.indexOf(startPhase), 0);
|
|
1401
1317
|
const makeRunner = (fn, phase) => async () => {
|
|
1318
|
+
await ensureRepositoryWritable(phase);
|
|
1402
1319
|
const code = await fn();
|
|
1403
1320
|
if (code !== 0) {
|
|
1404
1321
|
throw new Error(`Phase ${phase} failed with exit code ${code}`);
|
|
@@ -1429,7 +1346,11 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1429
1346
|
}
|
|
1430
1347
|
},
|
|
1431
1348
|
execute: makeRunner(
|
|
1432
|
-
() => runExecute(void 0, {
|
|
1349
|
+
() => runExecute(void 0, {
|
|
1350
|
+
issue: issueNumber,
|
|
1351
|
+
commitScope: queueCommitScope,
|
|
1352
|
+
...executeRetry
|
|
1353
|
+
}),
|
|
1433
1354
|
"execute"
|
|
1434
1355
|
),
|
|
1435
1356
|
review: async () => {
|
|
@@ -1453,7 +1374,8 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1453
1374
|
}
|
|
1454
1375
|
const execCode = await runExecute(void 0, {
|
|
1455
1376
|
issue: issueNumber,
|
|
1456
|
-
commitScope: queueCommitScope
|
|
1377
|
+
commitScope: queueCommitScope,
|
|
1378
|
+
...executeRetry
|
|
1457
1379
|
});
|
|
1458
1380
|
if (execCode !== 0) {
|
|
1459
1381
|
throw new Error("Correction execution failed");
|
|
@@ -1640,9 +1562,14 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
|
1640
1562
|
const startedAtMs = Date.now();
|
|
1641
1563
|
const summaries = [];
|
|
1642
1564
|
let firstOfThisRun = true;
|
|
1565
|
+
const resilience = getActiveResilienceConfig();
|
|
1566
|
+
const failureMode = options.runOptions?.onIssueFailure ?? resilience.queue?.onIssueFailure ?? "stop";
|
|
1567
|
+
const maxIssueAttempts = resilience.queue?.maxIssueAttempts ?? DEFAULT_MAX_ISSUE_ATTEMPTS;
|
|
1568
|
+
const exhausted = /* @__PURE__ */ new Set();
|
|
1569
|
+
const failedIds = /* @__PURE__ */ new Set();
|
|
1643
1570
|
try {
|
|
1644
1571
|
while (true) {
|
|
1645
|
-
const entry = nextQueueIssue(plan);
|
|
1572
|
+
const entry = nextQueueIssue(plan, { exclude: exhausted });
|
|
1646
1573
|
if (entry === null) break;
|
|
1647
1574
|
plan = markQueueIssueInProgress(plan, entry.id);
|
|
1648
1575
|
await saveExecutionPlan(planFile, plan);
|
|
@@ -1675,20 +1602,55 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
|
1675
1602
|
}
|
|
1676
1603
|
const usage = issueUsage.totals();
|
|
1677
1604
|
if (result.code !== 0) {
|
|
1678
|
-
|
|
1605
|
+
const failure2 = {
|
|
1679
1606
|
phase: result.failedPhase,
|
|
1680
1607
|
error: {
|
|
1681
1608
|
category: "queue_issue_failed",
|
|
1682
1609
|
message: `Issue #${entry.id} failed${result.failedPhase === null ? "" : ` in phase ${result.failedPhase}`}`,
|
|
1683
1610
|
at: isoNow()
|
|
1684
1611
|
}
|
|
1685
|
-
}
|
|
1612
|
+
};
|
|
1613
|
+
const where = result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`;
|
|
1614
|
+
if (failureMode === "stop") {
|
|
1615
|
+
plan = markQueueIssueFailed(plan, entry.id, failure2);
|
|
1616
|
+
await saveExecutionPlan(planFile, plan);
|
|
1617
|
+
printError(
|
|
1618
|
+
`Queue stopped at issue #${entry.id}${where}. The branch and every commit made so far were kept.`
|
|
1619
|
+
);
|
|
1620
|
+
printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
|
|
1621
|
+
return result.code;
|
|
1622
|
+
}
|
|
1623
|
+
if (failureMode === "block") {
|
|
1624
|
+
plan = markQueueIssueBlocked(
|
|
1625
|
+
plan,
|
|
1626
|
+
entry.id,
|
|
1627
|
+
`Failed${where} and --on-issue-failure block was in force`,
|
|
1628
|
+
failure2
|
|
1629
|
+
);
|
|
1630
|
+
await saveExecutionPlan(planFile, plan);
|
|
1631
|
+
exhausted.add(entry.id);
|
|
1632
|
+
failedIds.add(entry.id);
|
|
1633
|
+
printWarning(
|
|
1634
|
+
`Issue #${entry.id} failed${where} and is blocked for review. Continuing with the rest of the queue.`
|
|
1635
|
+
);
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const attempts = entry.attempts + 1;
|
|
1639
|
+
if (attempts >= maxIssueAttempts) {
|
|
1640
|
+
plan = markQueueIssueFailed(plan, entry.id, failure2);
|
|
1641
|
+
exhausted.add(entry.id);
|
|
1642
|
+
failedIds.add(entry.id);
|
|
1643
|
+
printWarning(
|
|
1644
|
+
`Issue #${entry.id} failed${where} after ${attempts} attempt(s). Continuing with the rest of the queue.`
|
|
1645
|
+
);
|
|
1646
|
+
} else {
|
|
1647
|
+
plan = markQueueIssueSkipped(plan, entry.id, failure2);
|
|
1648
|
+
printWarning(
|
|
1649
|
+
`Issue #${entry.id} failed${where}. Skipping it for now and coming back at the end of the queue.`
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1686
1652
|
await saveExecutionPlan(planFile, plan);
|
|
1687
|
-
|
|
1688
|
-
`Queue stopped at issue #${entry.id}${result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`}. The branch and every commit made so far were kept.`
|
|
1689
|
-
);
|
|
1690
|
-
printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
|
|
1691
|
-
return result.code;
|
|
1653
|
+
continue;
|
|
1692
1654
|
}
|
|
1693
1655
|
if (plan.branchName === null && result.branchName !== null) {
|
|
1694
1656
|
plan = setQueueBranch(plan, result.branchName);
|
|
@@ -1703,7 +1665,7 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
|
1703
1665
|
usage
|
|
1704
1666
|
});
|
|
1705
1667
|
}
|
|
1706
|
-
|
|
1668
|
+
const code = await finishQueue(
|
|
1707
1669
|
plan,
|
|
1708
1670
|
planFile,
|
|
1709
1671
|
summaries,
|
|
@@ -1712,6 +1674,14 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
|
1712
1674
|
options,
|
|
1713
1675
|
resolvedPrimary
|
|
1714
1676
|
);
|
|
1677
|
+
if (failedIds.size > 0) {
|
|
1678
|
+
printError(
|
|
1679
|
+
`${failedIds.size} issue(s) did not finish: ${[...failedIds].map((id) => `#${id}`).join(", ")}.`
|
|
1680
|
+
);
|
|
1681
|
+
printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
|
|
1682
|
+
return code === 0 ? 1 : code;
|
|
1683
|
+
}
|
|
1684
|
+
return code;
|
|
1715
1685
|
} finally {
|
|
1716
1686
|
queueUsage.end();
|
|
1717
1687
|
}
|
|
@@ -1853,9 +1823,10 @@ async function adoptQueueBranch(tasksPath, queueBranch) {
|
|
|
1853
1823
|
return queueBranch;
|
|
1854
1824
|
}
|
|
1855
1825
|
}
|
|
1826
|
+
|
|
1856
1827
|
export {
|
|
1857
|
-
publishIssueDetails,
|
|
1858
1828
|
publishStorySeed,
|
|
1829
|
+
publishIssueDetails,
|
|
1859
1830
|
runPipeline
|
|
1860
1831
|
};
|
|
1861
|
-
//# sourceMappingURL=
|
|
1832
|
+
//# sourceMappingURL=chunk-NTMMF5UD.js.map
|