patchrelay 0.47.2 → 0.49.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 +71 -362
- package/dist/agent-session-plan.js +42 -0
- package/dist/build-info.json +3 -3
- package/dist/db/issue-store.js +101 -4
- package/dist/db/migrations.js +44 -0
- package/dist/db.js +12 -0
- package/dist/github-webhook-terminal-handler.js +7 -0
- package/dist/idle-reconciliation.js +19 -0
- package/dist/issue-class.js +35 -0
- package/dist/issue-overview-query.js +2 -0
- package/dist/issue-session-events.js +12 -2
- package/dist/issue-session.js +6 -0
- package/dist/linear-client.js +13 -0
- package/dist/linear-status-comment-sync.js +1 -0
- package/dist/no-pr-completion-check.js +18 -3
- package/dist/orchestration-parent-wake.js +96 -0
- package/dist/prompting/patchrelay.js +99 -22
- package/dist/run-orchestrator.js +22 -9
- package/dist/tracked-issue-list-query.js +3 -0
- package/dist/tracked-issue-projector.js +3 -0
- package/dist/waiting-reason.js +7 -0
- package/dist/webhooks/comment-wake-handler.js +6 -1
- package/dist/webhooks/decision-helpers.js +3 -0
- package/dist/webhooks/desired-stage-recorder.js +86 -1
- package/dist/webhooks.js +7 -0
- package/package.json +1 -1
package/dist/db/issue-store.js
CHANGED
|
@@ -20,6 +20,22 @@ export class IssueStore {
|
|
|
20
20
|
sets.push("delegated_to_patchrelay = @delegatedToPatchRelay");
|
|
21
21
|
values.delegatedToPatchRelay = params.delegatedToPatchRelay ? 1 : 0;
|
|
22
22
|
}
|
|
23
|
+
if (params.issueClass !== undefined) {
|
|
24
|
+
sets.push("issue_class = @issueClass");
|
|
25
|
+
values.issueClass = params.issueClass;
|
|
26
|
+
}
|
|
27
|
+
if (params.issueClassSource !== undefined) {
|
|
28
|
+
sets.push("issue_class_source = @issueClassSource");
|
|
29
|
+
values.issueClassSource = params.issueClassSource;
|
|
30
|
+
}
|
|
31
|
+
if (params.parentLinearIssueId !== undefined) {
|
|
32
|
+
sets.push("parent_linear_issue_id = @parentLinearIssueId");
|
|
33
|
+
values.parentLinearIssueId = params.parentLinearIssueId;
|
|
34
|
+
}
|
|
35
|
+
if (params.parentIssueKey !== undefined) {
|
|
36
|
+
sets.push("parent_issue_key = @parentIssueKey");
|
|
37
|
+
values.parentIssueKey = params.parentIssueKey;
|
|
38
|
+
}
|
|
23
39
|
if (params.issueKey !== undefined) {
|
|
24
40
|
sets.push("issue_key = COALESCE(@issueKey, issue_key)");
|
|
25
41
|
values.issueKey = params.issueKey;
|
|
@@ -216,12 +232,16 @@ export class IssueStore {
|
|
|
216
232
|
sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
|
|
217
233
|
values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
|
|
218
234
|
}
|
|
235
|
+
if (params.orchestrationSettleUntil !== undefined) {
|
|
236
|
+
sets.push("orchestration_settle_until = @orchestrationSettleUntil");
|
|
237
|
+
values.orchestrationSettleUntil = params.orchestrationSettleUntil;
|
|
238
|
+
}
|
|
219
239
|
this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
|
|
220
240
|
}
|
|
221
241
|
else {
|
|
222
242
|
this.connection.prepare(`
|
|
223
243
|
INSERT INTO issues (
|
|
224
|
-
project_id, linear_issue_id, delegated_to_patchrelay, issue_key, title, description, url,
|
|
244
|
+
project_id, linear_issue_id, delegated_to_patchrelay, issue_class, issue_class_source, parent_linear_issue_id, parent_issue_key, issue_key, title, description, url,
|
|
225
245
|
priority, estimate,
|
|
226
246
|
current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
|
|
227
247
|
branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
|
|
@@ -231,10 +251,10 @@ export class IssueStore {
|
|
|
231
251
|
last_github_ci_snapshot_head_sha, last_github_ci_snapshot_gate_check_name, last_github_ci_snapshot_gate_check_status, last_github_ci_snapshot_json, last_github_ci_snapshot_settled_at,
|
|
232
252
|
last_queue_signal_at, last_queue_incident_json,
|
|
233
253
|
last_attempted_failure_head_sha, last_attempted_failure_signature, last_attempted_failure_at,
|
|
234
|
-
ci_repair_attempts, queue_repair_attempts, review_fix_attempts, zombie_recovery_attempts, last_zombie_recovery_at,
|
|
254
|
+
ci_repair_attempts, queue_repair_attempts, review_fix_attempts, zombie_recovery_attempts, last_zombie_recovery_at, orchestration_settle_until,
|
|
235
255
|
updated_at
|
|
236
256
|
) VALUES (
|
|
237
|
-
@projectId, @linearIssueId, @delegatedToPatchRelay, @issueKey, @title, @description, @url,
|
|
257
|
+
@projectId, @linearIssueId, @delegatedToPatchRelay, @issueClass, @issueClassSource, @parentLinearIssueId, @parentIssueKey, @issueKey, @title, @description, @url,
|
|
238
258
|
@priority, @estimate,
|
|
239
259
|
@currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
240
260
|
@branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
|
|
@@ -244,13 +264,17 @@ export class IssueStore {
|
|
|
244
264
|
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
245
265
|
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
246
266
|
@lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature, @lastAttemptedFailureAt,
|
|
247
|
-
@ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt,
|
|
267
|
+
@ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt, @orchestrationSettleUntil,
|
|
248
268
|
@now
|
|
249
269
|
)
|
|
250
270
|
`).run({
|
|
251
271
|
projectId: params.projectId,
|
|
252
272
|
linearIssueId: params.linearIssueId,
|
|
253
273
|
delegatedToPatchRelay: params.delegatedToPatchRelay === false ? 0 : 1,
|
|
274
|
+
issueClass: params.issueClass ?? null,
|
|
275
|
+
issueClassSource: params.issueClassSource ?? null,
|
|
276
|
+
parentLinearIssueId: params.parentLinearIssueId ?? null,
|
|
277
|
+
parentIssueKey: params.parentIssueKey ?? null,
|
|
254
278
|
issueKey: params.issueKey ?? null,
|
|
255
279
|
title: params.title ?? null,
|
|
256
280
|
description: params.description ?? null,
|
|
@@ -300,6 +324,7 @@ export class IssueStore {
|
|
|
300
324
|
reviewFixAttempts: params.reviewFixAttempts ?? 0,
|
|
301
325
|
zombieRecoveryAttempts: params.zombieRecoveryAttempts ?? 0,
|
|
302
326
|
lastZombieRecoveryAt: params.lastZombieRecoveryAt ?? null,
|
|
327
|
+
orchestrationSettleUntil: params.orchestrationSettleUntil ?? null,
|
|
303
328
|
now,
|
|
304
329
|
});
|
|
305
330
|
}
|
|
@@ -446,6 +471,67 @@ export class IssueStore {
|
|
|
446
471
|
linearIssueId: String(row.linear_issue_id),
|
|
447
472
|
}));
|
|
448
473
|
}
|
|
474
|
+
replaceIssueParentLink(params) {
|
|
475
|
+
const now = isoNow();
|
|
476
|
+
this.connection
|
|
477
|
+
.prepare("DELETE FROM issue_children WHERE project_id = ? AND child_linear_issue_id = ?")
|
|
478
|
+
.run(params.projectId, params.childLinearIssueId);
|
|
479
|
+
if (!params.parentLinearIssueId) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
this.connection.prepare(`
|
|
483
|
+
INSERT INTO issue_children (
|
|
484
|
+
project_id,
|
|
485
|
+
parent_linear_issue_id,
|
|
486
|
+
child_linear_issue_id,
|
|
487
|
+
updated_at
|
|
488
|
+
) VALUES (?, ?, ?, ?)
|
|
489
|
+
`).run(params.projectId, params.parentLinearIssueId, params.childLinearIssueId, now);
|
|
490
|
+
}
|
|
491
|
+
listChildLinks(projectId, parentLinearIssueId) {
|
|
492
|
+
const rows = this.connection.prepare(`
|
|
493
|
+
SELECT project_id, parent_linear_issue_id, child_linear_issue_id, updated_at
|
|
494
|
+
FROM issue_children
|
|
495
|
+
WHERE project_id = ? AND parent_linear_issue_id = ?
|
|
496
|
+
ORDER BY child_linear_issue_id ASC
|
|
497
|
+
`).all(projectId, parentLinearIssueId);
|
|
498
|
+
return rows.map((row) => ({
|
|
499
|
+
projectId: String(row.project_id),
|
|
500
|
+
parentLinearIssueId: String(row.parent_linear_issue_id),
|
|
501
|
+
childLinearIssueId: String(row.child_linear_issue_id),
|
|
502
|
+
updatedAt: String(row.updated_at),
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
listChildIssues(projectId, parentLinearIssueId) {
|
|
506
|
+
const rows = this.connection.prepare(`
|
|
507
|
+
SELECT child.*
|
|
508
|
+
FROM issue_children edges
|
|
509
|
+
JOIN issues child
|
|
510
|
+
ON child.project_id = edges.project_id
|
|
511
|
+
AND child.linear_issue_id = edges.child_linear_issue_id
|
|
512
|
+
WHERE edges.project_id = ? AND edges.parent_linear_issue_id = ?
|
|
513
|
+
ORDER BY COALESCE(child.issue_key, child.linear_issue_id) ASC
|
|
514
|
+
`).all(projectId, parentLinearIssueId);
|
|
515
|
+
return rows.map(mapIssueRow);
|
|
516
|
+
}
|
|
517
|
+
countOpenChildIssues(projectId, parentLinearIssueId) {
|
|
518
|
+
const row = this.connection.prepare(`
|
|
519
|
+
SELECT COUNT(*) AS count
|
|
520
|
+
FROM issue_children edges
|
|
521
|
+
LEFT JOIN issues child
|
|
522
|
+
ON child.project_id = edges.project_id
|
|
523
|
+
AND child.linear_issue_id = edges.child_linear_issue_id
|
|
524
|
+
WHERE edges.project_id = ? AND edges.parent_linear_issue_id = ?
|
|
525
|
+
AND (
|
|
526
|
+
child.linear_issue_id IS NULL
|
|
527
|
+
OR (
|
|
528
|
+
COALESCE(child.current_linear_state_type, '') NOT IN ('completed', 'canceled')
|
|
529
|
+
AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('done', 'duplicate', 'canceled')
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
`).get(projectId, parentLinearIssueId);
|
|
533
|
+
return Number(row?.count ?? 0);
|
|
534
|
+
}
|
|
449
535
|
countUnresolvedBlockers(projectId, linearIssueId) {
|
|
450
536
|
const row = this.connection.prepare(`
|
|
451
537
|
SELECT COUNT(*) AS count
|
|
@@ -479,6 +565,14 @@ export function mapIssueRow(row) {
|
|
|
479
565
|
projectId: String(row.project_id),
|
|
480
566
|
linearIssueId: String(row.linear_issue_id),
|
|
481
567
|
delegatedToPatchRelay: Number(row.delegated_to_patchrelay ?? 1) !== 0,
|
|
568
|
+
...(row.issue_class !== null && row.issue_class !== undefined ? { issueClass: String(row.issue_class) } : {}),
|
|
569
|
+
...(row.issue_class_source !== null && row.issue_class_source !== undefined
|
|
570
|
+
? { issueClassSource: String(row.issue_class_source) }
|
|
571
|
+
: {}),
|
|
572
|
+
...(row.parent_linear_issue_id !== null && row.parent_linear_issue_id !== undefined
|
|
573
|
+
? { parentLinearIssueId: String(row.parent_linear_issue_id) }
|
|
574
|
+
: {}),
|
|
575
|
+
...(row.parent_issue_key !== null && row.parent_issue_key !== undefined ? { parentIssueKey: String(row.parent_issue_key) } : {}),
|
|
482
576
|
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
483
577
|
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
484
578
|
...(row.description !== null && row.description !== undefined ? { description: String(row.description) } : {}),
|
|
@@ -569,5 +663,8 @@ export function mapIssueRow(row) {
|
|
|
569
663
|
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
570
664
|
zombieRecoveryAttempts: Number(row.zombie_recovery_attempts ?? 0),
|
|
571
665
|
...(row.last_zombie_recovery_at !== null && row.last_zombie_recovery_at !== undefined ? { lastZombieRecoveryAt: String(row.last_zombie_recovery_at) } : {}),
|
|
666
|
+
...(row.orchestration_settle_until !== null && row.orchestration_settle_until !== undefined
|
|
667
|
+
? { orchestrationSettleUntil: String(row.orchestration_settle_until) }
|
|
668
|
+
: {}),
|
|
572
669
|
};
|
|
573
670
|
}
|
package/dist/db/migrations.js
CHANGED
|
@@ -4,6 +4,10 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
4
4
|
project_id TEXT NOT NULL,
|
|
5
5
|
linear_issue_id TEXT NOT NULL,
|
|
6
6
|
delegated_to_patchrelay INTEGER NOT NULL DEFAULT 1,
|
|
7
|
+
issue_class TEXT,
|
|
8
|
+
issue_class_source TEXT,
|
|
9
|
+
parent_linear_issue_id TEXT,
|
|
10
|
+
parent_issue_key TEXT,
|
|
7
11
|
issue_key TEXT,
|
|
8
12
|
title TEXT,
|
|
9
13
|
url TEXT,
|
|
@@ -30,6 +34,7 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
30
34
|
last_blocking_review_head_sha TEXT,
|
|
31
35
|
ci_repair_attempts INTEGER NOT NULL DEFAULT 0,
|
|
32
36
|
queue_repair_attempts INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
orchestration_settle_until TEXT,
|
|
33
38
|
updated_at TEXT NOT NULL,
|
|
34
39
|
UNIQUE(project_id, linear_issue_id)
|
|
35
40
|
);
|
|
@@ -216,6 +221,14 @@ CREATE TABLE IF NOT EXISTS issue_dependencies (
|
|
|
216
221
|
PRIMARY KEY (project_id, linear_issue_id, blocker_linear_issue_id)
|
|
217
222
|
);
|
|
218
223
|
|
|
224
|
+
CREATE TABLE IF NOT EXISTS issue_children (
|
|
225
|
+
project_id TEXT NOT NULL,
|
|
226
|
+
parent_linear_issue_id TEXT NOT NULL,
|
|
227
|
+
child_linear_issue_id TEXT NOT NULL,
|
|
228
|
+
updated_at TEXT NOT NULL,
|
|
229
|
+
PRIMARY KEY (project_id, parent_linear_issue_id, child_linear_issue_id)
|
|
230
|
+
);
|
|
231
|
+
|
|
219
232
|
CREATE INDEX IF NOT EXISTS idx_issues_project ON issues(project_id, linear_issue_id);
|
|
220
233
|
CREATE INDEX IF NOT EXISTS idx_issues_key ON issues(issue_key);
|
|
221
234
|
CREATE INDEX IF NOT EXISTS idx_issues_ready ON issues(pending_run_type, active_run_id);
|
|
@@ -236,12 +249,28 @@ CREATE INDEX IF NOT EXISTS idx_linear_catalog_teams_installation ON linear_catal
|
|
|
236
249
|
CREATE INDEX IF NOT EXISTS idx_linear_catalog_projects_installation ON linear_catalog_projects(installation_id, project_name);
|
|
237
250
|
CREATE INDEX IF NOT EXISTS idx_issue_dependencies_issue ON issue_dependencies(project_id, linear_issue_id);
|
|
238
251
|
CREATE INDEX IF NOT EXISTS idx_issue_dependencies_blocker ON issue_dependencies(project_id, blocker_linear_issue_id);
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_issue_children_parent ON issue_children(project_id, parent_linear_issue_id);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_issue_children_child ON issue_children(project_id, child_linear_issue_id);
|
|
239
254
|
`;
|
|
240
255
|
export function runPatchRelayMigrations(connection) {
|
|
241
256
|
connection.exec(schema);
|
|
242
257
|
// Clean up stale dedupe-only webhook records (no payload, never processable)
|
|
243
258
|
connection.prepare("UPDATE webhook_events SET processing_status = 'processed' WHERE processing_status = 'pending' AND payload_json IS NULL").run();
|
|
244
259
|
addColumnIfMissing(connection, "issues", "delegated_to_patchrelay", "INTEGER NOT NULL DEFAULT 1");
|
|
260
|
+
addColumnIfMissing(connection, "issues", "issue_class", "TEXT");
|
|
261
|
+
addColumnIfMissing(connection, "issues", "issue_class_source", "TEXT");
|
|
262
|
+
addColumnIfMissing(connection, "issues", "parent_linear_issue_id", "TEXT");
|
|
263
|
+
addColumnIfMissing(connection, "issues", "parent_issue_key", "TEXT");
|
|
264
|
+
addColumnIfMissing(connection, "issues", "orchestration_settle_until", "TEXT");
|
|
265
|
+
// Earlier releases persisted derived classifications as "explicit", which
|
|
266
|
+
// made bad umbrella guesses sticky forever. We do not have a user-authored
|
|
267
|
+
// explicit classification path yet, so downgrade old rows back to heuristic
|
|
268
|
+
// and let current classification logic recompute them.
|
|
269
|
+
connection.prepare(`
|
|
270
|
+
UPDATE issues
|
|
271
|
+
SET issue_class_source = 'heuristic'
|
|
272
|
+
WHERE issue_class_source = 'explicit'
|
|
273
|
+
`).run();
|
|
245
274
|
// Add pending_merge_prep column for merge queue stewardship
|
|
246
275
|
addColumnIfMissing(connection, "issues", "pending_merge_prep", "INTEGER NOT NULL DEFAULT 0");
|
|
247
276
|
// Add merge_prep_attempts for retry budget / escalation
|
|
@@ -324,6 +353,10 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
324
353
|
project_id TEXT NOT NULL,
|
|
325
354
|
linear_issue_id TEXT NOT NULL,
|
|
326
355
|
delegated_to_patchrelay INTEGER NOT NULL DEFAULT 1,
|
|
356
|
+
issue_class TEXT,
|
|
357
|
+
issue_class_source TEXT,
|
|
358
|
+
parent_linear_issue_id TEXT,
|
|
359
|
+
parent_issue_key TEXT,
|
|
327
360
|
issue_key TEXT,
|
|
328
361
|
title TEXT,
|
|
329
362
|
description TEXT,
|
|
@@ -373,6 +406,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
373
406
|
review_fix_attempts INTEGER NOT NULL DEFAULT 0,
|
|
374
407
|
zombie_recovery_attempts INTEGER NOT NULL DEFAULT 0,
|
|
375
408
|
last_zombie_recovery_at TEXT,
|
|
409
|
+
orchestration_settle_until TEXT,
|
|
376
410
|
updated_at TEXT NOT NULL,
|
|
377
411
|
UNIQUE(project_id, linear_issue_id)
|
|
378
412
|
);
|
|
@@ -382,6 +416,10 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
382
416
|
project_id,
|
|
383
417
|
linear_issue_id,
|
|
384
418
|
delegated_to_patchrelay,
|
|
419
|
+
issue_class,
|
|
420
|
+
issue_class_source,
|
|
421
|
+
parent_linear_issue_id,
|
|
422
|
+
parent_issue_key,
|
|
385
423
|
issue_key,
|
|
386
424
|
title,
|
|
387
425
|
description,
|
|
@@ -431,6 +469,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
431
469
|
review_fix_attempts,
|
|
432
470
|
zombie_recovery_attempts,
|
|
433
471
|
last_zombie_recovery_at,
|
|
472
|
+
orchestration_settle_until,
|
|
434
473
|
updated_at
|
|
435
474
|
)
|
|
436
475
|
SELECT
|
|
@@ -438,6 +477,10 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
438
477
|
project_id,
|
|
439
478
|
linear_issue_id,
|
|
440
479
|
COALESCE(delegated_to_patchrelay, 1),
|
|
480
|
+
issue_class,
|
|
481
|
+
issue_class_source,
|
|
482
|
+
parent_linear_issue_id,
|
|
483
|
+
parent_issue_key,
|
|
441
484
|
issue_key,
|
|
442
485
|
title,
|
|
443
486
|
description,
|
|
@@ -487,6 +530,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
487
530
|
COALESCE(review_fix_attempts, 0),
|
|
488
531
|
COALESCE(zombie_recovery_attempts, 0),
|
|
489
532
|
last_zombie_recovery_at,
|
|
533
|
+
orchestration_settle_until,
|
|
490
534
|
updated_at
|
|
491
535
|
FROM issues;
|
|
492
536
|
|
package/dist/db.js
CHANGED
|
@@ -154,6 +154,18 @@ export class PatchRelayDatabase {
|
|
|
154
154
|
listDependents(projectId, blockerLinearIssueId) {
|
|
155
155
|
return this.issues.listDependents(projectId, blockerLinearIssueId);
|
|
156
156
|
}
|
|
157
|
+
replaceIssueParentLink(params) {
|
|
158
|
+
this.issues.replaceIssueParentLink(params);
|
|
159
|
+
}
|
|
160
|
+
listChildLinks(projectId, parentLinearIssueId) {
|
|
161
|
+
return this.issues.listChildLinks(projectId, parentLinearIssueId);
|
|
162
|
+
}
|
|
163
|
+
listChildIssues(projectId, parentLinearIssueId) {
|
|
164
|
+
return this.issues.listChildIssues(projectId, parentLinearIssueId);
|
|
165
|
+
}
|
|
166
|
+
countOpenChildIssues(projectId, parentLinearIssueId) {
|
|
167
|
+
return this.issues.countOpenChildIssues(projectId, parentLinearIssueId);
|
|
168
|
+
}
|
|
157
169
|
getLatestGitHubCiSnapshot(projectId, linearIssueId) {
|
|
158
170
|
return this.issues.getLatestGitHubCiSnapshot(projectId, linearIssueId);
|
|
159
171
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveClosedPrDisposition, resolveClosedPrFactoryState } from "./pr-state.js";
|
|
2
2
|
import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
|
|
3
3
|
import { syncGitHubLinearSession } from "./github-linear-session-sync.js";
|
|
4
|
+
import { wakeOrchestrationParentsForChildEvent } from "./orchestration-parent-wake.js";
|
|
4
5
|
export async function handleGitHubTerminalPrEvent(params) {
|
|
5
6
|
const { db, linearProvider, enqueueIssue, logger, codex, issue, event, config } = params;
|
|
6
7
|
const eventType = event.triggerEvent === "pr_merged" ? "pr_merged" : "pr_closed";
|
|
@@ -66,6 +67,12 @@ export async function handleGitHubTerminalPrEvent(params) {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
if (event.triggerEvent === "pr_merged") {
|
|
70
|
+
wakeOrchestrationParentsForChildEvent({
|
|
71
|
+
db,
|
|
72
|
+
child: updatedIssue,
|
|
73
|
+
eventType: "child_delivered",
|
|
74
|
+
enqueueIssue,
|
|
75
|
+
});
|
|
69
76
|
await completeLinearIssueAfterMerge(params, updatedIssue);
|
|
70
77
|
}
|
|
71
78
|
void syncGitHubLinearSession({
|
|
@@ -5,6 +5,7 @@ import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
|
5
5
|
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
6
6
|
import { buildClosedPrCleanupFields, resolveClosedPrDisposition } from "./pr-state.js";
|
|
7
7
|
import { getReviewFixBudget } from "./run-budgets.js";
|
|
8
|
+
import { queueSettledOrchestrationIssue } from "./orchestration-parent-wake.js";
|
|
8
9
|
import { execCommand } from "./utils.js";
|
|
9
10
|
function isFailingCheckStatus(status) {
|
|
10
11
|
return status === "failed" || status === "failure";
|
|
@@ -170,6 +171,24 @@ export class IdleIssueReconciler {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
}
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
for (const issue of this.db.issues.listIssues()) {
|
|
176
|
+
if (issue.issueClass !== "orchestration"
|
|
177
|
+
|| !issue.orchestrationSettleUntil
|
|
178
|
+
|| issue.activeRunId !== undefined
|
|
179
|
+
|| !issue.delegatedToPatchRelay) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const settleAt = Date.parse(issue.orchestrationSettleUntil);
|
|
183
|
+
if (!Number.isFinite(settleAt) || settleAt > now) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
queueSettledOrchestrationIssue({
|
|
187
|
+
db: this.db,
|
|
188
|
+
issue,
|
|
189
|
+
enqueueIssue: this.deps.enqueueIssue,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
173
192
|
}
|
|
174
193
|
shouldProbeTerminalIssueFromGitHub(issue) {
|
|
175
194
|
if (issue.prNumber === undefined)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function normalizeText(value) {
|
|
2
|
+
return value?.trim().toLowerCase() ?? "";
|
|
3
|
+
}
|
|
4
|
+
function looksLikeUmbrellaText(issue) {
|
|
5
|
+
const haystack = `${normalizeText(issue.title)}\n${normalizeText(issue.description)}`;
|
|
6
|
+
if (!haystack.trim())
|
|
7
|
+
return false;
|
|
8
|
+
return [
|
|
9
|
+
"umbrella",
|
|
10
|
+
"tracker",
|
|
11
|
+
"tracking",
|
|
12
|
+
"rollout",
|
|
13
|
+
"migration",
|
|
14
|
+
"convergence",
|
|
15
|
+
"audit",
|
|
16
|
+
"follow-up issues",
|
|
17
|
+
"planning/specification issue only",
|
|
18
|
+
].some((token) => haystack.includes(token));
|
|
19
|
+
}
|
|
20
|
+
export function classifyIssue(params) {
|
|
21
|
+
if (params.issue.issueClassSource === "explicit"
|
|
22
|
+
&& (params.issue.issueClass === "implementation" || params.issue.issueClass === "orchestration")) {
|
|
23
|
+
return { issueClass: params.issue.issueClass, issueClassSource: "explicit" };
|
|
24
|
+
}
|
|
25
|
+
if (params.issue.parentLinearIssueId) {
|
|
26
|
+
return { issueClass: "implementation", issueClassSource: "hierarchy" };
|
|
27
|
+
}
|
|
28
|
+
if (params.childIssueCount > 0) {
|
|
29
|
+
return { issueClass: "orchestration", issueClassSource: "hierarchy" };
|
|
30
|
+
}
|
|
31
|
+
if (looksLikeUmbrellaText(params.issue)) {
|
|
32
|
+
return { issueClass: "orchestration", issueClassSource: "heuristic" };
|
|
33
|
+
}
|
|
34
|
+
return { issueClass: "implementation", issueClassSource: "heuristic" };
|
|
35
|
+
}
|
|
@@ -94,6 +94,7 @@ export class IssueOverviewQuery {
|
|
|
94
94
|
blockedByKeys,
|
|
95
95
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
96
96
|
pendingRunType: issueRecord?.pendingRunType,
|
|
97
|
+
orchestrationSettleUntil: issueRecord?.orchestrationSettleUntil,
|
|
97
98
|
prNumber: session.prNumber,
|
|
98
99
|
prState: issueRecord?.prState,
|
|
99
100
|
prHeadSha: issueRecord?.prHeadSha ?? session.prHeadSha,
|
|
@@ -128,6 +129,7 @@ export class IssueOverviewQuery {
|
|
|
128
129
|
blockedByCount: unresolvedBlockedBy.length,
|
|
129
130
|
hasPendingWake: this.db.issueSessions.peekIssueSessionWake(session.projectId, session.linearIssueId) !== undefined,
|
|
130
131
|
hasLegacyPendingRun: issueRecord?.pendingRunType !== undefined,
|
|
132
|
+
orchestrationSettleUntil: issueRecord?.orchestrationSettleUntil,
|
|
131
133
|
...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
|
|
132
134
|
...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
|
|
133
135
|
...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
|
|
@@ -52,7 +52,7 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
52
52
|
case "delegated":
|
|
53
53
|
if (!runType) {
|
|
54
54
|
runType = "implementation";
|
|
55
|
-
wakeReason = "delegated";
|
|
55
|
+
wakeReason = issue.issueClass === "orchestration" ? "initial_delegate" : "delegated";
|
|
56
56
|
}
|
|
57
57
|
if (payload?.promptContext !== undefined) {
|
|
58
58
|
context.promptContext = payload.promptContext;
|
|
@@ -61,6 +61,16 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
61
61
|
context.promptBody = payload.promptBody;
|
|
62
62
|
}
|
|
63
63
|
break;
|
|
64
|
+
case "child_changed":
|
|
65
|
+
case "child_delivered":
|
|
66
|
+
case "child_regressed":
|
|
67
|
+
if (!runType) {
|
|
68
|
+
runType = "implementation";
|
|
69
|
+
wakeReason = event.eventType;
|
|
70
|
+
}
|
|
71
|
+
Object.assign(context, payload ?? {});
|
|
72
|
+
resumeThread = true;
|
|
73
|
+
break;
|
|
64
74
|
case "direct_reply": {
|
|
65
75
|
if (!runType) {
|
|
66
76
|
runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
|
|
@@ -98,7 +108,7 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
98
108
|
case "operator_prompt": {
|
|
99
109
|
if (!runType) {
|
|
100
110
|
runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
|
|
101
|
-
wakeReason = event.eventType;
|
|
111
|
+
wakeReason = issue.issueClass === "orchestration" ? "human_instruction" : event.eventType;
|
|
102
112
|
}
|
|
103
113
|
const text = typeof payload?.text === "string"
|
|
104
114
|
? payload.text
|
package/dist/issue-session.js
CHANGED
|
@@ -88,6 +88,12 @@ export function isIssueSessionReadyForExecution(params) {
|
|
|
88
88
|
return false;
|
|
89
89
|
if (params.blockedByCount > 0)
|
|
90
90
|
return false;
|
|
91
|
+
if (params.orchestrationSettleUntil) {
|
|
92
|
+
const settleAt = Date.parse(params.orchestrationSettleUntil);
|
|
93
|
+
if (Number.isFinite(settleAt) && settleAt > Date.now()) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
91
97
|
if (params.sessionState === "done" || params.sessionState === "waiting_input") {
|
|
92
98
|
return false;
|
|
93
99
|
}
|
package/dist/linear-client.js
CHANGED
|
@@ -2,6 +2,16 @@ import { refreshLinearOAuthToken } from "./linear-oauth.js";
|
|
|
2
2
|
import { decryptSecret, encryptSecret } from "./token-crypto.js";
|
|
3
3
|
const LINEAR_ISSUE_SELECTION = `
|
|
4
4
|
id
|
|
5
|
+
parent {
|
|
6
|
+
id
|
|
7
|
+
identifier
|
|
8
|
+
title
|
|
9
|
+
state {
|
|
10
|
+
id
|
|
11
|
+
name
|
|
12
|
+
type
|
|
13
|
+
}
|
|
14
|
+
}
|
|
5
15
|
identifier
|
|
6
16
|
title
|
|
7
17
|
description
|
|
@@ -334,6 +344,9 @@ export class LinearGraphqlClient {
|
|
|
334
344
|
}));
|
|
335
345
|
return {
|
|
336
346
|
id: issue.id,
|
|
347
|
+
...(issue.parent?.id ? { parentId: issue.parent.id } : {}),
|
|
348
|
+
...(issue.parent?.identifier ? { parentIdentifier: issue.parent.identifier } : {}),
|
|
349
|
+
...(issue.parent?.title ? { parentTitle: issue.parent.title } : {}),
|
|
337
350
|
...(issue.identifier ? { identifier: issue.identifier } : {}),
|
|
338
351
|
...(issue.title ? { title: issue.title } : {}),
|
|
339
352
|
...(issue.description ? { description: issue.description } : {}),
|
|
@@ -40,6 +40,7 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
40
40
|
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
41
41
|
factoryState: issue.factoryState,
|
|
42
42
|
pendingRunType: issue.pendingRunType,
|
|
43
|
+
orchestrationSettleUntil: issue.orchestrationSettleUntil,
|
|
43
44
|
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
44
45
|
...(issue.prState ? { prState: issue.prState } : {}),
|
|
45
46
|
prHeadSha: issue.prHeadSha,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildCompletionCheckActivity } from "./linear-session-reporting.js";
|
|
2
|
+
import { wakeOrchestrationParentsForChildEvent } from "./orchestration-parent-wake.js";
|
|
2
3
|
function shouldContinueForUnpublishedLocalChanges(message) {
|
|
3
4
|
const normalized = message.trim().toLowerCase();
|
|
4
5
|
if (!normalized)
|
|
@@ -167,6 +168,9 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
167
168
|
});
|
|
168
169
|
return;
|
|
169
170
|
}
|
|
171
|
+
const orchestrationOpenChildren = params.issue.issueClass === "orchestration"
|
|
172
|
+
? params.db.issues.countOpenChildIssues(params.run.projectId, params.run.linearIssueId)
|
|
173
|
+
: 0;
|
|
170
174
|
const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
171
175
|
params.db.runs.finishRun(params.run.id, completedRunUpdate);
|
|
172
176
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
|
@@ -175,9 +179,10 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
175
179
|
projectId: params.run.projectId,
|
|
176
180
|
linearIssueId: params.run.linearIssueId,
|
|
177
181
|
activeRunId: null,
|
|
178
|
-
factoryState: "done",
|
|
182
|
+
factoryState: params.issue.issueClass === "orchestration" && orchestrationOpenChildren > 0 ? "delegated" : "done",
|
|
179
183
|
pendingRunType: null,
|
|
180
184
|
pendingRunContextJson: null,
|
|
185
|
+
orchestrationSettleUntil: null,
|
|
181
186
|
lastGitHubFailureSource: null,
|
|
182
187
|
lastGitHubFailureHeadSha: null,
|
|
183
188
|
lastGitHubFailureSignature: null,
|
|
@@ -202,10 +207,20 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
202
207
|
fallbackIssue: params.issue,
|
|
203
208
|
level: "info",
|
|
204
209
|
status: "completion_check_done",
|
|
205
|
-
summary: "
|
|
206
|
-
|
|
210
|
+
summary: params.issue.issueClass === "orchestration" && orchestrationOpenChildren > 0
|
|
211
|
+
? "No PR found; orchestration will wait on child deliveries"
|
|
212
|
+
: "No PR found; confirmed done",
|
|
213
|
+
detail: params.issue.issueClass === "orchestration" && orchestrationOpenChildren > 0
|
|
214
|
+
? `${completionCheck.summary} Waiting on ${orchestrationOpenChildren} open child issue(s) before final convergence.`
|
|
215
|
+
: completionCheck.summary,
|
|
207
216
|
activity: buildCompletionCheckActivity("done", completionCheck),
|
|
208
217
|
});
|
|
218
|
+
const doneIssue = params.db.issues.getIssue(params.run.projectId, params.run.linearIssueId) ?? params.issue;
|
|
219
|
+
wakeOrchestrationParentsForChildEvent({
|
|
220
|
+
db: params.db,
|
|
221
|
+
child: doneIssue,
|
|
222
|
+
eventType: "child_delivered",
|
|
223
|
+
});
|
|
209
224
|
return;
|
|
210
225
|
}
|
|
211
226
|
const failureReason = `No PR observed and the completion check failed this run: ${completionCheck.summary}`;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { classifyIssue } from "./issue-class.js";
|
|
2
|
+
export const ORCHESTRATION_SETTLE_WINDOW_MS = 10_000;
|
|
3
|
+
export function computeOrchestrationSettleUntil(now = Date.now()) {
|
|
4
|
+
return new Date(now + ORCHESTRATION_SETTLE_WINDOW_MS).toISOString();
|
|
5
|
+
}
|
|
6
|
+
function resolveOrchestrationIssueClass(db, issue) {
|
|
7
|
+
return classifyIssue({
|
|
8
|
+
issue,
|
|
9
|
+
childIssueCount: db.issues.listChildIssues(issue.projectId, issue.linearIssueId).length,
|
|
10
|
+
}).issueClass;
|
|
11
|
+
}
|
|
12
|
+
function unique(values) {
|
|
13
|
+
return [...new Set(values)];
|
|
14
|
+
}
|
|
15
|
+
function resolveParentIssueIds(db, child) {
|
|
16
|
+
const parentIds = [];
|
|
17
|
+
if (child.parentLinearIssueId) {
|
|
18
|
+
parentIds.push(child.parentLinearIssueId);
|
|
19
|
+
}
|
|
20
|
+
for (const blocker of db.issues.listIssueDependencies(child.projectId, child.linearIssueId)) {
|
|
21
|
+
parentIds.push(blocker.blockerLinearIssueId);
|
|
22
|
+
}
|
|
23
|
+
return unique(parentIds);
|
|
24
|
+
}
|
|
25
|
+
export function startOrchestrationSettleWindow(db, issue, now = Date.now()) {
|
|
26
|
+
const settleUntil = computeOrchestrationSettleUntil(now);
|
|
27
|
+
db.issues.upsertIssue({
|
|
28
|
+
projectId: issue.projectId,
|
|
29
|
+
linearIssueId: issue.linearIssueId,
|
|
30
|
+
orchestrationSettleUntil: settleUntil,
|
|
31
|
+
});
|
|
32
|
+
return settleUntil;
|
|
33
|
+
}
|
|
34
|
+
export function queueSettledOrchestrationIssue(params) {
|
|
35
|
+
params.db.issues.upsertIssue({
|
|
36
|
+
projectId: params.issue.projectId,
|
|
37
|
+
linearIssueId: params.issue.linearIssueId,
|
|
38
|
+
orchestrationSettleUntil: null,
|
|
39
|
+
});
|
|
40
|
+
params.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.issue.projectId, params.issue.linearIssueId, {
|
|
41
|
+
projectId: params.issue.projectId,
|
|
42
|
+
linearIssueId: params.issue.linearIssueId,
|
|
43
|
+
eventType: "delegated",
|
|
44
|
+
eventJson: JSON.stringify({
|
|
45
|
+
...(params.promptContext
|
|
46
|
+
? { promptContext: params.promptContext }
|
|
47
|
+
: { promptContext: "The orchestration child set has settled enough to begin planning." }),
|
|
48
|
+
}),
|
|
49
|
+
dedupeKey: `delegated:orchestration_settle:${params.issue.linearIssueId}`,
|
|
50
|
+
});
|
|
51
|
+
if (params.db.issueSessions.peekIssueSessionWake(params.issue.projectId, params.issue.linearIssueId)) {
|
|
52
|
+
params.enqueueIssue?.(params.issue.projectId, params.issue.linearIssueId);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
export function wakeOrchestrationParentsForChildEvent(params) {
|
|
58
|
+
const parentIds = [];
|
|
59
|
+
for (const parentIssueId of resolveParentIssueIds(params.db, params.child)) {
|
|
60
|
+
const parent = params.db.issues.getIssue(params.child.projectId, parentIssueId);
|
|
61
|
+
if (!parent || !parent.delegatedToPatchRelay) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (resolveOrchestrationIssueClass(params.db, parent) !== "orchestration") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Before the umbrella has started its first turn, keep absorbing nearby
|
|
68
|
+
// child-set changes into the settle window instead of launching too early.
|
|
69
|
+
if (!parent.threadId && parent.activeRunId === undefined && parent.orchestrationSettleUntil) {
|
|
70
|
+
startOrchestrationSettleWindow(params.db, parent, params.now);
|
|
71
|
+
parentIds.push(parent.linearIssueId);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
params.db.issueSessions.appendIssueSessionEventRespectingActiveLease(parent.projectId, parent.linearIssueId, {
|
|
75
|
+
projectId: parent.projectId,
|
|
76
|
+
linearIssueId: parent.linearIssueId,
|
|
77
|
+
eventType: params.eventType,
|
|
78
|
+
eventJson: JSON.stringify({
|
|
79
|
+
childIssueId: params.child.linearIssueId,
|
|
80
|
+
...(params.child.issueKey ? { childIssueKey: params.child.issueKey } : {}),
|
|
81
|
+
...(params.child.title ? { childTitle: params.child.title } : {}),
|
|
82
|
+
factoryState: params.child.factoryState,
|
|
83
|
+
...(params.child.currentLinearState ? { currentLinearState: params.child.currentLinearState } : {}),
|
|
84
|
+
...(params.child.prNumber !== undefined ? { prNumber: params.child.prNumber } : {}),
|
|
85
|
+
...(params.child.prState ? { prState: params.child.prState } : {}),
|
|
86
|
+
...(params.changeKind ? { changeKind: params.changeKind } : {}),
|
|
87
|
+
}),
|
|
88
|
+
dedupeKey: `${params.eventType}:${parent.linearIssueId}:${params.child.linearIssueId}:${params.child.factoryState}:${params.changeKind ?? params.child.prState ?? "no-pr"}`,
|
|
89
|
+
});
|
|
90
|
+
if (params.db.issueSessions.peekIssueSessionWake(parent.projectId, parent.linearIssueId)) {
|
|
91
|
+
params.enqueueIssue?.(parent.projectId, parent.linearIssueId);
|
|
92
|
+
}
|
|
93
|
+
parentIds.push(parent.linearIssueId);
|
|
94
|
+
}
|
|
95
|
+
return unique(parentIds);
|
|
96
|
+
}
|