patchrelay 0.8.8 → 0.9.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 +64 -62
- package/dist/agent-session-plan.js +17 -17
- package/dist/build-info.json +3 -3
- package/dist/cli/commands/issues.js +12 -12
- package/dist/cli/data.js +109 -298
- package/dist/cli/formatters/text.js +22 -28
- package/dist/config.js +13 -166
- package/dist/db/migrations.js +46 -154
- package/dist/db.js +369 -45
- package/dist/factory-state.js +55 -0
- package/dist/github-webhook-handler.js +199 -0
- package/dist/github-webhooks.js +166 -0
- package/dist/hook-runner.js +28 -0
- package/dist/http.js +48 -22
- package/dist/issue-query-service.js +33 -38
- package/dist/linear-workflow.js +5 -118
- package/dist/preflight.js +1 -6
- package/dist/project-resolution.js +12 -1
- package/dist/run-orchestrator.js +446 -0
- package/dist/{stage-reporting.js → run-reporting.js} +11 -13
- package/dist/service-runtime.js +21 -54
- package/dist/service-webhooks.js +7 -52
- package/dist/service.js +39 -61
- package/dist/webhook-handler.js +387 -0
- package/dist/webhook-installation-handler.js +3 -8
- package/package.json +2 -1
- package/dist/db/authoritative-ledger-store.js +0 -536
- package/dist/db/issue-projection-store.js +0 -54
- package/dist/db/issue-workflow-coordinator.js +0 -320
- package/dist/db/issue-workflow-store.js +0 -194
- package/dist/db/run-report-store.js +0 -33
- package/dist/db/stage-event-store.js +0 -33
- package/dist/db/webhook-event-store.js +0 -59
- package/dist/db-ports.js +0 -5
- package/dist/ledger-ports.js +0 -1
- package/dist/reconciliation-action-applier.js +0 -68
- package/dist/reconciliation-actions.js +0 -1
- package/dist/reconciliation-engine.js +0 -350
- package/dist/reconciliation-snapshot-builder.js +0 -135
- package/dist/reconciliation-types.js +0 -1
- package/dist/service-stage-finalizer.js +0 -753
- package/dist/service-stage-runner.js +0 -336
- package/dist/service-webhook-processor.js +0 -411
- package/dist/stage-agent-activity-publisher.js +0 -59
- package/dist/stage-event-ports.js +0 -1
- package/dist/stage-failure.js +0 -92
- package/dist/stage-handoff.js +0 -107
- package/dist/stage-launch.js +0 -84
- package/dist/stage-lifecycle-publisher.js +0 -284
- package/dist/stage-turn-input-dispatcher.js +0 -104
- package/dist/webhook-agent-session-handler.js +0 -228
- package/dist/webhook-comment-handler.js +0 -141
- package/dist/webhook-desired-stage-recorder.js +0 -122
- package/dist/webhook-event-ports.js +0 -1
- package/dist/workflow-policy.js +0 -149
- package/dist/workflow-ports.js +0 -1
- /package/dist/{installation-ports.js → github-types.js} +0 -0
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
export class ReconciliationActionApplier {
|
|
2
|
-
callbacks;
|
|
3
|
-
constructor(callbacks) {
|
|
4
|
-
this.callbacks = callbacks;
|
|
5
|
-
}
|
|
6
|
-
async apply(params) {
|
|
7
|
-
const { snapshot, decision } = params;
|
|
8
|
-
const threadId = snapshot.runLease.threadId;
|
|
9
|
-
const turnId = snapshot.runLease.turnId;
|
|
10
|
-
const obligationTargetAction = decision.actions.find((action) => action.type === "deliver_obligation" || action.type === "route_obligation");
|
|
11
|
-
const targetThreadId = obligationTargetAction?.type === "deliver_obligation" || obligationTargetAction?.type === "route_obligation"
|
|
12
|
-
? obligationTargetAction.threadId
|
|
13
|
-
: threadId;
|
|
14
|
-
const targetTurnId = obligationTargetAction?.type === "deliver_obligation" || obligationTargetAction?.type === "route_obligation"
|
|
15
|
-
? obligationTargetAction.turnId
|
|
16
|
-
: turnId;
|
|
17
|
-
const clearAction = decision.actions.find((action) => action.type === "clear_active_run" || action.type === "release_issue_ownership");
|
|
18
|
-
const nextLifecycleStatus = clearAction?.type === "clear_active_run" || clearAction?.type === "release_issue_ownership"
|
|
19
|
-
? clearAction.nextLifecycleStatus
|
|
20
|
-
: undefined;
|
|
21
|
-
if (decision.outcome === "launch") {
|
|
22
|
-
this.callbacks.enqueueIssue(snapshot.runLease.projectId, snapshot.runLease.linearIssueId);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
if (decision.outcome === "continue") {
|
|
26
|
-
if (targetThreadId) {
|
|
27
|
-
await this.callbacks.deliverPendingObligations(snapshot.runLease.projectId, snapshot.runLease.linearIssueId, targetThreadId, targetTurnId);
|
|
28
|
-
}
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const completedAction = decision.actions.find((action) => action.type === "mark_run_completed");
|
|
32
|
-
if (decision.outcome === "complete" || (decision.outcome === "release" && completedAction?.type === "mark_run_completed")) {
|
|
33
|
-
const liveThread = snapshot.input.live?.codex?.status === "found" ? snapshot.input.live.codex.thread : undefined;
|
|
34
|
-
if (!liveThread) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const latestTurn = liveThread.turns.at(-1);
|
|
38
|
-
this.callbacks.completeRun(snapshot.runLease.projectId, snapshot.runLease.linearIssueId, liveThread, {
|
|
39
|
-
threadId: liveThread.id,
|
|
40
|
-
...(latestTurn?.id ? { turnId: latestTurn.id } : {}),
|
|
41
|
-
...(nextLifecycleStatus ? { nextLifecycleStatus } : {}),
|
|
42
|
-
});
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (decision.outcome === "fail" || decision.outcome === "release") {
|
|
46
|
-
const failedAction = decision.actions.find((action) => action.type === "mark_run_failed");
|
|
47
|
-
if (decision.outcome === "release" && failedAction?.type !== "mark_run_failed") {
|
|
48
|
-
const releasedAction = decision.actions.find((action) => action.type === "release_issue_ownership");
|
|
49
|
-
if (releasedAction?.type !== "release_issue_ownership") {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
await this.callbacks.releaseRunDuringReconciliation(snapshot.runLease.projectId, snapshot.runLease.linearIssueId, {
|
|
53
|
-
runId: snapshot.runLease.id,
|
|
54
|
-
...(threadId ? { threadId } : {}),
|
|
55
|
-
...(turnId ? { turnId } : {}),
|
|
56
|
-
...(nextLifecycleStatus ? { nextLifecycleStatus } : {}),
|
|
57
|
-
...(snapshot.input.live?.linear?.status === "known" && snapshot.input.live.linear.issue?.stateName
|
|
58
|
-
? { currentLinearState: snapshot.input.live.linear.issue.stateName }
|
|
59
|
-
: {}),
|
|
60
|
-
});
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
await this.callbacks.failRunDuringReconciliation(snapshot.runLease.projectId, snapshot.runLease.linearIssueId, failedAction?.type === "mark_run_failed" && failedAction.threadId
|
|
64
|
-
? failedAction.threadId
|
|
65
|
-
: threadId ?? `missing-thread-${snapshot.runLease.id}`, decision.reasons[0] ?? "Thread was not found during startup reconciliation", ...(failedAction?.type === "mark_run_failed" && failedAction.turnId ? [{ turnId: failedAction.turnId }] : []));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
export class ReconciliationEngine {
|
|
2
|
-
reconcile(input) {
|
|
3
|
-
const actions = [];
|
|
4
|
-
const issue = input.issue;
|
|
5
|
-
const policy = input.policy ?? {};
|
|
6
|
-
const liveLinear = input.live?.linear ?? { status: "unknown" };
|
|
7
|
-
const liveCodex = input.live?.codex ?? { status: "unknown" };
|
|
8
|
-
const obligations = relevantObligations(issue, input.obligations ?? []);
|
|
9
|
-
if (!issue.activeRun) {
|
|
10
|
-
if (!issue.desiredStage) {
|
|
11
|
-
return {
|
|
12
|
-
outcome: "noop",
|
|
13
|
-
reasons: ["issue has no active run and no desired stage"],
|
|
14
|
-
actions,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
return {
|
|
18
|
-
outcome: "launch",
|
|
19
|
-
reasons: [`desired stage ${issue.desiredStage} is ready to launch`],
|
|
20
|
-
actions: [
|
|
21
|
-
{
|
|
22
|
-
type: "launch_desired_stage",
|
|
23
|
-
projectId: issue.projectId,
|
|
24
|
-
linearIssueId: issue.linearIssueId,
|
|
25
|
-
stage: issue.desiredStage,
|
|
26
|
-
reason: "desired stage exists without an active run",
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
if (needsLinearState(issue, policy, liveLinear)) {
|
|
32
|
-
actions.push({
|
|
33
|
-
type: "read_linear_issue",
|
|
34
|
-
projectId: issue.projectId,
|
|
35
|
-
linearIssueId: issue.linearIssueId,
|
|
36
|
-
reason: "active reconciliation needs the live Linear state",
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
if (needsCodexState(issue.activeRun, liveCodex)) {
|
|
40
|
-
actions.push({
|
|
41
|
-
type: "read_codex_thread",
|
|
42
|
-
projectId: issue.projectId,
|
|
43
|
-
linearIssueId: issue.linearIssueId,
|
|
44
|
-
runId: issue.activeRun.id,
|
|
45
|
-
threadId: issue.activeRun.threadId,
|
|
46
|
-
reason: "active reconciliation needs the live Codex thread",
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
if (actions.length > 0) {
|
|
50
|
-
return {
|
|
51
|
-
outcome: "hydrate_live_state",
|
|
52
|
-
reasons: ["reconciliation needs fresh live state before deciding"],
|
|
53
|
-
actions,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
return reconcileActiveRun({
|
|
57
|
-
issue,
|
|
58
|
-
liveLinear,
|
|
59
|
-
liveCodex,
|
|
60
|
-
obligations,
|
|
61
|
-
policy,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
export function reconcileIssue(input) {
|
|
66
|
-
return new ReconciliationEngine().reconcile(input);
|
|
67
|
-
}
|
|
68
|
-
function reconcileActiveRun(params) {
|
|
69
|
-
const { issue, liveLinear, liveCodex, obligations, policy } = params;
|
|
70
|
-
const run = issue.activeRun;
|
|
71
|
-
const authoritativeStopState = resolveAuthoritativeStopState(liveLinear);
|
|
72
|
-
if (authoritativeStopState) {
|
|
73
|
-
return {
|
|
74
|
-
outcome: "release",
|
|
75
|
-
reasons: [`live Linear state is already ${authoritativeStopState.stateName}`],
|
|
76
|
-
actions: [
|
|
77
|
-
{
|
|
78
|
-
type: "release_issue_ownership",
|
|
79
|
-
projectId: issue.projectId,
|
|
80
|
-
linearIssueId: issue.linearIssueId,
|
|
81
|
-
runId: run.id,
|
|
82
|
-
nextLifecycleStatus: authoritativeStopState.lifecycleStatus,
|
|
83
|
-
reason: `live Linear state is already ${authoritativeStopState.stateName}`,
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
if (run.status === "queued") {
|
|
89
|
-
return {
|
|
90
|
-
outcome: "launch",
|
|
91
|
-
reasons: ["queued run has not been materialized yet"],
|
|
92
|
-
actions: [
|
|
93
|
-
{
|
|
94
|
-
type: "launch_desired_stage",
|
|
95
|
-
projectId: issue.projectId,
|
|
96
|
-
linearIssueId: issue.linearIssueId,
|
|
97
|
-
stage: run.stage,
|
|
98
|
-
runId: run.id,
|
|
99
|
-
reason: "active run is queued and should be launched",
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
if (!run.threadId) {
|
|
105
|
-
return failRun(issue, run, liveLinear, policy, "active run is missing a persisted thread id");
|
|
106
|
-
}
|
|
107
|
-
if (liveCodex.status === "error") {
|
|
108
|
-
return {
|
|
109
|
-
outcome: "continue",
|
|
110
|
-
reasons: [liveCodex.errorMessage ?? "codex thread lookup failed"],
|
|
111
|
-
actions: [
|
|
112
|
-
{
|
|
113
|
-
type: "await_codex_retry",
|
|
114
|
-
projectId: issue.projectId,
|
|
115
|
-
linearIssueId: issue.linearIssueId,
|
|
116
|
-
runId: run.id,
|
|
117
|
-
reason: liveCodex.errorMessage ?? "codex thread lookup failed",
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (liveCodex.status === "missing" || !liveCodex.thread) {
|
|
123
|
-
return failRun(issue, run, liveLinear, policy, "thread was not found during reconciliation");
|
|
124
|
-
}
|
|
125
|
-
const latestTurn = latestThreadTurn(liveCodex.thread);
|
|
126
|
-
const targetTurnId = latestTurn?.id ?? run.turnId;
|
|
127
|
-
if (!latestTurn || latestTurn.status === "inProgress") {
|
|
128
|
-
const actions = routePendingObligations(issue, run, obligations, liveCodex.thread.id, targetTurnId);
|
|
129
|
-
actions.push({
|
|
130
|
-
type: "keep_run_active",
|
|
131
|
-
projectId: issue.projectId,
|
|
132
|
-
linearIssueId: issue.linearIssueId,
|
|
133
|
-
runId: run.id,
|
|
134
|
-
reason: !latestTurn ? "thread has not produced a turn yet" : "latest turn is still in progress",
|
|
135
|
-
});
|
|
136
|
-
return {
|
|
137
|
-
outcome: "continue",
|
|
138
|
-
reasons: [!latestTurn ? "thread has no completed turns yet" : "latest turn is still in progress"],
|
|
139
|
-
actions,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (latestTurn.status !== "completed") {
|
|
143
|
-
return failRun(issue, run, liveLinear, policy, "thread completed reconciliation in a failed state", latestTurn.id);
|
|
144
|
-
}
|
|
145
|
-
const actions = [
|
|
146
|
-
{
|
|
147
|
-
type: "mark_run_completed",
|
|
148
|
-
projectId: issue.projectId,
|
|
149
|
-
linearIssueId: issue.linearIssueId,
|
|
150
|
-
runId: run.id,
|
|
151
|
-
threadId: liveCodex.thread.id,
|
|
152
|
-
...(latestTurn.id ? { turnId: latestTurn.id } : {}),
|
|
153
|
-
reason: "latest turn completed successfully during reconciliation",
|
|
154
|
-
},
|
|
155
|
-
];
|
|
156
|
-
if (shouldAwaitHandoff(liveLinear, policy)) {
|
|
157
|
-
actions.push({
|
|
158
|
-
type: "clear_active_run",
|
|
159
|
-
projectId: issue.projectId,
|
|
160
|
-
linearIssueId: issue.linearIssueId,
|
|
161
|
-
runId: run.id,
|
|
162
|
-
nextLifecycleStatus: "paused",
|
|
163
|
-
reason: "stage completed while the issue still matches the service-owned active Linear state",
|
|
164
|
-
}, {
|
|
165
|
-
type: "refresh_status_comment",
|
|
166
|
-
projectId: issue.projectId,
|
|
167
|
-
linearIssueId: issue.linearIssueId,
|
|
168
|
-
runId: run.id,
|
|
169
|
-
...(issue.statusCommentId ? { commentId: issue.statusCommentId } : {}),
|
|
170
|
-
mode: "awaiting_handoff",
|
|
171
|
-
reason: "stage completed and should publish an awaiting handoff status",
|
|
172
|
-
});
|
|
173
|
-
return {
|
|
174
|
-
outcome: "complete",
|
|
175
|
-
reasons: ["stage completed and should pause for human handoff"],
|
|
176
|
-
actions,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
actions.push({
|
|
180
|
-
type: "release_issue_ownership",
|
|
181
|
-
projectId: issue.projectId,
|
|
182
|
-
linearIssueId: issue.linearIssueId,
|
|
183
|
-
runId: run.id,
|
|
184
|
-
nextLifecycleStatus: "completed",
|
|
185
|
-
reason: "stage completed after the live Linear state moved on",
|
|
186
|
-
});
|
|
187
|
-
return {
|
|
188
|
-
outcome: "release",
|
|
189
|
-
reasons: ["stage completed and the live Linear state already moved on"],
|
|
190
|
-
actions,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
function failRun(issue, run, liveLinear, policy, message, turnId) {
|
|
194
|
-
const actions = [
|
|
195
|
-
{
|
|
196
|
-
type: "mark_run_failed",
|
|
197
|
-
projectId: issue.projectId,
|
|
198
|
-
linearIssueId: issue.linearIssueId,
|
|
199
|
-
runId: run.id,
|
|
200
|
-
...(run.threadId ? { threadId: run.threadId } : {}),
|
|
201
|
-
...(turnId ? { turnId } : run.turnId ? { turnId: run.turnId } : {}),
|
|
202
|
-
reason: message,
|
|
203
|
-
},
|
|
204
|
-
];
|
|
205
|
-
if (shouldFailBack(liveLinear, policy)) {
|
|
206
|
-
actions.push({
|
|
207
|
-
type: "sync_linear_failure",
|
|
208
|
-
projectId: issue.projectId,
|
|
209
|
-
linearIssueId: issue.linearIssueId,
|
|
210
|
-
runId: run.id,
|
|
211
|
-
...(policy.activeLinearStateName ? { expectedStateName: policy.activeLinearStateName } : {}),
|
|
212
|
-
...(policy.fallbackLinearStateName ? { fallbackStateName: policy.fallbackLinearStateName } : {}),
|
|
213
|
-
message,
|
|
214
|
-
}, {
|
|
215
|
-
type: "clear_active_run",
|
|
216
|
-
projectId: issue.projectId,
|
|
217
|
-
linearIssueId: issue.linearIssueId,
|
|
218
|
-
runId: run.id,
|
|
219
|
-
nextLifecycleStatus: "failed",
|
|
220
|
-
reason: "run failed while PatchRelay still owned the expected active Linear state",
|
|
221
|
-
});
|
|
222
|
-
if (issue.statusCommentId) {
|
|
223
|
-
actions.push({
|
|
224
|
-
type: "refresh_status_comment",
|
|
225
|
-
projectId: issue.projectId,
|
|
226
|
-
linearIssueId: issue.linearIssueId,
|
|
227
|
-
runId: run.id,
|
|
228
|
-
commentId: issue.statusCommentId,
|
|
229
|
-
mode: "failed",
|
|
230
|
-
reason: "run failed and should refresh the service-owned status comment",
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
return {
|
|
234
|
-
outcome: "fail",
|
|
235
|
-
reasons: [message],
|
|
236
|
-
actions,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
actions.push({
|
|
240
|
-
type: "release_issue_ownership",
|
|
241
|
-
projectId: issue.projectId,
|
|
242
|
-
linearIssueId: issue.linearIssueId,
|
|
243
|
-
runId: run.id,
|
|
244
|
-
nextLifecycleStatus: "failed",
|
|
245
|
-
reason: "run failed after the live Linear state moved on",
|
|
246
|
-
});
|
|
247
|
-
return {
|
|
248
|
-
outcome: "release",
|
|
249
|
-
reasons: [message, "live Linear state no longer matches the expected service-owned active state"],
|
|
250
|
-
actions,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
function routePendingObligations(issue, run, obligations, threadId, turnId) {
|
|
254
|
-
if (!turnId) {
|
|
255
|
-
return [];
|
|
256
|
-
}
|
|
257
|
-
const actions = [];
|
|
258
|
-
for (const obligation of obligations) {
|
|
259
|
-
const needsRouting = obligation.threadId !== threadId || obligation.turnId !== turnId;
|
|
260
|
-
if (needsRouting) {
|
|
261
|
-
actions.push({
|
|
262
|
-
type: "route_obligation",
|
|
263
|
-
projectId: issue.projectId,
|
|
264
|
-
linearIssueId: issue.linearIssueId,
|
|
265
|
-
obligationId: obligation.id,
|
|
266
|
-
runId: run.id,
|
|
267
|
-
threadId,
|
|
268
|
-
turnId,
|
|
269
|
-
reason: "pending obligation should target the latest live turn",
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
actions.push({
|
|
273
|
-
type: "deliver_obligation",
|
|
274
|
-
projectId: issue.projectId,
|
|
275
|
-
linearIssueId: issue.linearIssueId,
|
|
276
|
-
obligationId: obligation.id,
|
|
277
|
-
runId: run.id,
|
|
278
|
-
threadId,
|
|
279
|
-
turnId,
|
|
280
|
-
reason: "pending obligation can be delivered to the active turn",
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
return actions;
|
|
284
|
-
}
|
|
285
|
-
function needsLinearState(issue, policy, liveLinear) {
|
|
286
|
-
if (!issue.activeRun) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
if (!policy.activeLinearStateName && !policy.fallbackLinearStateName) {
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
return liveLinear.status === "unknown";
|
|
293
|
-
}
|
|
294
|
-
function needsCodexState(run, liveCodex) {
|
|
295
|
-
if (!run.threadId) {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
return liveCodex.status === "unknown";
|
|
299
|
-
}
|
|
300
|
-
function shouldFailBack(liveLinear, policy) {
|
|
301
|
-
return matchesActiveLinearOwnership(liveLinear, policy);
|
|
302
|
-
}
|
|
303
|
-
function shouldAwaitHandoff(liveLinear, policy) {
|
|
304
|
-
return matchesActiveLinearOwnership(liveLinear, policy);
|
|
305
|
-
}
|
|
306
|
-
function matchesActiveLinearOwnership(liveLinear, policy) {
|
|
307
|
-
if (!policy.activeLinearStateName) {
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
if (liveLinear.status !== "known") {
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
return liveLinear.issue?.stateName === policy.activeLinearStateName;
|
|
314
|
-
}
|
|
315
|
-
function resolveAuthoritativeStopState(liveLinear) {
|
|
316
|
-
if (liveLinear.status !== "known" || !liveLinear.issue?.stateName) {
|
|
317
|
-
return undefined;
|
|
318
|
-
}
|
|
319
|
-
const stateName = liveLinear.issue.stateName.trim();
|
|
320
|
-
const normalizedName = stateName.toLowerCase();
|
|
321
|
-
const normalizedType = liveLinear.issue.stateType?.trim().toLowerCase();
|
|
322
|
-
if (normalizedType === "completed" || normalizedName === "done" || normalizedName === "completed" || normalizedName === "complete") {
|
|
323
|
-
return {
|
|
324
|
-
stateName,
|
|
325
|
-
lifecycleStatus: "completed",
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
if (normalizedName === "human needed") {
|
|
329
|
-
return {
|
|
330
|
-
stateName,
|
|
331
|
-
lifecycleStatus: "paused",
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
return undefined;
|
|
335
|
-
}
|
|
336
|
-
function relevantObligations(issue, obligations) {
|
|
337
|
-
const activeRunId = issue.activeRun?.id;
|
|
338
|
-
return obligations.filter((obligation) => {
|
|
339
|
-
if (obligation.status === "completed" || obligation.status === "cancelled") {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
if (activeRunId === undefined) {
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
return obligation.runId === undefined || obligation.runId === activeRunId;
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
function latestThreadTurn(thread) {
|
|
349
|
-
return thread.turns.at(-1);
|
|
350
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { resolveWorkflowStageConfig } from "./workflow-policy.js";
|
|
2
|
-
import { safeJsonParse } from "./utils.js";
|
|
3
|
-
export async function buildReconciliationSnapshot(params) {
|
|
4
|
-
const runLease = params.stores.runLeases.getRunLease(params.runLeaseId);
|
|
5
|
-
if (!runLease) {
|
|
6
|
-
return undefined;
|
|
7
|
-
}
|
|
8
|
-
const issueControl = params.stores.issueControl.getIssueControl(runLease.projectId, runLease.linearIssueId);
|
|
9
|
-
if (!issueControl) {
|
|
10
|
-
return undefined;
|
|
11
|
-
}
|
|
12
|
-
const workspaceOwnership = params.stores.workspaceOwnership.getWorkspaceOwnership(runLease.workspaceOwnershipId);
|
|
13
|
-
const project = params.config.projects.find((candidate) => candidate.id === runLease.projectId);
|
|
14
|
-
const workflowConfig = project ? resolveWorkflowStageConfig(project, runLease.stage, issueControl.selectedWorkflowId) : undefined;
|
|
15
|
-
const liveLinear = project
|
|
16
|
-
? await params.linearProvider
|
|
17
|
-
.forProject(runLease.projectId)
|
|
18
|
-
.then((linear) => linear
|
|
19
|
-
? linear.getIssue(runLease.linearIssueId).then((issue) => issue.stateName
|
|
20
|
-
? {
|
|
21
|
-
status: "known",
|
|
22
|
-
issue: {
|
|
23
|
-
id: issue.id,
|
|
24
|
-
stateName: issue.stateName,
|
|
25
|
-
...(() => {
|
|
26
|
-
const currentState = issue.workflowStates?.find((state) => state.name === issue.stateName);
|
|
27
|
-
return currentState?.type ? { stateType: currentState.type } : {};
|
|
28
|
-
})(),
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
: ({ status: "unknown" }))
|
|
32
|
-
: ({ status: "unknown" }))
|
|
33
|
-
.catch(() => ({ status: "unknown" }))
|
|
34
|
-
: ({ status: "unknown" });
|
|
35
|
-
const liveCodex = runLease.threadId
|
|
36
|
-
? await hydrateLiveCodexState({
|
|
37
|
-
codex: params.codex,
|
|
38
|
-
threadId: runLease.threadId,
|
|
39
|
-
...(workspaceOwnership?.worktreePath ? { cwd: workspaceOwnership.worktreePath } : {}),
|
|
40
|
-
})
|
|
41
|
-
: ({ status: "unknown" });
|
|
42
|
-
const obligations = params.stores.obligations
|
|
43
|
-
.listPendingObligations({ runLeaseId: runLease.id, includeInProgress: true })
|
|
44
|
-
.map((obligation) => {
|
|
45
|
-
const payload = safeJsonParse(obligation.payloadJson);
|
|
46
|
-
return {
|
|
47
|
-
id: obligation.id,
|
|
48
|
-
kind: obligation.kind,
|
|
49
|
-
status: obligation.status,
|
|
50
|
-
...(obligation.runLeaseId !== undefined ? { runId: obligation.runLeaseId } : {}),
|
|
51
|
-
...(obligation.threadId ? { threadId: obligation.threadId } : {}),
|
|
52
|
-
...(obligation.turnId ? { turnId: obligation.turnId } : {}),
|
|
53
|
-
...(payload !== undefined ? { payload } : {}),
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
return {
|
|
57
|
-
issueControl,
|
|
58
|
-
runLease,
|
|
59
|
-
...(workspaceOwnership ? { workspaceOwnership } : {}),
|
|
60
|
-
input: {
|
|
61
|
-
issue: {
|
|
62
|
-
projectId: runLease.projectId,
|
|
63
|
-
linearIssueId: runLease.linearIssueId,
|
|
64
|
-
...(issueControl.desiredStage ? { desiredStage: issueControl.desiredStage } : {}),
|
|
65
|
-
lifecycleStatus: issueControl.lifecycleStatus,
|
|
66
|
-
...(issueControl.serviceOwnedCommentId ? { statusCommentId: issueControl.serviceOwnedCommentId } : {}),
|
|
67
|
-
activeRun: {
|
|
68
|
-
id: runLease.id,
|
|
69
|
-
stage: runLease.stage,
|
|
70
|
-
status: runLease.status,
|
|
71
|
-
...(runLease.threadId ? { threadId: runLease.threadId } : {}),
|
|
72
|
-
...(runLease.turnId ? { turnId: runLease.turnId } : {}),
|
|
73
|
-
...(runLease.parentThreadId ? { parentThreadId: runLease.parentThreadId } : {}),
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
...(obligations.length > 0 ? { obligations } : {}),
|
|
77
|
-
...(workflowConfig
|
|
78
|
-
? {
|
|
79
|
-
policy: {
|
|
80
|
-
...(workflowConfig.activeState ? { activeLinearStateName: workflowConfig.activeState } : {}),
|
|
81
|
-
...(workflowConfig.fallbackState ? { fallbackLinearStateName: workflowConfig.fallbackState } : {}),
|
|
82
|
-
},
|
|
83
|
-
}
|
|
84
|
-
: {}),
|
|
85
|
-
live: {
|
|
86
|
-
linear: liveLinear,
|
|
87
|
-
codex: liveCodex,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
async function hydrateLiveCodexState(params) {
|
|
93
|
-
try {
|
|
94
|
-
const thread = await params.codex.readThread(params.threadId, true);
|
|
95
|
-
if (latestThreadTurn(thread)?.status === "interrupted" && params.cwd) {
|
|
96
|
-
const resumedThread = await tryResumeThread(params.codex, params.threadId, params.cwd);
|
|
97
|
-
if (resumedThread) {
|
|
98
|
-
return { status: "found", thread: resumedThread };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return { status: "found", thread };
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
const mapped = mapCodexReadFailure(error);
|
|
105
|
-
if (mapped.status === "missing" && params.cwd) {
|
|
106
|
-
const resumedThread = await tryResumeThread(params.codex, params.threadId, params.cwd);
|
|
107
|
-
if (resumedThread) {
|
|
108
|
-
return { status: "found", thread: resumedThread };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return mapped;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
async function tryResumeThread(codex, threadId, cwd) {
|
|
115
|
-
try {
|
|
116
|
-
return await codex.resumeThread(threadId, cwd);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function latestThreadTurn(thread) {
|
|
123
|
-
return thread.turns.at(-1);
|
|
124
|
-
}
|
|
125
|
-
function mapCodexReadFailure(error) {
|
|
126
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
-
const normalized = message.trim().toLowerCase();
|
|
128
|
-
if (normalized.includes("not found") || normalized.includes("missing")) {
|
|
129
|
-
return { status: "missing" };
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
status: "error",
|
|
133
|
-
errorMessage: message,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|