issue-flow 0.11.1 → 0.13.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 +79 -274
- package/dist/agent-BXRIEKVM.js +238 -0
- package/dist/agent-BXRIEKVM.js.map +1 -0
- package/dist/{analyze-XV3K25K3.js → analyze-WS47UYGN.js} +32 -15
- package/dist/analyze-WS47UYGN.js.map +1 -0
- package/dist/{apply-WUJHDD7A.js → apply-OS62UWRN.js} +15 -5
- package/dist/{apply-WUJHDD7A.js.map → apply-OS62UWRN.js.map} +1 -1
- package/dist/bench-SA3BOME5.js +906 -0
- package/dist/bench-SA3BOME5.js.map +1 -0
- package/dist/chunk-37N2RJW2.js +120 -0
- package/dist/chunk-37N2RJW2.js.map +1 -0
- package/dist/chunk-3XLCNVMD.js +180 -0
- package/dist/chunk-3XLCNVMD.js.map +1 -0
- package/dist/chunk-5TB6KLC2.js +72 -0
- package/dist/chunk-5TB6KLC2.js.map +1 -0
- package/dist/{chunk-OBLVLIUS.js → chunk-664Y3NDX.js} +25 -13
- package/dist/chunk-664Y3NDX.js.map +1 -0
- package/dist/chunk-6FDFRNXZ.js +357 -0
- package/dist/chunk-6FDFRNXZ.js.map +1 -0
- package/dist/chunk-7KD66LIM.js +658 -0
- package/dist/chunk-7KD66LIM.js.map +1 -0
- package/dist/chunk-7Z4Y3VKM.js +26 -0
- package/dist/chunk-7Z4Y3VKM.js.map +1 -0
- package/dist/chunk-ARFVHC2F.js +1111 -0
- package/dist/chunk-ARFVHC2F.js.map +1 -0
- package/dist/{chunk-RKTEV6EG.js → chunk-BHJCGWQX.js} +141 -12
- package/dist/chunk-BHJCGWQX.js.map +1 -0
- package/dist/chunk-BQDQTIM7.js +74 -0
- package/dist/chunk-BQDQTIM7.js.map +1 -0
- package/dist/chunk-CNBYCMSU.js +2709 -0
- package/dist/chunk-CNBYCMSU.js.map +1 -0
- package/dist/chunk-CRGIQFUQ.js +83 -0
- package/dist/chunk-CRGIQFUQ.js.map +1 -0
- package/dist/chunk-DIG5XKQQ.js +298 -0
- package/dist/chunk-DIG5XKQQ.js.map +1 -0
- package/dist/chunk-FZDR7CGH.js +71 -0
- package/dist/chunk-FZDR7CGH.js.map +1 -0
- package/dist/chunk-HEF2RMKN.js +413 -0
- package/dist/chunk-HEF2RMKN.js.map +1 -0
- package/dist/{chunk-IVHZEYAX.js → chunk-HJGGNLWI.js} +25 -19
- package/dist/chunk-HJGGNLWI.js.map +1 -0
- package/dist/chunk-HQAHNIAR.js +283 -0
- package/dist/chunk-HQAHNIAR.js.map +1 -0
- package/dist/{chunk-GANSDJV7.js → chunk-IHIEXUTC.js} +46 -24
- package/dist/chunk-IHIEXUTC.js.map +1 -0
- package/dist/chunk-IV5SS4JN.js +84 -0
- package/dist/chunk-IV5SS4JN.js.map +1 -0
- package/dist/{chunk-HC2MT7BX.js → chunk-IYV6KNVF.js} +158 -592
- package/dist/chunk-IYV6KNVF.js.map +1 -0
- package/dist/{chunk-LVKQ2AKT.js → chunk-J62QSVP6.js} +10 -8
- package/dist/chunk-J62QSVP6.js.map +1 -0
- package/dist/chunk-KMMASOA6.js +134 -0
- package/dist/chunk-KMMASOA6.js.map +1 -0
- package/dist/chunk-KQHB7ZT3.js +559 -0
- package/dist/chunk-KQHB7ZT3.js.map +1 -0
- package/dist/chunk-L6HSKTQE.js +77 -0
- package/dist/chunk-L6HSKTQE.js.map +1 -0
- package/dist/{chunk-6N7LZZ5U.js → chunk-LZ6GFQAG.js} +474 -160
- package/dist/chunk-LZ6GFQAG.js.map +1 -0
- package/dist/{chunk-HQYXVIH6.js → chunk-MIVHK5YI.js} +5 -5
- package/dist/chunk-NEZZ6KDR.js +1217 -0
- package/dist/chunk-NEZZ6KDR.js.map +1 -0
- package/dist/{run-DBF2VWUN.js → chunk-OPNGPUKJ.js} +747 -594
- package/dist/chunk-OPNGPUKJ.js.map +1 -0
- package/dist/chunk-P3LRUNC6.js +16 -0
- package/dist/chunk-P3LRUNC6.js.map +1 -0
- package/dist/{chunk-BXCPXRBR.js → chunk-PIVGJM6K.js} +129 -32
- package/dist/chunk-PIVGJM6K.js.map +1 -0
- package/dist/{chunk-AAY6KM7N.js → chunk-PV4OX5TC.js} +167 -196
- package/dist/chunk-PV4OX5TC.js.map +1 -0
- package/dist/chunk-QCXPNSEL.js +141 -0
- package/dist/chunk-QCXPNSEL.js.map +1 -0
- package/dist/{chunk-34WNTK3K.js → chunk-QXDNBYQF.js} +53 -13
- package/dist/chunk-QXDNBYQF.js.map +1 -0
- package/dist/chunk-SNTTOQAD.js +30 -0
- package/dist/chunk-SNTTOQAD.js.map +1 -0
- package/dist/{chunk-EY6WMHV2.js → chunk-TM67EHSV.js} +67 -23
- package/dist/chunk-TM67EHSV.js.map +1 -0
- package/dist/chunk-TVN2JVOF.js +77 -0
- package/dist/chunk-TVN2JVOF.js.map +1 -0
- package/dist/chunk-TX5WIVYP.js +129 -0
- package/dist/chunk-TX5WIVYP.js.map +1 -0
- package/dist/{chunk-KQ4JQN7T.js → chunk-UJ7XWK7J.js} +83 -21
- package/dist/chunk-UJ7XWK7J.js.map +1 -0
- package/dist/{chunk-47VIN33V.js → chunk-UOCPC2Y2.js} +596 -17
- package/dist/chunk-UOCPC2Y2.js.map +1 -0
- package/dist/chunk-XEYH6ZZI.js +289 -0
- package/dist/chunk-XEYH6ZZI.js.map +1 -0
- package/dist/cli.js +390 -40
- package/dist/cli.js.map +1 -1
- package/dist/config-MEM2IWWN.js +76 -0
- package/dist/contract-JH2WBIO6.js +8 -0
- package/dist/conventions-WV4NDXGH.js +143 -0
- package/dist/conventions-WV4NDXGH.js.map +1 -0
- package/dist/execute-NITIHHCK.js +31 -0
- package/dist/{generate-TBVTLMOP.js → generate-MVQI7A45.js} +39 -19
- package/dist/{generate-TBVTLMOP.js.map → generate-MVQI7A45.js.map} +1 -1
- package/dist/git-YRVRUN3C.js +31 -0
- package/dist/init-OKWEC3EV.js +21 -0
- package/dist/operations-LU6JA3BX.js +374 -0
- package/dist/operations-LU6JA3BX.js.map +1 -0
- package/dist/permissions-7NMDFU27.js +18 -0
- package/dist/plan-LEJJF3DE.js +31 -0
- package/dist/{policy-XQW5NUIE.js → policy-JSYEK5B2.js} +17 -4
- package/dist/policy-JSYEK5B2.js.map +1 -0
- package/dist/pr-HPVUZ7S3.js +36 -0
- package/dist/pr-review-R2QZBT3F.js +29 -0
- package/dist/pr-review-R2QZBT3F.js.map +1 -0
- package/dist/prd-5O6GYCC6.js +30 -0
- package/dist/prd-5O6GYCC6.js.map +1 -0
- package/dist/ps-TNE44R5V.js +90 -0
- package/dist/ps-TNE44R5V.js.map +1 -0
- package/dist/recorder-XGF55WXI.js +31 -0
- package/dist/recorder-XGF55WXI.js.map +1 -0
- package/dist/registry-LGPE53QD.js +15 -0
- package/dist/registry-LGPE53QD.js.map +1 -0
- package/dist/resume-QMEPMSZW.js +238 -0
- package/dist/resume-QMEPMSZW.js.map +1 -0
- package/dist/review-YKWD6SNN.js +32 -0
- package/dist/review-YKWD6SNN.js.map +1 -0
- package/dist/routing-2ZUMZPJB.js +91 -0
- package/dist/routing-2ZUMZPJB.js.map +1 -0
- package/dist/run-YU4TR62R.js +50 -0
- package/dist/run-YU4TR62R.js.map +1 -0
- package/dist/state-manager-X3PT3FGT.js +36 -0
- package/dist/state-manager-X3PT3FGT.js.map +1 -0
- package/dist/usage-VCFLDA5Y.js +174 -0
- package/dist/usage-VCFLDA5Y.js.map +1 -0
- package/dist/{web-ANQQTTRD.js → web-XVHLX62B.js} +43 -27
- package/dist/web-XVHLX62B.js.map +1 -0
- package/package.json +6 -2
- package/prompts/execute.md +7 -8
- package/prompts/plan.md +4 -5
- package/prompts/pr.md +13 -6
- package/web/AGENTS.md +3 -3
- package/web/public/app.css +38 -0
- package/web/public/app.js +132 -10
- package/web/public/index.html +24 -0
- package/dist/analyze-XV3K25K3.js.map +0 -1
- package/dist/chunk-34WNTK3K.js.map +0 -1
- package/dist/chunk-3BOHTH4V.js +0 -58
- package/dist/chunk-3BOHTH4V.js.map +0 -1
- package/dist/chunk-47VIN33V.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 +0 -303
- package/dist/chunk-EWDJENFW.js.map +0 -1
- package/dist/chunk-EY6WMHV2.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-IVHZEYAX.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-RKTEV6EG.js.map +0 -1
- package/dist/chunk-VQ5VN7DF.js +0 -1018
- package/dist/chunk-VQ5VN7DF.js.map +0 -1
- package/dist/config-4THHOXNN.js +0 -41
- package/dist/execute-5OEKLM5X.js +0 -15
- package/dist/init-PRMOTQ7N.js +0 -9
- package/dist/plan-Y2BYZD24.js +0 -17
- package/dist/policy-XQW5NUIE.js.map +0 -1
- 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/web-ANQQTTRD.js.map +0 -1
- /package/dist/{chunk-HQYXVIH6.js.map → chunk-MIVHK5YI.js.map} +0 -0
- /package/dist/{config-4THHOXNN.js.map → config-MEM2IWWN.js.map} +0 -0
- /package/dist/{execute-5OEKLM5X.js.map → contract-JH2WBIO6.js.map} +0 -0
- /package/dist/{init-PRMOTQ7N.js.map → execute-NITIHHCK.js.map} +0 -0
- /package/dist/{plan-Y2BYZD24.js.map → git-YRVRUN3C.js.map} +0 -0
- /package/dist/{pr-H6GLOSXF.js.map → init-OKWEC3EV.js.map} +0 -0
- /package/dist/{pr-review-CK5FR6WX.js.map → permissions-7NMDFU27.js.map} +0 -0
- /package/dist/{prd-4PWLCGH5.js.map → plan-LEJJF3DE.js.map} +0 -0
- /package/dist/{review-QGVAHEZF.js.map → pr-HPVUZ7S3.js.map} +0 -0
|
@@ -1,580 +1,209 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "./chunk-KQ4JQN7T.js";
|
|
8
|
-
import {
|
|
9
|
-
runPrd
|
|
10
|
-
} from "./chunk-IVHZEYAX.js";
|
|
3
|
+
formatPhaseLine,
|
|
4
|
+
loadPhaseTiming,
|
|
5
|
+
snapshotTimingFields
|
|
6
|
+
} from "./chunk-37N2RJW2.js";
|
|
11
7
|
import {
|
|
12
8
|
runReview
|
|
13
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-QXDNBYQF.js";
|
|
14
10
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
PIPELINE_PHASES,
|
|
12
|
+
PIPELINE_PHASES_NO_BRANCH,
|
|
13
|
+
PIPELINE_PHASES_WITH_PR_REVIEW,
|
|
14
|
+
PipelineManager,
|
|
15
|
+
buildDependencyGraph,
|
|
16
|
+
buildExecutionPlan,
|
|
17
|
+
collectCascadeIds,
|
|
18
|
+
computeExecutionOrder,
|
|
19
|
+
describeCycles,
|
|
20
|
+
isQueueComplete,
|
|
21
|
+
loadExecutionPlan,
|
|
22
|
+
markQueueIssueBlocked,
|
|
23
|
+
markQueueIssueCompleted,
|
|
24
|
+
markQueueIssueFailed,
|
|
25
|
+
markQueueIssueInProgress,
|
|
26
|
+
markQueueIssueSkipped,
|
|
27
|
+
nextQueueIssue,
|
|
28
|
+
saveExecutionPlan,
|
|
29
|
+
setQueueBranch,
|
|
30
|
+
setQueuePullRequest
|
|
31
|
+
} from "./chunk-7KD66LIM.js";
|
|
17
32
|
import {
|
|
18
33
|
runInit
|
|
19
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-BHJCGWQX.js";
|
|
20
35
|
import {
|
|
21
36
|
printQueueSummary,
|
|
22
37
|
printRunSummary,
|
|
23
|
-
runExecute
|
|
24
|
-
|
|
38
|
+
runExecute,
|
|
39
|
+
stripMarkdown
|
|
40
|
+
} from "./chunk-LZ6GFQAG.js";
|
|
25
41
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} from "./chunk-LVKQ2AKT.js";
|
|
42
|
+
ensureWebMonitor
|
|
43
|
+
} from "./chunk-IHIEXUTC.js";
|
|
29
44
|
import {
|
|
30
45
|
runPlan
|
|
31
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-TM67EHSV.js";
|
|
32
47
|
import {
|
|
33
48
|
runPr
|
|
34
|
-
} from "./chunk-
|
|
35
|
-
import
|
|
49
|
+
} from "./chunk-PIVGJM6K.js";
|
|
50
|
+
import {
|
|
51
|
+
mostRecent,
|
|
52
|
+
prReviewDir,
|
|
53
|
+
readPrReviewIndex,
|
|
54
|
+
runPrReview
|
|
55
|
+
} from "./chunk-UJ7XWK7J.js";
|
|
56
|
+
import {
|
|
57
|
+
listPullRequests,
|
|
58
|
+
publishGitState
|
|
59
|
+
} from "./chunk-J62QSVP6.js";
|
|
60
|
+
import {
|
|
61
|
+
runPrd
|
|
62
|
+
} from "./chunk-HJGGNLWI.js";
|
|
63
|
+
import {
|
|
64
|
+
resolveCommandIssue
|
|
65
|
+
} from "./chunk-FZDR7CGH.js";
|
|
36
66
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
resolveCommandIssue,
|
|
40
|
-
uniqueIds
|
|
41
|
-
} from "./chunk-AAY6KM7N.js";
|
|
42
|
-
import "./chunk-EWDJENFW.js";
|
|
43
|
-
import "./chunk-HDV3YDTG.js";
|
|
67
|
+
getProvider
|
|
68
|
+
} from "./chunk-PV4OX5TC.js";
|
|
44
69
|
import {
|
|
45
70
|
beginUsageScope,
|
|
46
71
|
getRunUsageTotals
|
|
47
|
-
} from "./chunk-
|
|
48
|
-
import "./chunk-HQYXVIH6.js";
|
|
49
|
-
import "./chunk-47VIN33V.js";
|
|
72
|
+
} from "./chunk-664Y3NDX.js";
|
|
50
73
|
import {
|
|
51
|
-
|
|
74
|
+
assessDecomposition,
|
|
75
|
+
buildDecompositionReport,
|
|
76
|
+
proposeSubIssues,
|
|
77
|
+
resetAgentInvocationState
|
|
78
|
+
} from "./chunk-ARFVHC2F.js";
|
|
79
|
+
import {
|
|
80
|
+
JournalPublisher,
|
|
81
|
+
MultiPublisher
|
|
82
|
+
} from "./chunk-QCXPNSEL.js";
|
|
83
|
+
import {
|
|
84
|
+
describeRunAgents,
|
|
85
|
+
getActiveResilienceConfig,
|
|
86
|
+
initResilienceConfig,
|
|
52
87
|
loadIssuesConfig,
|
|
53
88
|
loadWebConfig,
|
|
89
|
+
onShutdown
|
|
90
|
+
} from "./chunk-CNBYCMSU.js";
|
|
91
|
+
import {
|
|
54
92
|
resolveIssuePaths,
|
|
55
93
|
resolveProjectPaths,
|
|
56
94
|
resolveQueuePaths
|
|
57
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-HQAHNIAR.js";
|
|
58
96
|
import {
|
|
59
97
|
FilePublisher,
|
|
60
|
-
|
|
98
|
+
MemoryPublisher,
|
|
99
|
+
formatDuration,
|
|
100
|
+
getIcons,
|
|
101
|
+
getInactivityTimeout,
|
|
102
|
+
getSessionPublisher,
|
|
61
103
|
isVerbose,
|
|
62
|
-
isoNow,
|
|
63
|
-
loadTaskPlan,
|
|
64
104
|
printError,
|
|
65
105
|
printInfo,
|
|
66
106
|
printSuccess,
|
|
67
107
|
printWarning,
|
|
68
|
-
|
|
108
|
+
setActivityCallback,
|
|
109
|
+
setInactivityTimeout,
|
|
69
110
|
setOutputCallback,
|
|
70
111
|
setSessionPublisher,
|
|
71
112
|
setStoryStageCallback,
|
|
72
|
-
setStoryUpdateCallback
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
setStoryUpdateCallback
|
|
114
|
+
} from "./chunk-IYV6KNVF.js";
|
|
115
|
+
import {
|
|
116
|
+
isoNow,
|
|
117
|
+
loadTaskPlan,
|
|
118
|
+
saveTaskPlan
|
|
119
|
+
} from "./chunk-DIG5XKQQ.js";
|
|
120
|
+
import {
|
|
121
|
+
acquireRunLock,
|
|
122
|
+
describeRunLockOwner
|
|
123
|
+
} from "./chunk-NEZZ6KDR.js";
|
|
124
|
+
import {
|
|
125
|
+
committedStoryIds,
|
|
126
|
+
describePreflight,
|
|
127
|
+
getBaseBranch,
|
|
128
|
+
preflightRepository,
|
|
129
|
+
run
|
|
130
|
+
} from "./chunk-KQHB7ZT3.js";
|
|
75
131
|
|
|
76
132
|
// src/commands/run.ts
|
|
77
133
|
import { randomUUID } from "crypto";
|
|
78
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
134
|
+
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
79
135
|
import { join } from "path";
|
|
80
136
|
import { execa } from "execa";
|
|
81
137
|
|
|
82
|
-
// src/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
var PIPELINE_PHASES_NO_BRANCH = PIPELINE_PHASES.filter(
|
|
86
|
-
(p) => p !== "pr"
|
|
87
|
-
);
|
|
88
|
-
var PHASE_TO_FIELD = {
|
|
89
|
-
init: null,
|
|
90
|
-
prd: "prdCompleted",
|
|
91
|
-
plan: "jsonCompleted",
|
|
92
|
-
execute: "executionCompleted",
|
|
93
|
-
review: "reviewCompleted",
|
|
94
|
-
pr: "prCreated",
|
|
95
|
-
"pr-review": "prReviewCompleted"
|
|
96
|
-
};
|
|
97
|
-
var PipelineManager = class {
|
|
98
|
-
tasksJsonPath;
|
|
99
|
-
plan;
|
|
100
|
-
activePhases;
|
|
101
|
-
constructor(plan, tasksJsonPath, activePhases) {
|
|
102
|
-
this.plan = plan;
|
|
103
|
-
this.tasksJsonPath = tasksJsonPath;
|
|
104
|
-
this.activePhases = activePhases ?? PIPELINE_PHASES;
|
|
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
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Get the current phase (alias for getNextPhase).
|
|
134
|
-
*/
|
|
135
|
-
getCurrentPhase() {
|
|
136
|
-
return this.getNextPhase();
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Check whether we can resume from a specific phase.
|
|
140
|
-
* All prerequisite phases must be complete.
|
|
141
|
-
*/
|
|
142
|
-
canResume(fromPhase) {
|
|
143
|
-
const idx = this.activePhases.indexOf(fromPhase);
|
|
144
|
-
if (idx < 0) return false;
|
|
145
|
-
for (let i = 0; i < idx; i++) {
|
|
146
|
-
if (!this.isPhaseComplete(this.activePhases[i])) {
|
|
147
|
-
return false;
|
|
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";
|
|
138
|
+
// src/execution/detach.ts
|
|
139
|
+
import { spawn } from "child_process";
|
|
140
|
+
import { mkdir, open } from "fs/promises";
|
|
177
141
|
import { dirname } from "path";
|
|
178
|
-
import { ZodError } from "zod";
|
|
179
142
|
|
|
180
|
-
// src/
|
|
181
|
-
|
|
182
|
-
var
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
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);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return edges;
|
|
202
|
-
}
|
|
203
|
-
function findCycles(graph) {
|
|
204
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
205
|
-
for (const { from, to } of dependencyEdges(graph)) {
|
|
206
|
-
const list = adjacency.get(from);
|
|
207
|
-
if (list === void 0) {
|
|
208
|
-
adjacency.set(from, [to]);
|
|
209
|
-
} else {
|
|
210
|
-
list.push(to);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
const cycles = [];
|
|
214
|
-
const reported = /* @__PURE__ */ new Set();
|
|
215
|
-
const state = /* @__PURE__ */ new Map();
|
|
216
|
-
const stack = [];
|
|
217
|
-
const record = (cycle) => {
|
|
218
|
-
let pivot = 0;
|
|
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);
|
|
246
|
-
}
|
|
247
|
-
return cycles;
|
|
248
|
-
}
|
|
249
|
-
function compareIds(a, b) {
|
|
250
|
-
const na = Number(a);
|
|
251
|
-
const nb = Number(b);
|
|
252
|
-
if (Number.isInteger(na) && Number.isInteger(nb)) {
|
|
253
|
-
return na - nb;
|
|
143
|
+
// src/storage/run-log.ts
|
|
144
|
+
import { rename, stat } from "fs/promises";
|
|
145
|
+
var DEFAULT_RUN_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
146
|
+
async function rotateRunLogIfNeeded(filePath, rotatedPath, maxBytes = DEFAULT_RUN_LOG_MAX_BYTES) {
|
|
147
|
+
if (maxBytes <= 0) return;
|
|
148
|
+
try {
|
|
149
|
+
const size = (await stat(filePath)).size;
|
|
150
|
+
if (size < maxBytes) return;
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
254
153
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return uniqueIds(
|
|
259
|
-
[
|
|
260
|
-
...relations.parent === null ? [] : [relations.parent],
|
|
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
|
-
}
|
|
154
|
+
try {
|
|
155
|
+
await rename(filePath, rotatedPath);
|
|
156
|
+
} catch {
|
|
346
157
|
}
|
|
347
158
|
}
|
|
348
159
|
|
|
349
|
-
// src/execution/
|
|
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];
|
|
160
|
+
// src/execution/detach.ts
|
|
161
|
+
function backgroundRejection(mode, env = process.env) {
|
|
162
|
+
if (mode === "manual") {
|
|
163
|
+
return "--background cannot be used with --mode manual: a detached run cannot answer prompts.";
|
|
365
164
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (order.length !== selected.size) {
|
|
432
|
-
const cycles = graph.cycles.length > 0 ? graph.cycles : findCycles(graph);
|
|
433
|
-
const relevant = cycles.filter((cycle) => cycle.every((id) => selected.has(id)));
|
|
434
|
-
return { ok: false, cycles: relevant.length > 0 ? relevant : cycles };
|
|
165
|
+
const ci = env.CI;
|
|
166
|
+
const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
|
|
167
|
+
if (inCi || !process.stdout.isTTY) {
|
|
168
|
+
return "--background needs an interactive terminal. In CI, run in the foreground so the exit code is visible.";
|
|
435
169
|
}
|
|
436
|
-
return
|
|
437
|
-
}
|
|
438
|
-
function describeCycles(cycles) {
|
|
439
|
-
return cycles.map((cycle) => [...cycle, cycle[0]].map((id) => `#${id}`).join(" \u2192 ")).join("; ");
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// src/execution/plan.ts
|
|
443
|
-
function describeIssue(issue, id) {
|
|
444
|
-
const parsed = Number.parseInt(id, 10);
|
|
445
|
-
return {
|
|
446
|
-
number: issue?.number ?? (Number.isNaN(parsed) ? null : parsed),
|
|
447
|
-
title: issue?.title ?? "",
|
|
448
|
-
url: issue?.remoteRef ?? null,
|
|
449
|
-
source: issue?.source ?? "github"
|
|
450
|
-
};
|
|
170
|
+
return null;
|
|
451
171
|
}
|
|
452
|
-
function
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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"
|
|
492
|
-
});
|
|
493
|
-
}
|
|
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
|
-
};
|
|
172
|
+
function childArgv(argv) {
|
|
173
|
+
const next = [];
|
|
174
|
+
for (const arg of argv) {
|
|
175
|
+
if (arg === "--background" || arg === "-d") continue;
|
|
176
|
+
next.push(arg);
|
|
177
|
+
}
|
|
178
|
+
next.push("--detached-child");
|
|
179
|
+
return next;
|
|
509
180
|
}
|
|
510
|
-
async function
|
|
511
|
-
const
|
|
181
|
+
async function spawnDetachedRun(input) {
|
|
182
|
+
const entryScript = input.entryScript ?? process.argv[1];
|
|
183
|
+
if (entryScript === void 0) {
|
|
184
|
+
throw new Error("Could not determine the CLI entry point to spawn a detached run.");
|
|
185
|
+
}
|
|
186
|
+
await mkdir(dirname(input.paths.runLogFile), { recursive: true });
|
|
187
|
+
await rotateRunLogIfNeeded(input.paths.runLogFile, input.paths.rotatedRunLogFile);
|
|
188
|
+
const log = await open(input.paths.runLogFile, "a");
|
|
512
189
|
try {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
190
|
+
const args = [entryScript, ...childArgv((input.argv ?? process.argv.slice(2)).slice())];
|
|
191
|
+
const spawnFn = input.spawnFn ?? spawn;
|
|
192
|
+
const child = spawnFn(input.execPath ?? process.execPath, args, {
|
|
193
|
+
detached: true,
|
|
194
|
+
stdio: ["ignore", log.fd, log.fd],
|
|
195
|
+
cwd: input.cwd ?? process.cwd(),
|
|
196
|
+
env: input.env ?? process.env
|
|
197
|
+
});
|
|
198
|
+
if (child.pid === void 0) {
|
|
199
|
+
throw new Error("The detached run started but reported no pid.");
|
|
519
200
|
}
|
|
520
|
-
|
|
201
|
+
child.unref();
|
|
202
|
+
return { pid: child.pid, logFile: input.paths.runLogFile };
|
|
203
|
+
} finally {
|
|
204
|
+
await log.close();
|
|
521
205
|
}
|
|
522
206
|
}
|
|
523
|
-
async function saveExecutionPlan(path, plan, now = isoNow) {
|
|
524
|
-
await mkdir(dirname(path), { recursive: true });
|
|
525
|
-
const stamped = { ...plan, updatedAt: now() };
|
|
526
|
-
await writeFileAtomic(path, `${JSON.stringify(stamped, null, 2)}
|
|
527
|
-
`);
|
|
528
|
-
}
|
|
529
|
-
function nextQueueIssue(plan) {
|
|
530
|
-
return plan.issues.find((entry) => entry.status === "in_progress") ?? plan.issues.find((entry) => entry.status === "failed") ?? plan.issues.find((entry) => entry.status === "pending") ?? null;
|
|
531
|
-
}
|
|
532
|
-
function queueStatus(plan) {
|
|
533
|
-
if (plan.issues.some((entry) => entry.status === "failed")) return "failed";
|
|
534
|
-
if (plan.issues.every((entry) => entry.status === "completed")) return "completed";
|
|
535
|
-
if (plan.issues.some((entry) => entry.status !== "pending")) return "in_progress";
|
|
536
|
-
return "pending";
|
|
537
|
-
}
|
|
538
|
-
function isQueueComplete(plan) {
|
|
539
|
-
return plan.issues.every((entry) => entry.status === "completed");
|
|
540
|
-
}
|
|
541
|
-
function updateIssue(plan, id, patch) {
|
|
542
|
-
const issues = plan.issues.map((entry) => entry.id === id ? { ...entry, ...patch } : entry);
|
|
543
|
-
const next = { ...plan, issues };
|
|
544
|
-
return { ...next, status: queueStatus(next) };
|
|
545
|
-
}
|
|
546
|
-
function markQueueIssueInProgress(plan, id, now = isoNow) {
|
|
547
|
-
const current = plan.issues.find((entry) => entry.id === id);
|
|
548
|
-
return updateIssue(plan, id, {
|
|
549
|
-
status: "in_progress",
|
|
550
|
-
// Preserved across a resume: the queue reports when the work on this Issue
|
|
551
|
-
// really began, not when it was last picked up.
|
|
552
|
-
startedAt: current?.startedAt ?? now(),
|
|
553
|
-
failedPhase: null,
|
|
554
|
-
lastError: null
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
function markQueueIssueCompleted(plan, id, now = isoNow) {
|
|
558
|
-
return updateIssue(plan, id, {
|
|
559
|
-
status: "completed",
|
|
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 };
|
|
577
|
-
}
|
|
578
207
|
|
|
579
208
|
// src/execution/confirm.ts
|
|
580
209
|
import { createInterface } from "readline";
|
|
@@ -615,16 +244,25 @@ function buildQueueSummaryLines(plan) {
|
|
|
615
244
|
const primary = plan.issues.find((entry) => entry.id === plan.id) ?? plan.issues[0];
|
|
616
245
|
const lines = [];
|
|
617
246
|
if (primary !== void 0) {
|
|
618
|
-
|
|
247
|
+
const role = primary.role === "container" ? " (container)" : "";
|
|
248
|
+
lines.push(
|
|
249
|
+
` Main issue: ${issueLabel(primary)}${primary.title ? ` ${primary.title}` : ""}${role}`
|
|
250
|
+
);
|
|
619
251
|
}
|
|
620
|
-
|
|
252
|
+
const executables = plan.issues.filter((entry) => entry.role !== "container").length;
|
|
253
|
+
const containers = plan.issues.length - executables;
|
|
254
|
+
lines.push(
|
|
255
|
+
containers > 0 ? ` Total issues: ${executables} executable + ${containers} container` : ` Total issues: ${plan.issues.length}`
|
|
256
|
+
);
|
|
621
257
|
lines.push(" Suggested order:");
|
|
622
258
|
for (const entry of plan.issues) {
|
|
623
259
|
const reason = reasonFor(entry);
|
|
624
260
|
const flag = entry.heuristic ? " ~" : "";
|
|
261
|
+
const external = entry.externalDependencies.length > 0 ? ` \u26A0 depends on ${entry.externalDependencies.map((id) => `#${id}`).join(", ")} (outside this scope)` : "";
|
|
262
|
+
const role = entry.role === "container" ? " (container)" : "";
|
|
625
263
|
const title = entry.title === "" ? "" : ` ${entry.title}`;
|
|
626
264
|
lines.push(
|
|
627
|
-
` ${entry.position}. ${issueLabel(entry)}${title}${reason === "" ? "" : ` (${reason})`}${flag}`
|
|
265
|
+
` ${entry.position}. ${issueLabel(entry)}${title}${role}${reason === "" ? "" : ` (${reason})`}${flag}${external}`
|
|
628
266
|
);
|
|
629
267
|
}
|
|
630
268
|
if (plan.issues.some((entry) => entry.heuristic)) {
|
|
@@ -635,6 +273,23 @@ function buildQueueSummaryLines(plan) {
|
|
|
635
273
|
}
|
|
636
274
|
return lines;
|
|
637
275
|
}
|
|
276
|
+
function buildScopeSummaryLine(plan, choice) {
|
|
277
|
+
if (choice === "cancel") return null;
|
|
278
|
+
const primary = plan.requested[0] ?? plan.id;
|
|
279
|
+
if (choice === "requested") {
|
|
280
|
+
return `Scope: ${plan.requested.length} requested issue(s).`;
|
|
281
|
+
}
|
|
282
|
+
if (choice === "cascade") {
|
|
283
|
+
const executables = plan.issues.filter((entry) => entry.role !== "container").length;
|
|
284
|
+
return `Scope: hierarchy of #${primary} (${executables} executable, container not implemented).`;
|
|
285
|
+
}
|
|
286
|
+
return `Scope: ${plan.issues.length} issues from the hierarchy of #${primary}.`;
|
|
287
|
+
}
|
|
288
|
+
function requestedIsContainer(plan) {
|
|
289
|
+
return plan.issues.some(
|
|
290
|
+
(entry) => plan.requested.includes(entry.id) && entry.role === "container"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
638
293
|
async function confirmQueue(plan, requestedOnlyCount, options = {}) {
|
|
639
294
|
const info = options.info ?? printInfo;
|
|
640
295
|
const warn = options.warn ?? printWarning;
|
|
@@ -643,19 +298,41 @@ async function confirmQueue(plan, requestedOnlyCount, options = {}) {
|
|
|
643
298
|
(options.stdout ?? process.stdout).write(`${line}
|
|
644
299
|
`);
|
|
645
300
|
}
|
|
301
|
+
const container = requestedIsContainer(plan);
|
|
646
302
|
if (options.only === true) {
|
|
303
|
+
if (container) {
|
|
304
|
+
warn(
|
|
305
|
+
`${plan.requested.map((id) => `#${id}`).join(", ")} is a container. --only will run it as an executable issue.`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
647
308
|
info(`--only: running just the ${requestedOnlyCount} issue(s) you asked for.`);
|
|
309
|
+
info(buildScopeSummaryLine(plan, "requested") ?? "");
|
|
648
310
|
return "requested";
|
|
649
311
|
}
|
|
312
|
+
if (options.cascade === true || options.yes === true && container) {
|
|
313
|
+
info(
|
|
314
|
+
`${options.cascade === true ? "--cascade" : "--yes"}: running the hierarchy of the container, without implementing it.`
|
|
315
|
+
);
|
|
316
|
+
info(buildScopeSummaryLine(plan, "cascade") ?? "");
|
|
317
|
+
return "cascade";
|
|
318
|
+
}
|
|
650
319
|
if (options.yes === true) {
|
|
651
320
|
info(`--yes: running the whole hierarchy (${plan.issues.length} issues).`);
|
|
321
|
+
info(buildScopeSummaryLine(plan, "all") ?? "");
|
|
652
322
|
return "all";
|
|
653
323
|
}
|
|
654
324
|
const interactive = options.interactive ?? isInteractiveByDefault();
|
|
325
|
+
if (!interactive && container) {
|
|
326
|
+
throw new QueueConfirmationError(
|
|
327
|
+
`${plan.requested.map((id) => `#${id}`).join(", ")} is a container (umbrella) and the terminal is not interactive. Re-run with --cascade to execute its children, or --only to execute just that issue.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
655
330
|
if (!interactive && options.singleRequest === true) {
|
|
656
331
|
warn(
|
|
657
332
|
`A larger structure was found, but the terminal is not interactive: running just issue ${plan.requested.map((id) => `#${id}`).join(", ")}. Re-run with --yes to execute the whole hierarchy.`
|
|
658
333
|
);
|
|
334
|
+
const summary2 = buildScopeSummaryLine(plan, "requested");
|
|
335
|
+
if (summary2) info(summary2);
|
|
659
336
|
return "requested";
|
|
660
337
|
}
|
|
661
338
|
if (!interactive) {
|
|
@@ -663,10 +340,19 @@ async function confirmQueue(plan, requestedOnlyCount, options = {}) {
|
|
|
663
340
|
"This run involves more than one issue and the terminal is not interactive. Re-run with --yes to execute the whole hierarchy, or --only to execute just the issues you informed."
|
|
664
341
|
);
|
|
665
342
|
}
|
|
666
|
-
const query = `Which scope should run? [1] Only the issues informed (${requestedOnlyCount}) [2] The whole hierarchy (${plan.issues.length}) [3] Cancel: `;
|
|
667
|
-
|
|
343
|
+
const query = container ? `Which scope should run? [1] The hierarchy of #${plan.id} (${plan.issues.filter((entry) => entry.role !== "container").length} issues) [2] Include dependencies outside it [3] Just #${plan.id} itself [4] Cancel: ` : `Which scope should run? [1] Only the issues informed (${requestedOnlyCount}) [2] The whole hierarchy (${plan.issues.length}) [3] Cancel: `;
|
|
344
|
+
const choice = await ask(
|
|
345
|
+
query,
|
|
346
|
+
options.stdin ?? process.stdin,
|
|
347
|
+
options.stdout ?? process.stdout,
|
|
348
|
+
warn,
|
|
349
|
+
container ? "container" : "plain"
|
|
350
|
+
);
|
|
351
|
+
const summary = buildScopeSummaryLine(plan, choice);
|
|
352
|
+
if (summary) info(summary);
|
|
353
|
+
return choice;
|
|
668
354
|
}
|
|
669
|
-
async function ask(query, stdin, stdout, warn) {
|
|
355
|
+
async function ask(query, stdin, stdout, warn, mode = "plain") {
|
|
670
356
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
671
357
|
const queued = [];
|
|
672
358
|
let pending = null;
|
|
@@ -708,6 +394,14 @@ async function ask(query, stdin, stdout, warn) {
|
|
|
708
394
|
return "cancel";
|
|
709
395
|
}
|
|
710
396
|
const choice = answer.trim();
|
|
397
|
+
if (mode === "container") {
|
|
398
|
+
if (choice === "" || choice === "1") return "cascade";
|
|
399
|
+
if (choice === "2") return "all";
|
|
400
|
+
if (choice === "3") return "requested";
|
|
401
|
+
if (choice === "4") return "cancel";
|
|
402
|
+
warn(`Invalid choice: '${choice}'. Enter 1, 2, 3 or 4.`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
711
405
|
if (choice === "1") return "requested";
|
|
712
406
|
if (choice === "" || choice === "2") return "all";
|
|
713
407
|
if (choice === "3") return "cancel";
|
|
@@ -750,6 +444,14 @@ async function discoverIssueGraph(roots, options = {}) {
|
|
|
750
444
|
}
|
|
751
445
|
|
|
752
446
|
// src/execution/queue.ts
|
|
447
|
+
function forceRequestedExecutable(plan) {
|
|
448
|
+
return {
|
|
449
|
+
...plan,
|
|
450
|
+
issues: plan.issues.map(
|
|
451
|
+
(entry) => plan.requested.includes(entry.id) ? { ...entry, role: "executable" } : entry
|
|
452
|
+
)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
753
455
|
async function loadResumableQueue(planFile, requested) {
|
|
754
456
|
let plan;
|
|
755
457
|
try {
|
|
@@ -834,7 +536,30 @@ async function planQueue(input) {
|
|
|
834
536
|
return { kind: "stop", code: 1 };
|
|
835
537
|
}
|
|
836
538
|
let plan = suggestedPlan;
|
|
837
|
-
if (choice === "
|
|
539
|
+
if (choice === "cascade") {
|
|
540
|
+
const include = collectCascadeIds(graph, input.requested);
|
|
541
|
+
const cascadeOrder = computeExecutionOrder(graph, { include });
|
|
542
|
+
if (!cascadeOrder.ok) {
|
|
543
|
+
warn(`Dependency cycle between issues: ${describeCycles(cascadeOrder.cycles)}.`);
|
|
544
|
+
return { kind: "stop", code: 1 };
|
|
545
|
+
}
|
|
546
|
+
if (cascadeOrder.order.filter(
|
|
547
|
+
(id) => suggestedPlan.issues.find((entry) => entry.id === id)?.role !== "container"
|
|
548
|
+
).length === 0) {
|
|
549
|
+
warn(
|
|
550
|
+
`Issue #${input.requested[0]} is a container with no executable children in scope. Re-run with --only if you really want to implement it.`
|
|
551
|
+
);
|
|
552
|
+
return { kind: "stop", code: 1 };
|
|
553
|
+
}
|
|
554
|
+
plan = buildExecutionPlan({
|
|
555
|
+
projectId: input.projectId,
|
|
556
|
+
requested: input.requested,
|
|
557
|
+
graph,
|
|
558
|
+
order: cascadeOrder.order,
|
|
559
|
+
noBranch: input.noBranch,
|
|
560
|
+
prReview: input.prReview
|
|
561
|
+
});
|
|
562
|
+
} else if (choice === "requested") {
|
|
838
563
|
if (!requestedOrder.ok) {
|
|
839
564
|
warn(
|
|
840
565
|
`Dependency cycle between the issues you informed: ${describeCycles(requestedOrder.cycles)}.`
|
|
@@ -851,14 +576,16 @@ async function planQueue(input) {
|
|
|
851
576
|
}
|
|
852
577
|
return { kind: "single" };
|
|
853
578
|
}
|
|
854
|
-
plan =
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
579
|
+
plan = forceRequestedExecutable(
|
|
580
|
+
buildExecutionPlan({
|
|
581
|
+
projectId: input.projectId,
|
|
582
|
+
requested: input.requested,
|
|
583
|
+
graph,
|
|
584
|
+
order: requestedOrder.order,
|
|
585
|
+
noBranch: input.noBranch,
|
|
586
|
+
prReview: input.prReview
|
|
587
|
+
})
|
|
588
|
+
);
|
|
862
589
|
}
|
|
863
590
|
await saveExecutionPlan(input.planFile, plan);
|
|
864
591
|
return { kind: "queue", plan, resumed: false };
|
|
@@ -902,6 +629,34 @@ function parseIssueArguments(values) {
|
|
|
902
629
|
|
|
903
630
|
// src/ui/pipeline-renderer.ts
|
|
904
631
|
import { Listr, PRESET_TIMER, PRESET_TIMESTAMP } from "listr2";
|
|
632
|
+
|
|
633
|
+
// src/ui/status-view.ts
|
|
634
|
+
function renderExecuteFocus(snapshot, options = {}) {
|
|
635
|
+
const icons = options.icons ?? getIcons();
|
|
636
|
+
const story = activeStory(snapshot);
|
|
637
|
+
if (story === void 0 && snapshot.currentActivity === null) return [];
|
|
638
|
+
const lines = [];
|
|
639
|
+
if (story !== void 0) {
|
|
640
|
+
const duration = story.durationSeconds !== null && story.durationSeconds !== void 0 ? ` ${formatDuration(story.durationSeconds)}` : "";
|
|
641
|
+
lines.push(` ${icons.pending} ${story.id} ${stripMarkdown(story.title)}${duration}`);
|
|
642
|
+
}
|
|
643
|
+
const activity = snapshot.currentActivity;
|
|
644
|
+
if (activity?.tool) {
|
|
645
|
+
const detail = activity.detail ? ` ${stripMarkdown(activity.detail)}` : "";
|
|
646
|
+
lines.push(` ${activity.tool}${detail}`);
|
|
647
|
+
}
|
|
648
|
+
return lines;
|
|
649
|
+
}
|
|
650
|
+
function activeStory(snapshot) {
|
|
651
|
+
const fromActivity = snapshot.currentActivity?.story;
|
|
652
|
+
if (fromActivity) {
|
|
653
|
+
const match = snapshot.stories.find((story) => story.id === fromActivity);
|
|
654
|
+
if (match) return match;
|
|
655
|
+
}
|
|
656
|
+
return snapshot.stories.find((story) => story.stage === "executing");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/ui/pipeline-renderer.ts
|
|
905
660
|
var PHASE_LABELS = {
|
|
906
661
|
prd: "PRD",
|
|
907
662
|
plan: "Plan",
|
|
@@ -950,6 +705,13 @@ function createActiveStoryTracker(stories, storyTasks) {
|
|
|
950
705
|
}
|
|
951
706
|
};
|
|
952
707
|
}
|
|
708
|
+
function executeExpandsStories(verbose) {
|
|
709
|
+
return verbose;
|
|
710
|
+
}
|
|
711
|
+
function paintExecuteFocus(task) {
|
|
712
|
+
const lines = renderExecuteFocus(getSessionPublisher().snapshot());
|
|
713
|
+
if (lines.length > 0) task.output = lines.join("\n");
|
|
714
|
+
}
|
|
953
715
|
function buildExecutePhaseTask(runner, tasksPath, verbose) {
|
|
954
716
|
return async (_ctx, task) => {
|
|
955
717
|
let stories;
|
|
@@ -970,6 +732,31 @@ function buildExecutePhaseTask(runner, tasksPath, verbose) {
|
|
|
970
732
|
const totalStories = stories.length;
|
|
971
733
|
const initialPassed = stories.filter((s) => s.passes).length;
|
|
972
734
|
task.title = `Execute (${initialPassed}/${totalStories} stories passing)`;
|
|
735
|
+
if (!executeExpandsStories(verbose)) {
|
|
736
|
+
setStoryUpdateCallback((updatedStories) => {
|
|
737
|
+
const passed = updatedStories.filter((s) => s.passes).length;
|
|
738
|
+
task.title = `Execute (${passed}/${totalStories} stories passing)`;
|
|
739
|
+
paintExecuteFocus(task);
|
|
740
|
+
});
|
|
741
|
+
setStoryStageCallback(() => {
|
|
742
|
+
paintExecuteFocus(task);
|
|
743
|
+
});
|
|
744
|
+
setActivityCallback(() => {
|
|
745
|
+
paintExecuteFocus(task);
|
|
746
|
+
});
|
|
747
|
+
setOutputCallback((line) => {
|
|
748
|
+
task.output = line;
|
|
749
|
+
});
|
|
750
|
+
try {
|
|
751
|
+
await runner();
|
|
752
|
+
} finally {
|
|
753
|
+
setOutputCallback(void 0);
|
|
754
|
+
setStoryUpdateCallback(void 0);
|
|
755
|
+
setStoryStageCallback(void 0);
|
|
756
|
+
setActivityCallback(void 0);
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
973
760
|
const resolvers = /* @__PURE__ */ new Map();
|
|
974
761
|
setStoryUpdateCallback((updatedStories) => {
|
|
975
762
|
const passed = updatedStories.filter((s) => s.passes).length;
|
|
@@ -1036,14 +823,14 @@ function buildExecutePhaseTask(runner, tasksPath, verbose) {
|
|
|
1036
823
|
};
|
|
1037
824
|
}
|
|
1038
825
|
async function runPipelineWithRenderer(options) {
|
|
1039
|
-
const { phases, startIndex, verbose, runners, tasksPath } = options;
|
|
826
|
+
const { phases, startIndex, verbose, runners, tasksPath, phaseSuffixes } = options;
|
|
1040
827
|
const overallStart = Date.now();
|
|
1041
828
|
let currentPhase;
|
|
1042
829
|
const renderer = selectRenderer(verbose);
|
|
1043
830
|
const isSimple = renderer === "simple";
|
|
1044
831
|
const tasks = new Listr(
|
|
1045
832
|
phases.map((phase, index) => {
|
|
1046
|
-
const label = phaseLabel(phase);
|
|
833
|
+
const label = phaseSuffixes?.[phase] ? `${phaseLabel(phase)} (${phaseSuffixes[phase]})` : phaseLabel(phase);
|
|
1047
834
|
const isExecutePhase = phase === "execute" && tasksPath;
|
|
1048
835
|
return {
|
|
1049
836
|
title: label,
|
|
@@ -1096,6 +883,7 @@ async function runPipelineWithRenderer(options) {
|
|
|
1096
883
|
setOutputCallback(void 0);
|
|
1097
884
|
setStoryUpdateCallback(void 0);
|
|
1098
885
|
setStoryStageCallback(void 0);
|
|
886
|
+
setActivityCallback(void 0);
|
|
1099
887
|
}
|
|
1100
888
|
const overallElapsedSeconds = Math.floor((Date.now() - overallStart) / 1e3);
|
|
1101
889
|
return {
|
|
@@ -1110,6 +898,35 @@ function toIssueNumber(id) {
|
|
|
1110
898
|
const parsed = Number.parseInt(id, 10);
|
|
1111
899
|
return Number.isNaN(parsed) ? null : parsed;
|
|
1112
900
|
}
|
|
901
|
+
async function publishInstrumentedPhaseEnd(publisher, phase, issueNumber, success, error) {
|
|
902
|
+
const at = isoNow();
|
|
903
|
+
const startedAt = publisher.snapshot().phases.find((entry) => entry.name === phase)?.startedAt;
|
|
904
|
+
const wallMs = startedAt === null || startedAt === void 0 ? null : Math.max(0, Date.parse(at) - Date.parse(startedAt));
|
|
905
|
+
const timing = await loadPhaseTiming(phase, wallMs);
|
|
906
|
+
publisher.publish({
|
|
907
|
+
type: "phase:end",
|
|
908
|
+
at,
|
|
909
|
+
phase,
|
|
910
|
+
success,
|
|
911
|
+
...error === void 0 ? {} : { error },
|
|
912
|
+
...snapshotTimingFields(timing)
|
|
913
|
+
});
|
|
914
|
+
if (isVerbose()) {
|
|
915
|
+
printInfo(
|
|
916
|
+
formatPhaseLine({
|
|
917
|
+
issueNumber: toIssueNumber(issueNumber) ?? issueNumber,
|
|
918
|
+
phase,
|
|
919
|
+
iteration: timing.iteration,
|
|
920
|
+
wallMs,
|
|
921
|
+
cliDurationMs: timing.cliDurationMs,
|
|
922
|
+
harnessStartupMs: timing.harnessStartupMs,
|
|
923
|
+
ttftMs: timing.ttftMs,
|
|
924
|
+
numTurns: timing.numTurns,
|
|
925
|
+
outputTokens: timing.outputTokens
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
1113
930
|
function publishStorySeed(publisher, stories, at) {
|
|
1114
931
|
if (stories.length === 0) return false;
|
|
1115
932
|
publisher.publish({ type: "stories:update", at, stories: [...stories] });
|
|
@@ -1140,6 +957,10 @@ var QUEUE_PR_PHASES_WITH_REVIEW = [
|
|
|
1140
957
|
];
|
|
1141
958
|
var RUNNABLE_QUEUE_PR_PHASES = ["pr"];
|
|
1142
959
|
var RUNNABLE_QUEUE_PR_PHASES_WITH_REVIEW = ["pr", "pr-review"];
|
|
960
|
+
function verificationForSummary(value) {
|
|
961
|
+
if (value === null || value.verdict === null) return null;
|
|
962
|
+
return { verdict: value.verdict, level: value.level };
|
|
963
|
+
}
|
|
1143
964
|
async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
|
|
1144
965
|
const outcome = { requestedChanges, recommendation: null, reportPath: null };
|
|
1145
966
|
try {
|
|
@@ -1162,6 +983,7 @@ async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
|
|
|
1162
983
|
}
|
|
1163
984
|
return outcome;
|
|
1164
985
|
}
|
|
986
|
+
var DEFAULT_MAX_ISSUE_ATTEMPTS = 2;
|
|
1165
987
|
async function runPipeline(issue, mode, from, noBranch, prReview, options = {}) {
|
|
1166
988
|
let requested;
|
|
1167
989
|
try {
|
|
@@ -1170,34 +992,112 @@ async function runPipeline(issue, mode, from, noBranch, prReview, options = {})
|
|
|
1170
992
|
printError(err instanceof Error ? err.message : String(err));
|
|
1171
993
|
return 1;
|
|
1172
994
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (first.queue === void 0) {
|
|
1181
|
-
return first.code;
|
|
995
|
+
if (options.background === true && options.detachedChild !== true) {
|
|
996
|
+
const reason = backgroundRejection(mode);
|
|
997
|
+
if (reason !== null) {
|
|
998
|
+
printError(reason);
|
|
999
|
+
return 1;
|
|
1000
|
+
}
|
|
1001
|
+
return detachAfterConfirm(requested, noBranch, prReview, options);
|
|
1182
1002
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1003
|
+
const ownership = await claimRunOwnership(requested[0], options.detachedChild === true);
|
|
1004
|
+
if (!ownership.ok) {
|
|
1005
|
+
printError(
|
|
1006
|
+
`Another issue-flow run owns this project: ${describeRunLockOwner(ownership.owner)}.`
|
|
1007
|
+
);
|
|
1008
|
+
printInfo("Wait for it to finish, or stop that process before running again.");
|
|
1009
|
+
return 1;
|
|
1010
|
+
}
|
|
1011
|
+
try {
|
|
1012
|
+
const first = await runIssueSession(requested[0], mode, {
|
|
1013
|
+
from,
|
|
1014
|
+
noBranch,
|
|
1015
|
+
prReview,
|
|
1016
|
+
requested,
|
|
1017
|
+
runOptions: options,
|
|
1018
|
+
...ownership.interruptedBy === null ? {} : { interruptedBy: ownership.interruptedBy }
|
|
1019
|
+
});
|
|
1020
|
+
if (first.queue === void 0) {
|
|
1021
|
+
return first.code;
|
|
1022
|
+
}
|
|
1023
|
+
return runQueue(
|
|
1024
|
+
first.queue.plan,
|
|
1025
|
+
{ mode, from, noBranch, prReview, runOptions: options },
|
|
1026
|
+
first.queue.resolved
|
|
1027
|
+
);
|
|
1028
|
+
} finally {
|
|
1029
|
+
await ownership.release();
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
var WRITING_PHASES = /* @__PURE__ */ new Set(["plan", "execute", "pr"]);
|
|
1033
|
+
async function ensureRepositoryWritable(phase) {
|
|
1034
|
+
if (!WRITING_PHASES.has(phase)) return;
|
|
1035
|
+
const preflight = await preflightRepository({ intent: "resume-same-phase" });
|
|
1036
|
+
if (preflight.ok) return;
|
|
1037
|
+
for (const line of describePreflight(preflight)) {
|
|
1038
|
+
printError(line);
|
|
1039
|
+
}
|
|
1040
|
+
throw new Error(`The repository is not in a state the ${phase} phase can write to`);
|
|
1041
|
+
}
|
|
1042
|
+
async function claimRunOwnership(target, detached = false) {
|
|
1043
|
+
let lockFile;
|
|
1044
|
+
try {
|
|
1045
|
+
lockFile = (await resolveProjectPaths()).runLockFile;
|
|
1046
|
+
} catch {
|
|
1047
|
+
return { ok: true, interruptedBy: null, release: async () => {
|
|
1048
|
+
} };
|
|
1049
|
+
}
|
|
1050
|
+
const result = await acquireRunLock(lockFile, { target, detached });
|
|
1051
|
+
if (!result.ok) return result;
|
|
1052
|
+
return {
|
|
1053
|
+
ok: true,
|
|
1054
|
+
interruptedBy: result.handle.reclaimedFrom,
|
|
1055
|
+
release: () => result.handle.release()
|
|
1056
|
+
};
|
|
1188
1057
|
}
|
|
1189
1058
|
async function runIssueSession(issueNumber, mode, input) {
|
|
1059
|
+
resetAgentInvocationState();
|
|
1190
1060
|
const paths = await resolveIssuePaths(issueNumber);
|
|
1061
|
+
const resilience = await initResilienceConfig();
|
|
1062
|
+
const configuredInactivity = resilience.watchdog?.inactivityTimeoutMs;
|
|
1063
|
+
if (configuredInactivity !== void 0 && getInactivityTimeout() === void 0) {
|
|
1064
|
+
setInactivityTimeout(configuredInactivity);
|
|
1065
|
+
}
|
|
1191
1066
|
const webConfig = await loadWebConfig();
|
|
1192
|
-
|
|
1193
|
-
|
|
1067
|
+
const surfaces = [];
|
|
1068
|
+
const journalEnabled = resilience.journal?.enabled === true;
|
|
1069
|
+
const persistSnapshot = webConfig.enabled || input.runOptions?.detachedChild === true;
|
|
1070
|
+
if (persistSnapshot || journalEnabled) {
|
|
1194
1071
|
await mkdir2(paths.issueDir, { recursive: true });
|
|
1195
|
-
publisher = new FilePublisher(paths.sessionFile, {
|
|
1196
|
-
logLimit: webConfig.logLimit,
|
|
1197
|
-
includeLogs: webConfig.includeLogs
|
|
1198
|
-
});
|
|
1199
1072
|
}
|
|
1073
|
+
if (persistSnapshot) {
|
|
1074
|
+
surfaces.push(
|
|
1075
|
+
new FilePublisher(paths.sessionFile, {
|
|
1076
|
+
logLimit: webConfig.logLimit,
|
|
1077
|
+
includeLogs: webConfig.includeLogs
|
|
1078
|
+
})
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (journalEnabled) {
|
|
1082
|
+
surfaces.push(
|
|
1083
|
+
new JournalPublisher(paths.eventsFile, paths.rotatedEventsFile, {
|
|
1084
|
+
logLimit: webConfig.logLimit,
|
|
1085
|
+
includeLogs: webConfig.includeLogs,
|
|
1086
|
+
...resilience.journal?.maxFileBytes === void 0 ? {} : { maxFileBytes: resilience.journal.maxFileBytes }
|
|
1087
|
+
})
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
const publisher = surfaces.length === 0 ? new MemoryPublisher() : surfaces.length === 1 ? surfaces[0] : new MultiPublisher(surfaces);
|
|
1200
1091
|
setSessionPublisher(publisher);
|
|
1092
|
+
if (input.interruptedBy !== void 0) {
|
|
1093
|
+
const previous = input.interruptedBy;
|
|
1094
|
+
publisher.publish({
|
|
1095
|
+
type: "log",
|
|
1096
|
+
at: isoNow(),
|
|
1097
|
+
level: "warn",
|
|
1098
|
+
message: `Previous run interrupted: ${describeRunLockOwner(previous)}. Its lock was stale and has been taken over.`
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1201
1101
|
if (webConfig.enabled) {
|
|
1202
1102
|
await ensureWebMonitor({
|
|
1203
1103
|
publisher,
|
|
@@ -1213,10 +1113,34 @@ async function runIssueSession(issueNumber, mode, input) {
|
|
|
1213
1113
|
storyCount: 0,
|
|
1214
1114
|
elapsedSeconds: 0
|
|
1215
1115
|
};
|
|
1116
|
+
const releaseCheckpoint = onShutdown({
|
|
1117
|
+
phase: "checkpoint",
|
|
1118
|
+
run: async () => {
|
|
1119
|
+
await pauseIssue(paths.tasksFile, issueNumber);
|
|
1120
|
+
publisher.publish({
|
|
1121
|
+
type: "log",
|
|
1122
|
+
at: isoNow(),
|
|
1123
|
+
level: "warn",
|
|
1124
|
+
message: `Interrupted during issue #${issueNumber}. A checkpoint was saved; resume with \`issue-flow resume ${issueNumber}\`.`
|
|
1125
|
+
});
|
|
1126
|
+
publisher.publish({ type: "session:end", at: isoNow(), status: "failed" });
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
const releaseClose = onShutdown({
|
|
1130
|
+
phase: "close",
|
|
1131
|
+
run: async () => {
|
|
1132
|
+
await publisher.close();
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1216
1135
|
try {
|
|
1217
1136
|
result = await runPipelinePhases(issueNumber, paths, mode, publisher, input);
|
|
1137
|
+
if (result.code !== 0) {
|
|
1138
|
+
await reportIfOversized(issueNumber, paths, result);
|
|
1139
|
+
}
|
|
1218
1140
|
return result;
|
|
1219
1141
|
} finally {
|
|
1142
|
+
releaseCheckpoint();
|
|
1143
|
+
releaseClose();
|
|
1220
1144
|
if (result.queue === void 0) {
|
|
1221
1145
|
publisher.publish({
|
|
1222
1146
|
type: "session:end",
|
|
@@ -1228,10 +1152,127 @@ async function runIssueSession(issueNumber, mode, input) {
|
|
|
1228
1152
|
setSessionPublisher(void 0);
|
|
1229
1153
|
}
|
|
1230
1154
|
}
|
|
1155
|
+
async function reportIfOversized(issueNumber, paths, result) {
|
|
1156
|
+
try {
|
|
1157
|
+
const journal = `${await readFileIfPresent(paths.rotatedEventsFile)}${await readFileIfPresent(paths.eventsFile)}`;
|
|
1158
|
+
const plan = await loadTaskPlan(paths.tasksFile).catch(() => null);
|
|
1159
|
+
const assessment = assessDecomposition({
|
|
1160
|
+
journal,
|
|
1161
|
+
plan,
|
|
1162
|
+
filesTouched: await countChangedFiles(),
|
|
1163
|
+
hitMaxIterations: result.failedPhase === "execute" && plan?.issueStatus !== "completed"
|
|
1164
|
+
});
|
|
1165
|
+
if (!assessment.oversized) return;
|
|
1166
|
+
const report = buildDecompositionReport({
|
|
1167
|
+
issueNumber,
|
|
1168
|
+
assessment,
|
|
1169
|
+
plan,
|
|
1170
|
+
at: isoNow()
|
|
1171
|
+
});
|
|
1172
|
+
const reportFile = paths.decompositionFile;
|
|
1173
|
+
await mkdir2(paths.issueDir, { recursive: true });
|
|
1174
|
+
await writeFile(reportFile, report, "utf-8");
|
|
1175
|
+
printWarning(`Issue #${issueNumber} looks larger than one run. Report: ${reportFile}`);
|
|
1176
|
+
for (const signal of assessment.signals) {
|
|
1177
|
+
printWarning(` ${signal.detail}`);
|
|
1178
|
+
}
|
|
1179
|
+
await maybeCreateSubIssues(issueNumber, plan, reportFile);
|
|
1180
|
+
if (plan !== null) {
|
|
1181
|
+
await saveTaskPlan(paths.tasksFile, {
|
|
1182
|
+
...plan,
|
|
1183
|
+
runState: {
|
|
1184
|
+
currentPhase: plan.runState?.currentPhase ?? result.failedPhase,
|
|
1185
|
+
attempt: plan.runState?.attempt ?? 0,
|
|
1186
|
+
owner: null,
|
|
1187
|
+
status: "blocked",
|
|
1188
|
+
blockedReason: `Looks larger than one run; see ${reportFile}`,
|
|
1189
|
+
lastHeartbeatAt: isoNow()
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
async function maybeCreateSubIssues(issueNumber, plan, reportFile) {
|
|
1197
|
+
if (getActiveResilienceConfig().decompose?.auto !== true) {
|
|
1198
|
+
printInfo(`Nothing was split. Read ${reportFile} and decide.`);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const committed = await committedStoryIds(await getBaseBranch()).catch(() => /* @__PURE__ */ new Set());
|
|
1202
|
+
if (committed.size > 0) {
|
|
1203
|
+
printWarning(
|
|
1204
|
+
`--auto-decompose did not run: ${committed.size} story(ies) are already committed on this branch. Splitting on top of committed work needs a person.`
|
|
1205
|
+
);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const proposals = proposeSubIssues(plan);
|
|
1209
|
+
if (proposals.length === 0) {
|
|
1210
|
+
printInfo("Nothing left to split: no pending story.");
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const { runGenerate } = await import("./generate-MVQI7A45.js");
|
|
1214
|
+
for (const proposal of proposals) {
|
|
1215
|
+
const instruction = [
|
|
1216
|
+
`Create a sub-issue of #${issueNumber}: ${proposal.title}.`,
|
|
1217
|
+
proposal.dependsOn.length === 0 ? "" : `It depends on: ${proposal.dependsOn.join(", ")}.`,
|
|
1218
|
+
"It covers exactly these user stories, and nothing else:",
|
|
1219
|
+
...proposal.stories.map((story) => `- ${story.id} ${story.title}: ${story.description}`)
|
|
1220
|
+
].filter((line) => line !== "").join("\n");
|
|
1221
|
+
const code = await runGenerate(instruction);
|
|
1222
|
+
if (code !== 0) {
|
|
1223
|
+
printWarning(
|
|
1224
|
+
`Could not create "${proposal.title}". The report at ${reportFile} still stands.`
|
|
1225
|
+
);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async function countChangedFiles() {
|
|
1231
|
+
try {
|
|
1232
|
+
const base = await getBaseBranch();
|
|
1233
|
+
const result = await run("git", ["diff", "--name-only", `${base}...HEAD`]);
|
|
1234
|
+
if (result.exitCode !== 0) return 0;
|
|
1235
|
+
return result.stdout.split("\n").filter((line) => line.trim() !== "").length;
|
|
1236
|
+
} catch {
|
|
1237
|
+
return 0;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
async function readFileIfPresent(path) {
|
|
1241
|
+
try {
|
|
1242
|
+
return await readFile(path, "utf-8");
|
|
1243
|
+
} catch {
|
|
1244
|
+
return "";
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
async function pauseIssue(tasksFile, issueNumber) {
|
|
1248
|
+
try {
|
|
1249
|
+
const plan = await loadTaskPlan(tasksFile);
|
|
1250
|
+
await saveTaskPlan(tasksFile, {
|
|
1251
|
+
...plan,
|
|
1252
|
+
runState: {
|
|
1253
|
+
...plan.runState ?? {
|
|
1254
|
+
currentPhase: null,
|
|
1255
|
+
attempt: 0,
|
|
1256
|
+
lastHeartbeatAt: null,
|
|
1257
|
+
blockedReason: null,
|
|
1258
|
+
owner: null
|
|
1259
|
+
},
|
|
1260
|
+
status: "paused",
|
|
1261
|
+
lastHeartbeatAt: isoNow()
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
} catch {
|
|
1265
|
+
printWarning(`Could not write a checkpoint for issue #${issueNumber}.`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1231
1268
|
async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
1232
1269
|
const { from, noBranch, prReview, queue } = input;
|
|
1233
1270
|
const continueNumbering = input.runOptions?.continueNumbering;
|
|
1234
1271
|
const startUs = input.runOptions?.startUs;
|
|
1272
|
+
const executeRetry = {
|
|
1273
|
+
retryLimit: input.runOptions?.retryLimit,
|
|
1274
|
+
retryForever: input.runOptions?.retryForever
|
|
1275
|
+
};
|
|
1235
1276
|
const tasksPath = paths.tasksFile;
|
|
1236
1277
|
const sessionId = randomUUID();
|
|
1237
1278
|
let publishedIssueNumber = toIssueNumber(issueNumber);
|
|
@@ -1244,15 +1285,27 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1244
1285
|
issueUrl: info?.issueUrl,
|
|
1245
1286
|
branch: info?.branch,
|
|
1246
1287
|
phases: [...phases],
|
|
1247
|
-
environment: {
|
|
1288
|
+
environment: {
|
|
1289
|
+
node: process.version,
|
|
1290
|
+
platform: process.platform,
|
|
1291
|
+
agent: agentSummary.defaultProvider,
|
|
1292
|
+
model: agentSummary.defaultModel
|
|
1293
|
+
}
|
|
1248
1294
|
});
|
|
1249
1295
|
};
|
|
1250
|
-
|
|
1296
|
+
const agentSummary = await describeRunAgents(
|
|
1297
|
+
prReview ? ["prd", "plan", "execute", "review", "pr", "pr-review"] : ["prd", "plan", "execute", "review", "pr"]
|
|
1298
|
+
);
|
|
1299
|
+
printInfo(
|
|
1300
|
+
`Starting pipeline for issue #${issueNumber} (mode: ${mode}, agent: ${agentSummary.label})`
|
|
1301
|
+
);
|
|
1251
1302
|
const issuesConfig = await loadIssuesConfig();
|
|
1252
1303
|
const sessionStartedAt = isoNow();
|
|
1253
1304
|
if (queue?.preChecked !== true) {
|
|
1254
|
-
|
|
1255
|
-
|
|
1305
|
+
if (isVerbose()) {
|
|
1306
|
+
printInfo("Running prerequisite checks...");
|
|
1307
|
+
}
|
|
1308
|
+
const initCode = await runInit(issuesConfig.preferredProvider, { compact: !isVerbose() });
|
|
1256
1309
|
if (initCode !== 0) {
|
|
1257
1310
|
publishSessionStart(PIPELINE_PHASES, sessionStartedAt);
|
|
1258
1311
|
publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
|
|
@@ -1399,6 +1452,7 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1399
1452
|
}
|
|
1400
1453
|
const startIdx = Math.max(phaseOrder.indexOf(startPhase), 0);
|
|
1401
1454
|
const makeRunner = (fn, phase) => async () => {
|
|
1455
|
+
await ensureRepositoryWritable(phase);
|
|
1402
1456
|
const code = await fn();
|
|
1403
1457
|
if (code !== 0) {
|
|
1404
1458
|
throw new Error(`Phase ${phase} failed with exit code ${code}`);
|
|
@@ -1411,25 +1465,28 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1411
1465
|
() => runPlan(issueNumber, resolvedIssue, { continueFlag: continueNumbering, startUs }),
|
|
1412
1466
|
"plan"
|
|
1413
1467
|
)();
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1468
|
+
try {
|
|
1469
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1470
|
+
publishStorySeed(publisher, plan.userStories, isoNow());
|
|
1471
|
+
if (effectiveNoBranch || effectivePrReview) {
|
|
1472
|
+
if (effectiveNoBranch) plan.noBranch = true;
|
|
1420
1473
|
if (effectivePrReview) {
|
|
1421
1474
|
plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
|
|
1422
1475
|
}
|
|
1423
1476
|
await saveTaskPlan(tasksPath, plan);
|
|
1424
|
-
} catch {
|
|
1425
1477
|
}
|
|
1478
|
+
} catch {
|
|
1426
1479
|
}
|
|
1427
1480
|
if (queue !== void 0 && !effectiveNoBranch) {
|
|
1428
1481
|
producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
|
|
1429
1482
|
}
|
|
1430
1483
|
},
|
|
1431
1484
|
execute: makeRunner(
|
|
1432
|
-
() => runExecute(void 0, {
|
|
1485
|
+
() => runExecute(void 0, {
|
|
1486
|
+
issue: issueNumber,
|
|
1487
|
+
commitScope: queueCommitScope,
|
|
1488
|
+
...executeRetry
|
|
1489
|
+
}),
|
|
1433
1490
|
"execute"
|
|
1434
1491
|
),
|
|
1435
1492
|
review: async () => {
|
|
@@ -1453,7 +1510,8 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1453
1510
|
}
|
|
1454
1511
|
const execCode = await runExecute(void 0, {
|
|
1455
1512
|
issue: issueNumber,
|
|
1456
|
-
commitScope: queueCommitScope
|
|
1513
|
+
commitScope: queueCommitScope,
|
|
1514
|
+
...executeRetry
|
|
1457
1515
|
});
|
|
1458
1516
|
if (execCode !== 0) {
|
|
1459
1517
|
throw new Error("Correction execution failed");
|
|
@@ -1489,27 +1547,34 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1489
1547
|
try {
|
|
1490
1548
|
await fn();
|
|
1491
1549
|
await publishGitState(publisher);
|
|
1492
|
-
|
|
1550
|
+
await publishInstrumentedPhaseEnd(publisher, phase, issueNumber, true);
|
|
1493
1551
|
} catch (err) {
|
|
1494
1552
|
await publishGitState(publisher);
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
at: isoNow(),
|
|
1553
|
+
await publishInstrumentedPhaseEnd(
|
|
1554
|
+
publisher,
|
|
1498
1555
|
phase,
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1556
|
+
issueNumber,
|
|
1557
|
+
false,
|
|
1558
|
+
err instanceof Error ? err.message : String(err)
|
|
1559
|
+
);
|
|
1502
1560
|
throw err;
|
|
1503
1561
|
}
|
|
1504
1562
|
}
|
|
1505
1563
|
])
|
|
1506
1564
|
);
|
|
1565
|
+
const phaseSuffixes = {};
|
|
1566
|
+
for (const [phase, resolved] of Object.entries(agentSummary.byPhase)) {
|
|
1567
|
+
if (resolved.provider !== agentSummary.defaultProvider) {
|
|
1568
|
+
phaseSuffixes[phase] = resolved.model ? `${resolved.provider} \xB7 ${resolved.model}` : resolved.provider;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1507
1571
|
const result = await runPipelineWithRenderer({
|
|
1508
1572
|
phases: phaseOrder,
|
|
1509
1573
|
startIndex: startIdx,
|
|
1510
1574
|
verbose: isVerbose(),
|
|
1511
1575
|
runners: instrumentedRunners,
|
|
1512
|
-
tasksPath
|
|
1576
|
+
tasksPath,
|
|
1577
|
+
phaseSuffixes
|
|
1513
1578
|
});
|
|
1514
1579
|
if (!result.success) {
|
|
1515
1580
|
printError(`Phase ${result.failedPhase} failed`);
|
|
@@ -1590,7 +1655,8 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1590
1655
|
prReview: review,
|
|
1591
1656
|
// Process-owned counters: the session snapshot is empty whenever web
|
|
1592
1657
|
// monitoring is off, so it cannot be the source of these totals.
|
|
1593
|
-
usage: getRunUsageTotals()
|
|
1658
|
+
usage: getRunUsageTotals(),
|
|
1659
|
+
verification: verificationForSummary(getSessionPublisher().snapshot().verification)
|
|
1594
1660
|
});
|
|
1595
1661
|
return {
|
|
1596
1662
|
code: 0,
|
|
@@ -1600,6 +1666,37 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
|
1600
1666
|
elapsedSeconds: result.overallElapsedSeconds
|
|
1601
1667
|
};
|
|
1602
1668
|
}
|
|
1669
|
+
async function detachAfterConfirm(requested, noBranch, prReview, options) {
|
|
1670
|
+
const childFlags = [...process.argv.slice(2)];
|
|
1671
|
+
if (options.yes !== true && options.only !== true) {
|
|
1672
|
+
try {
|
|
1673
|
+
const issuesConfig = await loadIssuesConfig();
|
|
1674
|
+
const resolution = await resolveCommandIssue(requested[0], void 0, {
|
|
1675
|
+
config: issuesConfig
|
|
1676
|
+
});
|
|
1677
|
+
if (resolution.ok) {
|
|
1678
|
+
const decision = await decideQueue({
|
|
1679
|
+
requested,
|
|
1680
|
+
resolved: resolution.resolved,
|
|
1681
|
+
noBranch: noBranch ?? false,
|
|
1682
|
+
prReview: prReview ?? false,
|
|
1683
|
+
runOptions: options
|
|
1684
|
+
});
|
|
1685
|
+
if (decision.kind === "stop") return decision.code;
|
|
1686
|
+
if (decision.kind === "queue" && !childFlags.includes("--yes")) childFlags.push("--yes");
|
|
1687
|
+
if (decision.kind === "single" && !childFlags.includes("--only")) childFlags.push("--only");
|
|
1688
|
+
}
|
|
1689
|
+
} catch {
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const paths = await resolveIssuePaths(requested[0]);
|
|
1693
|
+
const { pid, logFile } = await spawnDetachedRun({ paths, argv: childFlags });
|
|
1694
|
+
printSuccess(`Detached run started (pid ${pid}).`);
|
|
1695
|
+
printInfo(`Log: ${logFile}`);
|
|
1696
|
+
printInfo("Follow with: issue-flow ps");
|
|
1697
|
+
printInfo(`Stop with: kill ${pid}`);
|
|
1698
|
+
return 0;
|
|
1699
|
+
}
|
|
1603
1700
|
async function decideQueue(input) {
|
|
1604
1701
|
const primary = input.requested[0];
|
|
1605
1702
|
const single = input.requested.length === 1;
|
|
@@ -1619,7 +1716,11 @@ async function decideQueue(input) {
|
|
|
1619
1716
|
planFile: queuePaths.planFile,
|
|
1620
1717
|
noBranch: input.noBranch,
|
|
1621
1718
|
prReview: input.prReview,
|
|
1622
|
-
confirm: {
|
|
1719
|
+
confirm: {
|
|
1720
|
+
yes: input.runOptions.yes,
|
|
1721
|
+
only: input.runOptions.only,
|
|
1722
|
+
cascade: input.runOptions.cascade
|
|
1723
|
+
}
|
|
1623
1724
|
});
|
|
1624
1725
|
} catch (err) {
|
|
1625
1726
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1640,9 +1741,14 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
|
1640
1741
|
const startedAtMs = Date.now();
|
|
1641
1742
|
const summaries = [];
|
|
1642
1743
|
let firstOfThisRun = true;
|
|
1744
|
+
const resilience = getActiveResilienceConfig();
|
|
1745
|
+
const failureMode = options.runOptions?.onIssueFailure ?? resilience.queue?.onIssueFailure ?? "stop";
|
|
1746
|
+
const maxIssueAttempts = resilience.queue?.maxIssueAttempts ?? DEFAULT_MAX_ISSUE_ATTEMPTS;
|
|
1747
|
+
const exhausted = /* @__PURE__ */ new Set();
|
|
1748
|
+
const failedIds = /* @__PURE__ */ new Set();
|
|
1643
1749
|
try {
|
|
1644
1750
|
while (true) {
|
|
1645
|
-
const entry = nextQueueIssue(plan);
|
|
1751
|
+
const entry = nextQueueIssue(plan, { exclude: exhausted });
|
|
1646
1752
|
if (entry === null) break;
|
|
1647
1753
|
plan = markQueueIssueInProgress(plan, entry.id);
|
|
1648
1754
|
await saveExecutionPlan(planFile, plan);
|
|
@@ -1675,20 +1781,55 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
|
1675
1781
|
}
|
|
1676
1782
|
const usage = issueUsage.totals();
|
|
1677
1783
|
if (result.code !== 0) {
|
|
1678
|
-
|
|
1784
|
+
const failure2 = {
|
|
1679
1785
|
phase: result.failedPhase,
|
|
1680
1786
|
error: {
|
|
1681
1787
|
category: "queue_issue_failed",
|
|
1682
1788
|
message: `Issue #${entry.id} failed${result.failedPhase === null ? "" : ` in phase ${result.failedPhase}`}`,
|
|
1683
1789
|
at: isoNow()
|
|
1684
1790
|
}
|
|
1685
|
-
}
|
|
1791
|
+
};
|
|
1792
|
+
const where = result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`;
|
|
1793
|
+
if (failureMode === "stop") {
|
|
1794
|
+
plan = markQueueIssueFailed(plan, entry.id, failure2);
|
|
1795
|
+
await saveExecutionPlan(planFile, plan);
|
|
1796
|
+
printError(
|
|
1797
|
+
`Queue stopped at issue #${entry.id}${where}. The branch and every commit made so far were kept.`
|
|
1798
|
+
);
|
|
1799
|
+
printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
|
|
1800
|
+
return result.code;
|
|
1801
|
+
}
|
|
1802
|
+
if (failureMode === "block") {
|
|
1803
|
+
plan = markQueueIssueBlocked(
|
|
1804
|
+
plan,
|
|
1805
|
+
entry.id,
|
|
1806
|
+
`Failed${where} and --on-issue-failure block was in force`,
|
|
1807
|
+
failure2
|
|
1808
|
+
);
|
|
1809
|
+
await saveExecutionPlan(planFile, plan);
|
|
1810
|
+
exhausted.add(entry.id);
|
|
1811
|
+
failedIds.add(entry.id);
|
|
1812
|
+
printWarning(
|
|
1813
|
+
`Issue #${entry.id} failed${where} and is blocked for review. Continuing with the rest of the queue.`
|
|
1814
|
+
);
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
const attempts = entry.attempts + 1;
|
|
1818
|
+
if (attempts >= maxIssueAttempts) {
|
|
1819
|
+
plan = markQueueIssueFailed(plan, entry.id, failure2);
|
|
1820
|
+
exhausted.add(entry.id);
|
|
1821
|
+
failedIds.add(entry.id);
|
|
1822
|
+
printWarning(
|
|
1823
|
+
`Issue #${entry.id} failed${where} after ${attempts} attempt(s). Continuing with the rest of the queue.`
|
|
1824
|
+
);
|
|
1825
|
+
} else {
|
|
1826
|
+
plan = markQueueIssueSkipped(plan, entry.id, failure2);
|
|
1827
|
+
printWarning(
|
|
1828
|
+
`Issue #${entry.id} failed${where}. Skipping it for now and coming back at the end of the queue.`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1686
1831
|
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;
|
|
1832
|
+
continue;
|
|
1692
1833
|
}
|
|
1693
1834
|
if (plan.branchName === null && result.branchName !== null) {
|
|
1694
1835
|
plan = setQueueBranch(plan, result.branchName);
|
|
@@ -1703,7 +1844,7 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
|
1703
1844
|
usage
|
|
1704
1845
|
});
|
|
1705
1846
|
}
|
|
1706
|
-
|
|
1847
|
+
const code = await finishQueue(
|
|
1707
1848
|
plan,
|
|
1708
1849
|
planFile,
|
|
1709
1850
|
summaries,
|
|
@@ -1712,6 +1853,14 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
|
1712
1853
|
options,
|
|
1713
1854
|
resolvedPrimary
|
|
1714
1855
|
);
|
|
1856
|
+
if (failedIds.size > 0) {
|
|
1857
|
+
printError(
|
|
1858
|
+
`${failedIds.size} issue(s) did not finish: ${[...failedIds].map((id) => `#${id}`).join(", ")}.`
|
|
1859
|
+
);
|
|
1860
|
+
printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
|
|
1861
|
+
return code === 0 ? 1 : code;
|
|
1862
|
+
}
|
|
1863
|
+
return code;
|
|
1715
1864
|
} finally {
|
|
1716
1865
|
queueUsage.end();
|
|
1717
1866
|
}
|
|
@@ -1790,7 +1939,10 @@ async function buildPrQueueContext(plan) {
|
|
|
1790
1939
|
number: entry.number,
|
|
1791
1940
|
title: entry.title,
|
|
1792
1941
|
url: entry.url,
|
|
1793
|
-
source: entry.source
|
|
1942
|
+
source: entry.source,
|
|
1943
|
+
parent: entry.parent,
|
|
1944
|
+
role: entry.role,
|
|
1945
|
+
complete: entry.status === "completed"
|
|
1794
1946
|
})),
|
|
1795
1947
|
excluded: plan.excluded.map((entry) => ({
|
|
1796
1948
|
id: entry.id,
|
|
@@ -1853,9 +2005,10 @@ async function adoptQueueBranch(tasksPath, queueBranch) {
|
|
|
1853
2005
|
return queueBranch;
|
|
1854
2006
|
}
|
|
1855
2007
|
}
|
|
2008
|
+
|
|
1856
2009
|
export {
|
|
1857
|
-
publishIssueDetails,
|
|
1858
2010
|
publishStorySeed,
|
|
2011
|
+
publishIssueDetails,
|
|
1859
2012
|
runPipeline
|
|
1860
2013
|
};
|
|
1861
|
-
//# sourceMappingURL=
|
|
2014
|
+
//# sourceMappingURL=chunk-OPNGPUKJ.js.map
|