patchrelay 0.37.1 → 0.38.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 +47 -9
- package/dist/awaiting-input-reason.js +9 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +59 -3
- package/dist/cli/help.js +1 -1
- package/dist/cli/output.js +2 -0
- package/dist/db/issue-session-store.js +0 -14
- package/dist/db/issue-store.js +8 -16
- package/dist/db/migrations.js +6 -13
- package/dist/db.js +1 -3
- package/dist/github-webhook-handler.js +70 -51
- package/dist/github-webhooks.js +4 -0
- package/dist/idle-reconciliation.js +22 -23
- package/dist/issue-overview-query.js +3 -0
- package/dist/issue-session-projector.js +1 -0
- package/dist/issue-session.js +8 -0
- package/dist/linear-session-reporting.js +30 -1
- package/dist/linear-session-sync.js +9 -1
- package/dist/linear-status-comment-sync.js +34 -1
- package/dist/linear-workflow-state-sync.js +2 -2
- package/dist/operator-retry-event.js +15 -12
- package/dist/paused-issue-state.js +24 -0
- package/dist/run-launcher.js +0 -1
- package/dist/run-orchestrator.js +2 -5
- package/dist/run-reconciler.js +10 -0
- package/dist/run-recovery-service.js +1 -10
- package/dist/service-issue-actions.js +5 -0
- package/dist/service-startup-recovery.js +9 -6
- package/dist/service.js +0 -1
- package/dist/tracked-issue-list-query.js +3 -1
- package/dist/tracked-issue-projector.js +3 -0
- package/dist/waiting-reason.js +10 -0
- package/dist/webhooks/agent-session-handler.js +9 -1
- package/dist/webhooks/comment-wake-handler.js +12 -0
- package/dist/webhooks/decision-helpers.js +44 -3
- package/dist/webhooks/dependency-readiness-handler.js +1 -0
- package/dist/webhooks/desired-stage-recorder.js +40 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# PatchRelay
|
|
2
2
|
|
|
3
|
-
PatchRelay is a self-hosted harness for delegated Linear work and upkeep of
|
|
3
|
+
PatchRelay is a self-hosted harness for delegated Linear work and upkeep of linked pull requests on your own machine.
|
|
4
4
|
|
|
5
|
-
It receives Linear webhooks, routes issues to the right local repository, prepares durable issue worktrees, runs Codex sessions through `codex app-server`, and keeps the issue loop observable and resumable from the CLI. GitHub webhooks drive reactive loops for CI repair, review fixes, and merge-steward incidents on
|
|
5
|
+
It receives Linear webhooks, routes issues to the right local repository, prepares durable issue worktrees, runs Codex sessions through `codex app-server`, and keeps the issue loop observable and resumable from the CLI. GitHub webhooks drive reactive loops for CI repair, review fixes, and merge-steward incidents on linked delegated PRs. Separate downstream services own review automation and merge execution.
|
|
6
6
|
|
|
7
7
|
PatchRelay is the system around the model:
|
|
8
8
|
|
|
@@ -38,8 +38,9 @@ PatchRelay does the deterministic harness work that you do not want to re-implem
|
|
|
38
38
|
- creates and reuses one durable worktree and branch per issue lifecycle
|
|
39
39
|
- starts Codex threads for implementation runs
|
|
40
40
|
- triggers reactive runs for CI failures, review feedback, and Merge Steward evictions
|
|
41
|
-
- opens and updates
|
|
41
|
+
- opens and updates PRs for delegated implementation work
|
|
42
42
|
- marks its own PRs ready when implementation is complete
|
|
43
|
+
- can later repair a linked PR that was opened externally once the issue is delegated
|
|
43
44
|
- persists enough state to correlate the Linear issue, local workspace, run, and Codex thread
|
|
44
45
|
- reports progress back to Linear and forwards follow-up agent input into active runs
|
|
45
46
|
- exposes CLI and optional read-only inspection surfaces so operators can understand what happened
|
|
@@ -87,19 +88,48 @@ You will also need:
|
|
|
87
88
|
5. GitHub webhooks drive reactive verification and repair loops: CI repair on check failures and review fix on changes requested.
|
|
88
89
|
6. PatchRelay opens draft PRs while implementation is in progress and marks its own PR ready when implementation is complete.
|
|
89
90
|
7. Downstream automation reacts to GitHub truth: `reviewbot` reviews ready PRs with green CI, and Merge Steward admits ready PRs with green CI and approval into the merge queue.
|
|
90
|
-
8. If requested changes, red CI, or a merge-steward incident lands on a
|
|
91
|
+
8. If requested changes, red CI, or a merge-steward incident lands on a linked delegated PR, PatchRelay resumes work on that same PR branch.
|
|
91
92
|
9. Native agent prompts and Linear comments can steer the active run. An operator can take over from the exact same worktree when needed.
|
|
92
93
|
|
|
94
|
+
### Undelegation And Re-delegation
|
|
95
|
+
|
|
96
|
+
Undelegation pauses PatchRelay authority. It does not erase PR truth.
|
|
97
|
+
|
|
98
|
+
- If there is no PR yet, the issue keeps its literal local-work state such as `delegated` or `implementing`, but PatchRelay becomes paused.
|
|
99
|
+
- If a PR already exists, the issue keeps its PR-backed state and PatchRelay becomes observer-only.
|
|
100
|
+
- Worktrees, branches, and PRs remain in place.
|
|
101
|
+
- PatchRelay still reflects GitHub review, CI, queue, merge, and close events while undelegated.
|
|
102
|
+
- PatchRelay does not enqueue implementation, review-fix, CI-repair, or queue-repair work again until the issue is delegated back.
|
|
103
|
+
- If someone opens a new PR for the issue while it is undelegated, PatchRelay can link that PR when the title, body, or branch name contains one unambiguous tracked issue key for the project.
|
|
104
|
+
|
|
105
|
+
Downstream services stay PR-centric:
|
|
106
|
+
|
|
107
|
+
- `review-quill` may still review a qualifying PR
|
|
108
|
+
- `merge-steward` may still queue or merge a qualifying PR
|
|
109
|
+
|
|
110
|
+
When the issue is delegated back to PatchRelay, it should resume from current truth:
|
|
111
|
+
|
|
112
|
+
- no PR: queue implementation
|
|
113
|
+
- PR with requested changes: queue review fix or branch upkeep
|
|
114
|
+
- PR with failing CI: queue CI repair
|
|
115
|
+
- PR with queue eviction/conflict: queue queue repair
|
|
116
|
+
- healthy open PR: keep waiting on review
|
|
117
|
+
- approved PR: keep waiting downstream
|
|
118
|
+
|
|
93
119
|
## Ownership Model
|
|
94
120
|
|
|
95
|
-
PatchRelay
|
|
121
|
+
PatchRelay keeps ownership simple:
|
|
122
|
+
|
|
123
|
+
- workflow truth: the current factory state plus GitHub PR/review/CI facts
|
|
124
|
+
- runtime authority: whether PatchRelay may actively write or repair code right now
|
|
125
|
+
|
|
126
|
+
PatchRelay persists one explicit authority bit:
|
|
96
127
|
|
|
97
|
-
-
|
|
98
|
-
- PR ownership: who is responsible for keeping an existing PR healthy until it merges or closes
|
|
128
|
+
- `delegatedToPatchRelay`: whether PatchRelay may actively implement or repair code for the issue right now
|
|
99
129
|
|
|
100
|
-
|
|
130
|
+
Once a PR is linked to an issue, delegation decides whether PatchRelay may repair it. The PR may have been opened by PatchRelay, a human, or another external system.
|
|
101
131
|
|
|
102
|
-
That
|
|
132
|
+
That authority does not change just because:
|
|
103
133
|
|
|
104
134
|
- the issue is undelegated
|
|
105
135
|
- the PR becomes ready for review
|
|
@@ -138,6 +168,14 @@ The long-term runtime model is a small durable `IssueSession`:
|
|
|
138
168
|
|
|
139
169
|
Waiting on review or queue should be represented as a waiting reason, not as a large internal control-plane state machine.
|
|
140
170
|
|
|
171
|
+
`awaiting_input` is reserved for real human-needed situations:
|
|
172
|
+
|
|
173
|
+
- a completion check asked a question
|
|
174
|
+
- an operator explicitly stopped the run and wants a next decision
|
|
175
|
+
- a reply is required before PatchRelay can continue
|
|
176
|
+
|
|
177
|
+
Undelegated local work should stay in its literal workflow state and show a paused waiting reason instead.
|
|
178
|
+
|
|
141
179
|
## Restart And Reconciliation
|
|
142
180
|
|
|
143
181
|
PatchRelay treats restart safety as part of the harness contract, not as a best-effort extra.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function resolveAwaitingInputReason(params) {
|
|
2
|
+
if (params.issue.factoryState !== "awaiting_input") {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
if (params.latestRun?.completionCheckOutcome === "needs_input") {
|
|
6
|
+
return "completion_check_question";
|
|
7
|
+
}
|
|
8
|
+
return "paused_local_work";
|
|
9
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -302,6 +302,7 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
302
302
|
}
|
|
303
303
|
const ciEntry = buildCiEntry({
|
|
304
304
|
issue,
|
|
305
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
305
306
|
gateCheckStatus,
|
|
306
307
|
reviewDecision,
|
|
307
308
|
reviewRequested,
|
|
@@ -310,7 +311,31 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
310
311
|
mergeConflictDetected,
|
|
311
312
|
reviewQuillAttempt,
|
|
312
313
|
});
|
|
313
|
-
if (
|
|
314
|
+
if (pr.state === "MERGED" && issue.factoryState !== "done" && ageMs >= RECONCILIATION_GRACE_MS) {
|
|
315
|
+
return {
|
|
316
|
+
ciEntry,
|
|
317
|
+
finding: {
|
|
318
|
+
status: "fail",
|
|
319
|
+
scope: "github:reconcile",
|
|
320
|
+
message: "PR is already merged but the issue has not advanced to done",
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (pr.state === "CLOSED" && issue.factoryState !== "delegated" && issue.factoryState !== "done" && ageMs >= RECONCILIATION_GRACE_MS) {
|
|
325
|
+
return {
|
|
326
|
+
ciEntry,
|
|
327
|
+
finding: {
|
|
328
|
+
status: "fail",
|
|
329
|
+
scope: "github:reconcile",
|
|
330
|
+
message: "PR is closed but the issue is still waiting on PR state",
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (issue.delegatedToPatchRelay
|
|
335
|
+
&& gateCheckStatus === "failure"
|
|
336
|
+
&& issue.factoryState !== "repairing_ci"
|
|
337
|
+
&& issue.activeRunId === undefined
|
|
338
|
+
&& ageMs >= RECONCILIATION_GRACE_MS) {
|
|
314
339
|
return {
|
|
315
340
|
ciEntry,
|
|
316
341
|
finding: {
|
|
@@ -333,6 +358,7 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
333
358
|
if (gateCheckStatus === "success"
|
|
334
359
|
&& reviewDecision === "CHANGES_REQUESTED"
|
|
335
360
|
&& mergeConflictDetected
|
|
361
|
+
&& issue.delegatedToPatchRelay
|
|
336
362
|
&& issue.factoryState !== "changes_requested"
|
|
337
363
|
&& issue.activeRunId === undefined
|
|
338
364
|
&& ageMs >= RECONCILIATION_GRACE_MS) {
|
|
@@ -349,6 +375,7 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
349
375
|
&& reviewDecision === "CHANGES_REQUESTED"
|
|
350
376
|
&& latestBlockingReviewHeadSha === pr.headRefOid
|
|
351
377
|
&& !reviewQuillAttempt
|
|
378
|
+
&& issue.delegatedToPatchRelay
|
|
352
379
|
&& issue.factoryState !== "changes_requested"
|
|
353
380
|
&& ageMs >= RECONCILIATION_GRACE_MS) {
|
|
354
381
|
return {
|
|
@@ -370,7 +397,11 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
370
397
|
},
|
|
371
398
|
};
|
|
372
399
|
}
|
|
373
|
-
if (issue.
|
|
400
|
+
if (issue.delegatedToPatchRelay
|
|
401
|
+
&& issue.factoryState === "awaiting_queue"
|
|
402
|
+
&& mergeConflictDetected
|
|
403
|
+
&& issue.activeRunId === undefined
|
|
404
|
+
&& ageMs >= RECONCILIATION_GRACE_MS) {
|
|
374
405
|
return {
|
|
375
406
|
ciEntry,
|
|
376
407
|
finding: {
|
|
@@ -393,8 +424,9 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
393
424
|
return { ciEntry };
|
|
394
425
|
}
|
|
395
426
|
function buildCiEntry(params) {
|
|
396
|
-
const { issue, gateCheckStatus, reviewDecision, reviewRequested, currentHeadSha, latestBlockingReviewHeadSha, mergeConflictDetected, reviewQuillAttempt, } = params;
|
|
427
|
+
const { issue, delegatedToPatchRelay, gateCheckStatus, reviewDecision, reviewRequested, currentHeadSha, latestBlockingReviewHeadSha, mergeConflictDetected, reviewQuillAttempt, } = params;
|
|
397
428
|
const owner = deriveCiOwner({
|
|
429
|
+
delegatedToPatchRelay,
|
|
398
430
|
gateCheckStatus,
|
|
399
431
|
factoryState: issue.factoryState,
|
|
400
432
|
reviewDecision,
|
|
@@ -414,6 +446,7 @@ function buildCiEntry(params) {
|
|
|
414
446
|
factoryState: issue.factoryState,
|
|
415
447
|
...(reviewDecision ? { reviewDecision } : {}),
|
|
416
448
|
message: describeCiOwnership({
|
|
449
|
+
delegatedToPatchRelay,
|
|
417
450
|
gateCheckStatus,
|
|
418
451
|
owner,
|
|
419
452
|
reviewDecision,
|
|
@@ -430,20 +463,29 @@ function deriveCiOwner(params) {
|
|
|
430
463
|
&& params.latestBlockingReviewHeadSha
|
|
431
464
|
&& params.currentHeadSha !== params.latestBlockingReviewHeadSha);
|
|
432
465
|
if (params.gateCheckStatus === "failure") {
|
|
466
|
+
if (!params.delegatedToPatchRelay)
|
|
467
|
+
return "paused";
|
|
433
468
|
return params.factoryState === "repairing_ci" ? "patchrelay" : "unknown";
|
|
434
469
|
}
|
|
435
470
|
if (params.gateCheckStatus === "pending") {
|
|
436
471
|
return "external";
|
|
437
472
|
}
|
|
438
473
|
if (params.factoryState === "awaiting_queue" || params.reviewDecision === "APPROVED") {
|
|
474
|
+
if (params.mergeConflictDetected && !params.delegatedToPatchRelay) {
|
|
475
|
+
return "paused";
|
|
476
|
+
}
|
|
439
477
|
return params.mergeConflictDetected && params.factoryState !== "repairing_queue"
|
|
440
478
|
? "unknown"
|
|
441
479
|
: "downstream";
|
|
442
480
|
}
|
|
443
481
|
if (params.reviewDecision === "CHANGES_REQUESTED") {
|
|
444
482
|
if (params.mergeConflictDetected) {
|
|
483
|
+
if (!params.delegatedToPatchRelay)
|
|
484
|
+
return "paused";
|
|
445
485
|
return params.factoryState === "changes_requested" ? "patchrelay" : "unknown";
|
|
446
486
|
}
|
|
487
|
+
if (!params.delegatedToPatchRelay)
|
|
488
|
+
return "paused";
|
|
447
489
|
if (params.factoryState === "changes_requested")
|
|
448
490
|
return "patchrelay";
|
|
449
491
|
if (params.reviewQuillAttempt)
|
|
@@ -502,6 +544,20 @@ function describeCiOwnership(params) {
|
|
|
502
544
|
? "Waiting on external CI checks to settle"
|
|
503
545
|
: "Waiting on external GitHub automation";
|
|
504
546
|
}
|
|
547
|
+
if (params.owner === "paused") {
|
|
548
|
+
if (params.gateCheckStatus === "failure") {
|
|
549
|
+
return "PatchRelay is paused; delegate the issue again to repair failing CI";
|
|
550
|
+
}
|
|
551
|
+
if (params.reviewDecision === "CHANGES_REQUESTED") {
|
|
552
|
+
return params.mergeConflictDetected
|
|
553
|
+
? "PatchRelay is paused; delegate the issue again to repair the blocked PR branch"
|
|
554
|
+
: "PatchRelay is paused; delegate the issue again to address requested changes";
|
|
555
|
+
}
|
|
556
|
+
if (params.mergeConflictDetected) {
|
|
557
|
+
return "PatchRelay is paused; delegate the issue again to repair this merge conflict";
|
|
558
|
+
}
|
|
559
|
+
return "PatchRelay is paused; no automatic repair will start until the issue is delegated again";
|
|
560
|
+
}
|
|
505
561
|
if (params.reviewDecision === "CHANGES_REQUESTED") {
|
|
506
562
|
if (params.mergeConflictDetected) {
|
|
507
563
|
return headAdvancedPastBlockingReview
|
package/dist/cli/help.js
CHANGED
|
@@ -144,7 +144,7 @@ export function issueHelpText() {
|
|
|
144
144
|
"Commands:",
|
|
145
145
|
" show <issueKey> Show the latest known issue state",
|
|
146
146
|
" list List tracked issues",
|
|
147
|
-
" watch <issueKey> Follow
|
|
147
|
+
" watch <issueKey> Follow issue activity until it settles",
|
|
148
148
|
" path <issueKey> Print the issue worktree path",
|
|
149
149
|
" open <issueKey> Open Codex in the issue worktree",
|
|
150
150
|
" sessions <issueKey> Show recorded Codex app-server sessions",
|
package/dist/cli/output.js
CHANGED
|
@@ -263,20 +263,6 @@ export class IssueSessionStore {
|
|
|
263
263
|
WHERE project_id = ? AND linear_issue_id = ?
|
|
264
264
|
`).run(lastWakeReason ?? null, isoNow(), projectId, linearIssueId);
|
|
265
265
|
}
|
|
266
|
-
setBranchOwnerWithLease(lease, owner) {
|
|
267
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
268
|
-
this.issues.setBranchOwner(lease.projectId, lease.linearIssueId, owner);
|
|
269
|
-
return true;
|
|
270
|
-
}) ?? false;
|
|
271
|
-
}
|
|
272
|
-
setBranchOwnerRespectingActiveLease(projectId, linearIssueId, owner) {
|
|
273
|
-
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
274
|
-
if (!lease) {
|
|
275
|
-
this.issues.setBranchOwner(projectId, linearIssueId, owner);
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
return this.setBranchOwnerWithLease(lease, owner);
|
|
279
|
-
}
|
|
280
266
|
releaseIssueSessionLeaseRespectingActiveLease(projectId, linearIssueId) {
|
|
281
267
|
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
282
268
|
this.releaseIssueSessionLease(projectId, linearIssueId, lease?.leaseId);
|
package/dist/db/issue-store.js
CHANGED
|
@@ -16,6 +16,10 @@ export class IssueStore {
|
|
|
16
16
|
projectId: params.projectId,
|
|
17
17
|
linearIssueId: params.linearIssueId,
|
|
18
18
|
};
|
|
19
|
+
if (params.delegatedToPatchRelay !== undefined) {
|
|
20
|
+
sets.push("delegated_to_patchrelay = @delegatedToPatchRelay");
|
|
21
|
+
values.delegatedToPatchRelay = params.delegatedToPatchRelay ? 1 : 0;
|
|
22
|
+
}
|
|
19
23
|
if (params.issueKey !== undefined) {
|
|
20
24
|
sets.push("issue_key = COALESCE(@issueKey, issue_key)");
|
|
21
25
|
values.issueKey = params.issueKey;
|
|
@@ -205,7 +209,7 @@ export class IssueStore {
|
|
|
205
209
|
else {
|
|
206
210
|
this.connection.prepare(`
|
|
207
211
|
INSERT INTO issues (
|
|
208
|
-
project_id, linear_issue_id, issue_key, title, description, url,
|
|
212
|
+
project_id, linear_issue_id, delegated_to_patchrelay, issue_key, title, description, url,
|
|
209
213
|
priority, estimate,
|
|
210
214
|
current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
|
|
211
215
|
branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
|
|
@@ -218,7 +222,7 @@ export class IssueStore {
|
|
|
218
222
|
ci_repair_attempts, queue_repair_attempts, review_fix_attempts, zombie_recovery_attempts, last_zombie_recovery_at,
|
|
219
223
|
updated_at
|
|
220
224
|
) VALUES (
|
|
221
|
-
@projectId, @linearIssueId, @issueKey, @title, @description, @url,
|
|
225
|
+
@projectId, @linearIssueId, @delegatedToPatchRelay, @issueKey, @title, @description, @url,
|
|
222
226
|
@priority, @estimate,
|
|
223
227
|
@currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
224
228
|
@branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
|
|
@@ -234,6 +238,7 @@ export class IssueStore {
|
|
|
234
238
|
`).run({
|
|
235
239
|
projectId: params.projectId,
|
|
236
240
|
linearIssueId: params.linearIssueId,
|
|
241
|
+
delegatedToPatchRelay: params.delegatedToPatchRelay === false ? 0 : 1,
|
|
237
242
|
issueKey: params.issueKey ?? null,
|
|
238
243
|
title: params.title ?? null,
|
|
239
244
|
description: params.description ?? null,
|
|
@@ -357,14 +362,6 @@ export class IssueStore {
|
|
|
357
362
|
.all();
|
|
358
363
|
return rows.map(mapIssueRow);
|
|
359
364
|
}
|
|
360
|
-
setBranchOwner(projectId, linearIssueId, owner) {
|
|
361
|
-
const now = isoNow();
|
|
362
|
-
this.connection.prepare(`
|
|
363
|
-
UPDATE issues
|
|
364
|
-
SET branch_owner = ?, branch_ownership_changed_at = ?, updated_at = ?
|
|
365
|
-
WHERE project_id = ? AND linear_issue_id = ?
|
|
366
|
-
`).run(owner, now, now, projectId, linearIssueId);
|
|
367
|
-
}
|
|
368
365
|
replaceIssueDependencies(params) {
|
|
369
366
|
const now = isoNow();
|
|
370
367
|
this.connection
|
|
@@ -466,6 +463,7 @@ export function mapIssueRow(row) {
|
|
|
466
463
|
id: Number(row.id),
|
|
467
464
|
projectId: String(row.project_id),
|
|
468
465
|
linearIssueId: String(row.linear_issue_id),
|
|
466
|
+
delegatedToPatchRelay: Number(row.delegated_to_patchrelay ?? 1) !== 0,
|
|
469
467
|
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
470
468
|
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
471
469
|
...(row.description !== null && row.description !== undefined ? { description: String(row.description) } : {}),
|
|
@@ -480,12 +478,6 @@ export function mapIssueRow(row) {
|
|
|
480
478
|
...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
481
479
|
...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
|
|
482
480
|
...(row.branch_name !== null ? { branchName: String(row.branch_name) } : {}),
|
|
483
|
-
...(row.branch_owner !== null && row.branch_owner !== undefined && String(row.branch_owner) === "patchrelay"
|
|
484
|
-
? { branchOwner: "patchrelay" }
|
|
485
|
-
: { branchOwner: "patchrelay" }),
|
|
486
|
-
...(row.branch_ownership_changed_at !== null && row.branch_ownership_changed_at !== undefined
|
|
487
|
-
? { branchOwnershipChangedAt: String(row.branch_ownership_changed_at) }
|
|
488
|
-
: {}),
|
|
489
481
|
...(row.worktree_path !== null ? { worktreePath: String(row.worktree_path) } : {}),
|
|
490
482
|
...(row.thread_id !== null ? { threadId: String(row.thread_id) } : {}),
|
|
491
483
|
...(row.active_run_id !== null ? { activeRunId: Number(row.active_run_id) } : {}),
|
package/dist/db/migrations.js
CHANGED
|
@@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
3
3
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
4
|
project_id TEXT NOT NULL,
|
|
5
5
|
linear_issue_id TEXT NOT NULL,
|
|
6
|
+
delegated_to_patchrelay INTEGER NOT NULL DEFAULT 1,
|
|
6
7
|
issue_key TEXT,
|
|
7
8
|
title TEXT,
|
|
8
9
|
url TEXT,
|
|
@@ -12,8 +13,6 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
12
13
|
pending_run_type TEXT,
|
|
13
14
|
pending_run_context_json TEXT,
|
|
14
15
|
branch_name TEXT,
|
|
15
|
-
branch_owner TEXT NOT NULL DEFAULT 'patchrelay',
|
|
16
|
-
branch_ownership_changed_at TEXT,
|
|
17
16
|
worktree_path TEXT,
|
|
18
17
|
thread_id TEXT,
|
|
19
18
|
active_run_id INTEGER,
|
|
@@ -239,12 +238,9 @@ export function runPatchRelayMigrations(connection) {
|
|
|
239
238
|
connection.exec(schema);
|
|
240
239
|
// Clean up stale dedupe-only webhook records (no payload, never processable)
|
|
241
240
|
connection.prepare("UPDATE webhook_events SET processing_status = 'processed' WHERE processing_status = 'pending' AND payload_json IS NULL").run();
|
|
241
|
+
addColumnIfMissing(connection, "issues", "delegated_to_patchrelay", "INTEGER NOT NULL DEFAULT 1");
|
|
242
242
|
// Add pending_merge_prep column for merge queue stewardship
|
|
243
243
|
addColumnIfMissing(connection, "issues", "pending_merge_prep", "INTEGER NOT NULL DEFAULT 0");
|
|
244
|
-
// Explicit PR branch ownership hand-off between PatchRelay and MergeSteward
|
|
245
|
-
addColumnIfMissing(connection, "issues", "branch_owner", "TEXT NOT NULL DEFAULT 'patchrelay'");
|
|
246
|
-
addColumnIfMissing(connection, "issues", "branch_ownership_changed_at", "TEXT");
|
|
247
|
-
connection.prepare("UPDATE issues SET branch_owner = 'patchrelay' WHERE branch_owner IS NULL OR branch_owner != 'patchrelay'").run();
|
|
248
244
|
// Add merge_prep_attempts for retry budget / escalation
|
|
249
245
|
addColumnIfMissing(connection, "issues", "merge_prep_attempts", "INTEGER NOT NULL DEFAULT 0");
|
|
250
246
|
// Add review_fix_attempts counter
|
|
@@ -304,7 +300,7 @@ function addColumnIfMissing(connection, table, column, definition) {
|
|
|
304
300
|
function removeRetiredIssueColumnsIfPresent(connection) {
|
|
305
301
|
const cols = connection.prepare("PRAGMA table_info(issues)").all();
|
|
306
302
|
const columnNames = new Set(cols.map((column) => String(column.name)));
|
|
307
|
-
const retired = ["queue_label_applied", "pending_merge_prep", "merge_prep_attempts"];
|
|
303
|
+
const retired = ["queue_label_applied", "pending_merge_prep", "merge_prep_attempts", "branch_owner", "branch_ownership_changed_at"];
|
|
308
304
|
if (!retired.some((name) => columnNames.has(name))) {
|
|
309
305
|
return;
|
|
310
306
|
}
|
|
@@ -315,6 +311,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
315
311
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
316
312
|
project_id TEXT NOT NULL,
|
|
317
313
|
linear_issue_id TEXT NOT NULL,
|
|
314
|
+
delegated_to_patchrelay INTEGER NOT NULL DEFAULT 1,
|
|
318
315
|
issue_key TEXT,
|
|
319
316
|
title TEXT,
|
|
320
317
|
description TEXT,
|
|
@@ -327,8 +324,6 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
327
324
|
pending_run_type TEXT,
|
|
328
325
|
pending_run_context_json TEXT,
|
|
329
326
|
branch_name TEXT,
|
|
330
|
-
branch_owner TEXT NOT NULL DEFAULT 'patchrelay',
|
|
331
|
-
branch_ownership_changed_at TEXT,
|
|
332
327
|
worktree_path TEXT,
|
|
333
328
|
thread_id TEXT,
|
|
334
329
|
active_run_id INTEGER,
|
|
@@ -371,6 +366,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
371
366
|
id,
|
|
372
367
|
project_id,
|
|
373
368
|
linear_issue_id,
|
|
369
|
+
delegated_to_patchrelay,
|
|
374
370
|
issue_key,
|
|
375
371
|
title,
|
|
376
372
|
description,
|
|
@@ -383,8 +379,6 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
383
379
|
pending_run_type,
|
|
384
380
|
pending_run_context_json,
|
|
385
381
|
branch_name,
|
|
386
|
-
branch_owner,
|
|
387
|
-
branch_ownership_changed_at,
|
|
388
382
|
worktree_path,
|
|
389
383
|
thread_id,
|
|
390
384
|
active_run_id,
|
|
@@ -425,6 +419,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
425
419
|
id,
|
|
426
420
|
project_id,
|
|
427
421
|
linear_issue_id,
|
|
422
|
+
COALESCE(delegated_to_patchrelay, 1),
|
|
428
423
|
issue_key,
|
|
429
424
|
title,
|
|
430
425
|
description,
|
|
@@ -437,8 +432,6 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
437
432
|
pending_run_type,
|
|
438
433
|
pending_run_context_json,
|
|
439
434
|
branch_name,
|
|
440
|
-
COALESCE(branch_owner, 'patchrelay'),
|
|
441
|
-
branch_ownership_changed_at,
|
|
442
435
|
worktree_path,
|
|
443
436
|
thread_id,
|
|
444
437
|
active_run_id,
|
package/dist/db.js
CHANGED
|
@@ -34,6 +34,7 @@ function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
|
|
|
34
34
|
}
|
|
35
35
|
function deriveImplicitReactiveWake(issue) {
|
|
36
36
|
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
37
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
37
38
|
activeRunId: issue.activeRunId,
|
|
38
39
|
prNumber: issue.prNumber,
|
|
39
40
|
prState: issue.prState,
|
|
@@ -144,9 +145,6 @@ export class PatchRelayDatabase {
|
|
|
144
145
|
getIssueByPrNumber(prNumber) {
|
|
145
146
|
return this.issues.getIssueByPrNumber(prNumber);
|
|
146
147
|
}
|
|
147
|
-
setBranchOwner(projectId, linearIssueId, owner) {
|
|
148
|
-
this.issues.setBranchOwner(projectId, linearIssueId, owner);
|
|
149
|
-
}
|
|
150
148
|
replaceIssueDependencies(params) {
|
|
151
149
|
this.issues.replaceIssueDependencies(params);
|
|
152
150
|
}
|