patchrelay 0.48.0 → 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 +2 -2
- package/dist/agent-session-plan.js +12 -0
- package/dist/build-info.json +3 -3
- package/dist/db/issue-store.js +87 -4
- package/dist/db/migrations.js +34 -0
- package/dist/db.js +12 -0
- package/dist/idle-reconciliation.js +19 -0
- package/dist/issue-class.js +6 -2
- package/dist/issue-overview-query.js +2 -0
- 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 +11 -3
- package/dist/orchestration-parent-wake.js +68 -9
- package/dist/prompting/patchrelay.js +18 -12
- package/dist/run-orchestrator.js +6 -8
- package/dist/tracked-issue-list-query.js +3 -0
- package/dist/tracked-issue-projector.js +2 -0
- package/dist/waiting-reason.js +7 -0
- package/dist/webhooks/comment-wake-handler.js +1 -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/README.md
CHANGED
|
@@ -119,8 +119,8 @@ See the [merge-steward package README](./packages/merge-steward/README.md) for t
|
|
|
119
119
|
- [Prompting](./docs/prompting.md) — how workflow files and the built-in scaffold compose
|
|
120
120
|
- [Secrets](./docs/secrets.md) — systemd credentials, resolution order
|
|
121
121
|
- [review-quill reference](./docs/review-quill.md) · [merge-steward reference](./docs/merge-steward.md)
|
|
122
|
-
- [Design docs](./docs/design-docs/index.md) · [Core beliefs](./docs/design-docs/core-beliefs.md)
|
|
123
|
-
- [Security policy](./SECURITY.md)
|
|
122
|
+
- [Product specs](./docs/product-specs/index.md) · [Design docs](./docs/design-docs/index.md) · [Core beliefs](./docs/design-docs/core-beliefs.md)
|
|
123
|
+
- [Contributing](./CONTRIBUTING.md) · [Security policy](./SECURITY.md)
|
|
124
124
|
|
|
125
125
|
## Status
|
|
126
126
|
|
|
@@ -104,6 +104,17 @@ function resolvePlanRunType(params) {
|
|
|
104
104
|
}
|
|
105
105
|
export function buildAgentSessionPlan(params) {
|
|
106
106
|
if (params.issueClass === "orchestration") {
|
|
107
|
+
const settling = params.orchestrationSettleUntil
|
|
108
|
+
? Number.isFinite(Date.parse(params.orchestrationSettleUntil)) && Date.parse(params.orchestrationSettleUntil) > Date.now()
|
|
109
|
+
: false;
|
|
110
|
+
if (settling) {
|
|
111
|
+
return [
|
|
112
|
+
{ content: "Wait for child set to settle", status: "inProgress" },
|
|
113
|
+
{ content: "Review umbrella goal and child set", status: "pending" },
|
|
114
|
+
{ content: "Wait for or inspect child progress", status: "pending" },
|
|
115
|
+
{ content: "Audit delivered outcome", status: "pending" },
|
|
116
|
+
];
|
|
117
|
+
}
|
|
107
118
|
switch (params.factoryState) {
|
|
108
119
|
case "done":
|
|
109
120
|
return setStatuses(orchestrationPlan(), ["completed", "completed", "completed", "completed"]);
|
|
@@ -181,6 +192,7 @@ export function buildAgentSessionPlanForIssue(issue, options) {
|
|
|
181
192
|
ciRepairAttempts: issue.ciRepairAttempts,
|
|
182
193
|
queueRepairAttempts: issue.queueRepairAttempts,
|
|
183
194
|
...(issue.issueClass ? { issueClass: issue.issueClass } : {}),
|
|
195
|
+
...(issue.orchestrationSettleUntil ? { orchestrationSettleUntil: issue.orchestrationSettleUntil } : {}),
|
|
184
196
|
...(issue.pendingRunType ? { pendingRunType: issue.pendingRunType } : {}),
|
|
185
197
|
...(options?.activeRunType ? { activeRunType: options.activeRunType } : {}),
|
|
186
198
|
});
|
package/dist/build-info.json
CHANGED
package/dist/db/issue-store.js
CHANGED
|
@@ -28,6 +28,14 @@ export class IssueStore {
|
|
|
28
28
|
sets.push("issue_class_source = @issueClassSource");
|
|
29
29
|
values.issueClassSource = params.issueClassSource;
|
|
30
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
|
+
}
|
|
31
39
|
if (params.issueKey !== undefined) {
|
|
32
40
|
sets.push("issue_key = COALESCE(@issueKey, issue_key)");
|
|
33
41
|
values.issueKey = params.issueKey;
|
|
@@ -224,12 +232,16 @@ export class IssueStore {
|
|
|
224
232
|
sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
|
|
225
233
|
values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
|
|
226
234
|
}
|
|
235
|
+
if (params.orchestrationSettleUntil !== undefined) {
|
|
236
|
+
sets.push("orchestration_settle_until = @orchestrationSettleUntil");
|
|
237
|
+
values.orchestrationSettleUntil = params.orchestrationSettleUntil;
|
|
238
|
+
}
|
|
227
239
|
this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
|
|
228
240
|
}
|
|
229
241
|
else {
|
|
230
242
|
this.connection.prepare(`
|
|
231
243
|
INSERT INTO issues (
|
|
232
|
-
project_id, linear_issue_id, delegated_to_patchrelay, issue_class, issue_class_source, 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,
|
|
233
245
|
priority, estimate,
|
|
234
246
|
current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
|
|
235
247
|
branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
|
|
@@ -239,10 +251,10 @@ export class IssueStore {
|
|
|
239
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,
|
|
240
252
|
last_queue_signal_at, last_queue_incident_json,
|
|
241
253
|
last_attempted_failure_head_sha, last_attempted_failure_signature, last_attempted_failure_at,
|
|
242
|
-
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,
|
|
243
255
|
updated_at
|
|
244
256
|
) VALUES (
|
|
245
|
-
@projectId, @linearIssueId, @delegatedToPatchRelay, @issueClass, @issueClassSource, @issueKey, @title, @description, @url,
|
|
257
|
+
@projectId, @linearIssueId, @delegatedToPatchRelay, @issueClass, @issueClassSource, @parentLinearIssueId, @parentIssueKey, @issueKey, @title, @description, @url,
|
|
246
258
|
@priority, @estimate,
|
|
247
259
|
@currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
248
260
|
@branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
|
|
@@ -252,7 +264,7 @@ export class IssueStore {
|
|
|
252
264
|
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
253
265
|
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
254
266
|
@lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature, @lastAttemptedFailureAt,
|
|
255
|
-
@ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt,
|
|
267
|
+
@ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt, @orchestrationSettleUntil,
|
|
256
268
|
@now
|
|
257
269
|
)
|
|
258
270
|
`).run({
|
|
@@ -261,6 +273,8 @@ export class IssueStore {
|
|
|
261
273
|
delegatedToPatchRelay: params.delegatedToPatchRelay === false ? 0 : 1,
|
|
262
274
|
issueClass: params.issueClass ?? null,
|
|
263
275
|
issueClassSource: params.issueClassSource ?? null,
|
|
276
|
+
parentLinearIssueId: params.parentLinearIssueId ?? null,
|
|
277
|
+
parentIssueKey: params.parentIssueKey ?? null,
|
|
264
278
|
issueKey: params.issueKey ?? null,
|
|
265
279
|
title: params.title ?? null,
|
|
266
280
|
description: params.description ?? null,
|
|
@@ -310,6 +324,7 @@ export class IssueStore {
|
|
|
310
324
|
reviewFixAttempts: params.reviewFixAttempts ?? 0,
|
|
311
325
|
zombieRecoveryAttempts: params.zombieRecoveryAttempts ?? 0,
|
|
312
326
|
lastZombieRecoveryAt: params.lastZombieRecoveryAt ?? null,
|
|
327
|
+
orchestrationSettleUntil: params.orchestrationSettleUntil ?? null,
|
|
313
328
|
now,
|
|
314
329
|
});
|
|
315
330
|
}
|
|
@@ -456,6 +471,67 @@ export class IssueStore {
|
|
|
456
471
|
linearIssueId: String(row.linear_issue_id),
|
|
457
472
|
}));
|
|
458
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
|
+
}
|
|
459
535
|
countUnresolvedBlockers(projectId, linearIssueId) {
|
|
460
536
|
const row = this.connection.prepare(`
|
|
461
537
|
SELECT COUNT(*) AS count
|
|
@@ -493,6 +569,10 @@ export function mapIssueRow(row) {
|
|
|
493
569
|
...(row.issue_class_source !== null && row.issue_class_source !== undefined
|
|
494
570
|
? { issueClassSource: String(row.issue_class_source) }
|
|
495
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) } : {}),
|
|
496
576
|
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
497
577
|
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
498
578
|
...(row.description !== null && row.description !== undefined ? { description: String(row.description) } : {}),
|
|
@@ -583,5 +663,8 @@ export function mapIssueRow(row) {
|
|
|
583
663
|
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
584
664
|
zombieRecoveryAttempts: Number(row.zombie_recovery_attempts ?? 0),
|
|
585
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
|
+
: {}),
|
|
586
669
|
};
|
|
587
670
|
}
|
package/dist/db/migrations.js
CHANGED
|
@@ -6,6 +6,8 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
6
6
|
delegated_to_patchrelay INTEGER NOT NULL DEFAULT 1,
|
|
7
7
|
issue_class TEXT,
|
|
8
8
|
issue_class_source TEXT,
|
|
9
|
+
parent_linear_issue_id TEXT,
|
|
10
|
+
parent_issue_key TEXT,
|
|
9
11
|
issue_key TEXT,
|
|
10
12
|
title TEXT,
|
|
11
13
|
url TEXT,
|
|
@@ -32,6 +34,7 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
32
34
|
last_blocking_review_head_sha TEXT,
|
|
33
35
|
ci_repair_attempts INTEGER NOT NULL DEFAULT 0,
|
|
34
36
|
queue_repair_attempts INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
orchestration_settle_until TEXT,
|
|
35
38
|
updated_at TEXT NOT NULL,
|
|
36
39
|
UNIQUE(project_id, linear_issue_id)
|
|
37
40
|
);
|
|
@@ -218,6 +221,14 @@ CREATE TABLE IF NOT EXISTS issue_dependencies (
|
|
|
218
221
|
PRIMARY KEY (project_id, linear_issue_id, blocker_linear_issue_id)
|
|
219
222
|
);
|
|
220
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
|
+
|
|
221
232
|
CREATE INDEX IF NOT EXISTS idx_issues_project ON issues(project_id, linear_issue_id);
|
|
222
233
|
CREATE INDEX IF NOT EXISTS idx_issues_key ON issues(issue_key);
|
|
223
234
|
CREATE INDEX IF NOT EXISTS idx_issues_ready ON issues(pending_run_type, active_run_id);
|
|
@@ -238,6 +249,8 @@ CREATE INDEX IF NOT EXISTS idx_linear_catalog_teams_installation ON linear_catal
|
|
|
238
249
|
CREATE INDEX IF NOT EXISTS idx_linear_catalog_projects_installation ON linear_catalog_projects(installation_id, project_name);
|
|
239
250
|
CREATE INDEX IF NOT EXISTS idx_issue_dependencies_issue ON issue_dependencies(project_id, linear_issue_id);
|
|
240
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);
|
|
241
254
|
`;
|
|
242
255
|
export function runPatchRelayMigrations(connection) {
|
|
243
256
|
connection.exec(schema);
|
|
@@ -246,6 +259,18 @@ export function runPatchRelayMigrations(connection) {
|
|
|
246
259
|
addColumnIfMissing(connection, "issues", "delegated_to_patchrelay", "INTEGER NOT NULL DEFAULT 1");
|
|
247
260
|
addColumnIfMissing(connection, "issues", "issue_class", "TEXT");
|
|
248
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();
|
|
249
274
|
// Add pending_merge_prep column for merge queue stewardship
|
|
250
275
|
addColumnIfMissing(connection, "issues", "pending_merge_prep", "INTEGER NOT NULL DEFAULT 0");
|
|
251
276
|
// Add merge_prep_attempts for retry budget / escalation
|
|
@@ -330,6 +355,8 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
330
355
|
delegated_to_patchrelay INTEGER NOT NULL DEFAULT 1,
|
|
331
356
|
issue_class TEXT,
|
|
332
357
|
issue_class_source TEXT,
|
|
358
|
+
parent_linear_issue_id TEXT,
|
|
359
|
+
parent_issue_key TEXT,
|
|
333
360
|
issue_key TEXT,
|
|
334
361
|
title TEXT,
|
|
335
362
|
description TEXT,
|
|
@@ -379,6 +406,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
379
406
|
review_fix_attempts INTEGER NOT NULL DEFAULT 0,
|
|
380
407
|
zombie_recovery_attempts INTEGER NOT NULL DEFAULT 0,
|
|
381
408
|
last_zombie_recovery_at TEXT,
|
|
409
|
+
orchestration_settle_until TEXT,
|
|
382
410
|
updated_at TEXT NOT NULL,
|
|
383
411
|
UNIQUE(project_id, linear_issue_id)
|
|
384
412
|
);
|
|
@@ -390,6 +418,8 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
390
418
|
delegated_to_patchrelay,
|
|
391
419
|
issue_class,
|
|
392
420
|
issue_class_source,
|
|
421
|
+
parent_linear_issue_id,
|
|
422
|
+
parent_issue_key,
|
|
393
423
|
issue_key,
|
|
394
424
|
title,
|
|
395
425
|
description,
|
|
@@ -439,6 +469,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
439
469
|
review_fix_attempts,
|
|
440
470
|
zombie_recovery_attempts,
|
|
441
471
|
last_zombie_recovery_at,
|
|
472
|
+
orchestration_settle_until,
|
|
442
473
|
updated_at
|
|
443
474
|
)
|
|
444
475
|
SELECT
|
|
@@ -448,6 +479,8 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
448
479
|
COALESCE(delegated_to_patchrelay, 1),
|
|
449
480
|
issue_class,
|
|
450
481
|
issue_class_source,
|
|
482
|
+
parent_linear_issue_id,
|
|
483
|
+
parent_issue_key,
|
|
451
484
|
issue_key,
|
|
452
485
|
title,
|
|
453
486
|
description,
|
|
@@ -497,6 +530,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
497
530
|
COALESCE(review_fix_attempts, 0),
|
|
498
531
|
COALESCE(zombie_recovery_attempts, 0),
|
|
499
532
|
last_zombie_recovery_at,
|
|
533
|
+
orchestration_settle_until,
|
|
500
534
|
updated_at
|
|
501
535
|
FROM issues;
|
|
502
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
|
}
|
|
@@ -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)
|
package/dist/issue-class.js
CHANGED
|
@@ -18,10 +18,14 @@ function looksLikeUmbrellaText(issue) {
|
|
|
18
18
|
].some((token) => haystack.includes(token));
|
|
19
19
|
}
|
|
20
20
|
export function classifyIssue(params) {
|
|
21
|
-
if (params.issue.
|
|
21
|
+
if (params.issue.issueClassSource === "explicit"
|
|
22
|
+
&& (params.issue.issueClass === "implementation" || params.issue.issueClass === "orchestration")) {
|
|
22
23
|
return { issueClass: params.issue.issueClass, issueClassSource: "explicit" };
|
|
23
24
|
}
|
|
24
|
-
if (params.
|
|
25
|
+
if (params.issue.parentLinearIssueId) {
|
|
26
|
+
return { issueClass: "implementation", issueClassSource: "hierarchy" };
|
|
27
|
+
}
|
|
28
|
+
if (params.childIssueCount > 0) {
|
|
25
29
|
return { issueClass: "orchestration", issueClassSource: "hierarchy" };
|
|
26
30
|
}
|
|
27
31
|
if (looksLikeUmbrellaText(params.issue)) {
|
|
@@ -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 } : {}),
|
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,
|
|
@@ -168,6 +168,9 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
168
168
|
});
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
|
+
const orchestrationOpenChildren = params.issue.issueClass === "orchestration"
|
|
172
|
+
? params.db.issues.countOpenChildIssues(params.run.projectId, params.run.linearIssueId)
|
|
173
|
+
: 0;
|
|
171
174
|
const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
172
175
|
params.db.runs.finishRun(params.run.id, completedRunUpdate);
|
|
173
176
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
|
@@ -176,9 +179,10 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
176
179
|
projectId: params.run.projectId,
|
|
177
180
|
linearIssueId: params.run.linearIssueId,
|
|
178
181
|
activeRunId: null,
|
|
179
|
-
factoryState: "done",
|
|
182
|
+
factoryState: params.issue.issueClass === "orchestration" && orchestrationOpenChildren > 0 ? "delegated" : "done",
|
|
180
183
|
pendingRunType: null,
|
|
181
184
|
pendingRunContextJson: null,
|
|
185
|
+
orchestrationSettleUntil: null,
|
|
182
186
|
lastGitHubFailureSource: null,
|
|
183
187
|
lastGitHubFailureHeadSha: null,
|
|
184
188
|
lastGitHubFailureSignature: null,
|
|
@@ -203,8 +207,12 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
203
207
|
fallbackIssue: params.issue,
|
|
204
208
|
level: "info",
|
|
205
209
|
status: "completion_check_done",
|
|
206
|
-
summary: "
|
|
207
|
-
|
|
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,
|
|
208
216
|
activity: buildCompletionCheckActivity("done", completionCheck),
|
|
209
217
|
});
|
|
210
218
|
const doneIssue = params.db.issues.getIssue(params.run.projectId, params.run.linearIssueId) ?? params.issue;
|
|
@@ -1,16 +1,74 @@
|
|
|
1
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
|
+
}
|
|
2
57
|
export function wakeOrchestrationParentsForChildEvent(params) {
|
|
3
58
|
const parentIds = [];
|
|
4
|
-
for (const
|
|
5
|
-
const parent = params.db.issues.getIssue(params.child.projectId,
|
|
59
|
+
for (const parentIssueId of resolveParentIssueIds(params.db, params.child)) {
|
|
60
|
+
const parent = params.db.issues.getIssue(params.child.projectId, parentIssueId);
|
|
6
61
|
if (!parent || !parent.delegatedToPatchRelay) {
|
|
7
62
|
continue;
|
|
8
63
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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);
|
|
14
72
|
continue;
|
|
15
73
|
}
|
|
16
74
|
params.db.issueSessions.appendIssueSessionEventRespectingActiveLease(parent.projectId, parent.linearIssueId, {
|
|
@@ -25,13 +83,14 @@ export function wakeOrchestrationParentsForChildEvent(params) {
|
|
|
25
83
|
...(params.child.currentLinearState ? { currentLinearState: params.child.currentLinearState } : {}),
|
|
26
84
|
...(params.child.prNumber !== undefined ? { prNumber: params.child.prNumber } : {}),
|
|
27
85
|
...(params.child.prState ? { prState: params.child.prState } : {}),
|
|
86
|
+
...(params.changeKind ? { changeKind: params.changeKind } : {}),
|
|
28
87
|
}),
|
|
29
|
-
dedupeKey: `${params.eventType}:${parent.linearIssueId}:${params.child.linearIssueId}:${params.child.factoryState}:${params.child.prState ?? "no-pr"}`,
|
|
88
|
+
dedupeKey: `${params.eventType}:${parent.linearIssueId}:${params.child.linearIssueId}:${params.child.factoryState}:${params.changeKind ?? params.child.prState ?? "no-pr"}`,
|
|
30
89
|
});
|
|
31
90
|
if (params.db.issueSessions.peekIssueSessionWake(parent.projectId, parent.linearIssueId)) {
|
|
32
91
|
params.enqueueIssue?.(parent.projectId, parent.linearIssueId);
|
|
33
92
|
}
|
|
34
93
|
parentIds.push(parent.linearIssueId);
|
|
35
94
|
}
|
|
36
|
-
return parentIds;
|
|
95
|
+
return unique(parentIds);
|
|
37
96
|
}
|
|
@@ -106,9 +106,11 @@ function buildCoordinationGuidance(context) {
|
|
|
106
106
|
const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
|
|
107
107
|
? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
108
108
|
: [];
|
|
109
|
-
const
|
|
110
|
-
? context.
|
|
111
|
-
:
|
|
109
|
+
const childIssues = Array.isArray(context?.childIssues)
|
|
110
|
+
? context.childIssues.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
111
|
+
: Array.isArray(context?.trackedDependents)
|
|
112
|
+
? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
113
|
+
: [];
|
|
112
114
|
const lines = [
|
|
113
115
|
"### Coordination / Issue Topology",
|
|
114
116
|
"",
|
|
@@ -117,7 +119,7 @@ function buildCoordinationGuidance(context) {
|
|
|
117
119
|
"When child issues already own the concrete code slices, use this issue to coordinate, create or refine follow-up issues, or verify convergence. Only ship code here if this issue still has unique implementation scope that is not already owned elsewhere.",
|
|
118
120
|
"Prefer one PR per concrete implementation issue over a broad parent branch that restates overlapping child work.",
|
|
119
121
|
];
|
|
120
|
-
if (unresolvedBlockers.length === 0 &&
|
|
122
|
+
if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
|
|
121
123
|
return lines;
|
|
122
124
|
}
|
|
123
125
|
lines.push("", "Known relations from PatchRelay:");
|
|
@@ -125,12 +127,12 @@ function buildCoordinationGuidance(context) {
|
|
|
125
127
|
lines.push("Unresolved blockers:");
|
|
126
128
|
lines.push(...summarizeRelationEntries(unresolvedBlockers));
|
|
127
129
|
}
|
|
128
|
-
if (
|
|
130
|
+
if (childIssues.length > 0) {
|
|
129
131
|
if (unresolvedBlockers.length > 0) {
|
|
130
132
|
lines.push("");
|
|
131
133
|
}
|
|
132
|
-
lines.push("
|
|
133
|
-
lines.push(...summarizeRelationEntries(
|
|
134
|
+
lines.push("Canonical child issues:");
|
|
135
|
+
lines.push(...summarizeRelationEntries(childIssues));
|
|
134
136
|
}
|
|
135
137
|
return lines;
|
|
136
138
|
}
|
|
@@ -166,15 +168,19 @@ function buildOrchestrationScopeDiscipline(context) {
|
|
|
166
168
|
const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
|
|
167
169
|
? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
168
170
|
: [];
|
|
169
|
-
const
|
|
170
|
-
? context.
|
|
171
|
-
:
|
|
171
|
+
const childIssues = Array.isArray(context?.childIssues)
|
|
172
|
+
? context.childIssues.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
173
|
+
: Array.isArray(context?.trackedDependents)
|
|
174
|
+
? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
175
|
+
: [];
|
|
172
176
|
return [
|
|
173
177
|
"## Scope Discipline",
|
|
174
178
|
"",
|
|
175
179
|
"This issue is orchestration work.",
|
|
176
180
|
"Treat it as the owner of convergence across related issues rather than as a normal code-owning implementation branch.",
|
|
177
181
|
"Inspect why this wake happened before acting.",
|
|
182
|
+
"Adopt already-existing canonical child issues when they cover the intended split.",
|
|
183
|
+
"Do not recreate child issues that already exist under this parent unless a genuinely missing required slice remains.",
|
|
178
184
|
"Do not create an overlapping umbrella PR unless this parent clearly owns unique direct cleanup work that child issues do not already cover.",
|
|
179
185
|
"If child work is still in motion, babysit the plan, record useful observations, and return to waiting.",
|
|
180
186
|
"If child work looks delivered, audit whether the original parent goal is actually satisfied.",
|
|
@@ -183,8 +189,8 @@ function buildOrchestrationScopeDiscipline(context) {
|
|
|
183
189
|
"",
|
|
184
190
|
"### Child Issue Summaries",
|
|
185
191
|
"",
|
|
186
|
-
...(
|
|
187
|
-
? summarizeRelationEntries(
|
|
192
|
+
...(childIssues.length > 0
|
|
193
|
+
? summarizeRelationEntries(childIssues, { emptyText: "No child issues are currently tracked." })
|
|
188
194
|
: ["No child issues are currently tracked."]),
|
|
189
195
|
"",
|
|
190
196
|
...(unresolvedBlockers.length > 0
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -150,10 +150,8 @@ export class RunOrchestrator {
|
|
|
150
150
|
...(entry.blockerCurrentLinearState ? { stateName: entry.blockerCurrentLinearState } : {}),
|
|
151
151
|
...(entry.blockerCurrentLinearStateType ? { stateType: entry.blockerCurrentLinearStateType } : {}),
|
|
152
152
|
}));
|
|
153
|
-
const
|
|
154
|
-
.
|
|
155
|
-
.map((entry) => this.db.issues.getIssue(issue.projectId, entry.linearIssueId))
|
|
156
|
-
.filter((entry) => Boolean(entry))
|
|
153
|
+
const childIssues = this.db.issues
|
|
154
|
+
.listChildIssues(issue.projectId, issue.linearIssueId)
|
|
157
155
|
.map((entry) => ({
|
|
158
156
|
linearIssueId: entry.linearIssueId,
|
|
159
157
|
...(entry.issueKey ? { issueKey: entry.issueKey } : {}),
|
|
@@ -163,17 +161,17 @@ export class RunOrchestrator {
|
|
|
163
161
|
delegatedToPatchRelay: entry.delegatedToPatchRelay,
|
|
164
162
|
hasOpenPr: entry.prNumber !== undefined && entry.prState !== "closed" && entry.prState !== "merged",
|
|
165
163
|
}));
|
|
166
|
-
if (unresolvedBlockers.length === 0 &&
|
|
164
|
+
if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
|
|
167
165
|
return {};
|
|
168
166
|
}
|
|
169
167
|
return {
|
|
170
168
|
...(unresolvedBlockers.length > 0 ? { unresolvedBlockers } : {}),
|
|
171
|
-
...(
|
|
169
|
+
...(childIssues.length > 0 ? { childIssues } : {}),
|
|
172
170
|
};
|
|
173
171
|
}
|
|
174
172
|
classifyTrackedIssue(issue) {
|
|
175
|
-
const
|
|
176
|
-
const classification = classifyIssue({ issue,
|
|
173
|
+
const childIssueCount = this.db.issues.listChildIssues(issue.projectId, issue.linearIssueId).length;
|
|
174
|
+
const classification = classifyIssue({ issue, childIssueCount });
|
|
177
175
|
if (issue.issueClass === classification.issueClass && issue.issueClassSource === classification.issueClassSource) {
|
|
178
176
|
return issue;
|
|
179
177
|
}
|
|
@@ -82,6 +82,7 @@ export class TrackedIssueListQuery {
|
|
|
82
82
|
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
83
83
|
i.current_linear_state, i.factory_state, i.delegated_to_patchrelay, s.session_state, s.waiting_reason, s.summary_text, s.display_updated_at,
|
|
84
84
|
i.pending_run_type,
|
|
85
|
+
i.orchestration_settle_until,
|
|
85
86
|
i.pr_number, i.pr_state, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
86
87
|
i.last_github_ci_snapshot_json,
|
|
87
88
|
i.last_github_failure_source,
|
|
@@ -162,6 +163,7 @@ export class TrackedIssueListQuery {
|
|
|
162
163
|
blockedByCount,
|
|
163
164
|
hasPendingWake,
|
|
164
165
|
hasLegacyPendingRun: row.pending_run_type !== null && row.pending_run_type !== undefined,
|
|
166
|
+
...(row.orchestration_settle_until !== null ? { orchestrationSettleUntil: String(row.orchestration_settle_until) } : {}),
|
|
165
167
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
166
168
|
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
167
169
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
@@ -181,6 +183,7 @@ export class TrackedIssueListQuery {
|
|
|
181
183
|
blockedByKeys,
|
|
182
184
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
183
185
|
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
186
|
+
...(row.orchestration_settle_until !== null ? { orchestrationSettleUntil: String(row.orchestration_settle_until) } : {}),
|
|
184
187
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
185
188
|
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
186
189
|
...(row.pr_head_sha !== null ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
@@ -15,6 +15,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
15
15
|
blockedByKeys,
|
|
16
16
|
factoryState: params.issue.factoryState,
|
|
17
17
|
pendingRunType: params.issue.pendingRunType,
|
|
18
|
+
orchestrationSettleUntil: params.issue.orchestrationSettleUntil,
|
|
18
19
|
prNumber: params.issue.prNumber,
|
|
19
20
|
prState: params.issue.prState,
|
|
20
21
|
prHeadSha: params.issue.prHeadSha,
|
|
@@ -64,6 +65,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
64
65
|
blockedByCount: unresolvedBlockedBy.length,
|
|
65
66
|
hasPendingWake: params.hasPendingWake,
|
|
66
67
|
hasLegacyPendingRun: params.issue.pendingRunType !== undefined,
|
|
68
|
+
orchestrationSettleUntil: params.issue.orchestrationSettleUntil,
|
|
67
69
|
...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
|
|
68
70
|
...(params.issue.prState ? { prState: params.issue.prState } : {}),
|
|
69
71
|
...(params.issue.prReviewState ? { prReviewState: params.issue.prReviewState } : {}),
|
package/dist/waiting-reason.js
CHANGED
|
@@ -11,6 +11,7 @@ export const PATCHRELAY_WAITING_REASONS = {
|
|
|
11
11
|
sameHeadStillBlocked: "Requested changes still block the current head",
|
|
12
12
|
waitingForMergeStewardRepair: "Waiting to repair a merge-steward incident",
|
|
13
13
|
waitingForDownstreamAutomation: "PatchRelay work is done; waiting on downstream review/merge automation",
|
|
14
|
+
waitingForChildSettle: "Waiting briefly for child issues to settle before orchestration starts",
|
|
14
15
|
workComplete: "PatchRelay work is complete",
|
|
15
16
|
waitingForOperatorIntervention: "Waiting on operator intervention",
|
|
16
17
|
waitingForExternalReview: "Waiting on external review",
|
|
@@ -33,6 +34,12 @@ export function derivePatchRelayWaitingReason(params) {
|
|
|
33
34
|
if (params.activeRunId !== undefined) {
|
|
34
35
|
return PATCHRELAY_WAITING_REASONS.activeWork;
|
|
35
36
|
}
|
|
37
|
+
if (params.orchestrationSettleUntil) {
|
|
38
|
+
const settleAt = Date.parse(params.orchestrationSettleUntil);
|
|
39
|
+
if (Number.isFinite(settleAt) && settleAt > Date.now()) {
|
|
40
|
+
return PATCHRELAY_WAITING_REASONS.waitingForChildSettle;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
36
43
|
const blockedByKeys = (params.blockedByKeys ?? []).filter((value) => value.trim().length > 0);
|
|
37
44
|
if (blockedByKeys.length > 0) {
|
|
38
45
|
return `Blocked by ${blockedByKeys.join(", ")}`;
|
|
@@ -27,7 +27,7 @@ export class CommentWakeHandler {
|
|
|
27
27
|
return;
|
|
28
28
|
const issueClass = classifyIssue({
|
|
29
29
|
issue,
|
|
30
|
-
|
|
30
|
+
childIssueCount: this.db.issues.listChildIssues(project.id, normalized.issue.id).length,
|
|
31
31
|
}).issueClass;
|
|
32
32
|
const trimmedBody = normalized.comment.body.trim();
|
|
33
33
|
const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
|
|
@@ -104,6 +104,9 @@ export function hasCompleteIssueContext(issue) {
|
|
|
104
104
|
export function mergeIssueMetadata(issue, liveIssue) {
|
|
105
105
|
return {
|
|
106
106
|
...issue,
|
|
107
|
+
...(issue.parentId ? {} : liveIssue.parentId ? { parentId: liveIssue.parentId } : {}),
|
|
108
|
+
...(issue.parentIdentifier ? {} : liveIssue.parentIdentifier ? { parentIdentifier: liveIssue.parentIdentifier } : {}),
|
|
109
|
+
...(issue.parentTitle ? {} : liveIssue.parentTitle ? { parentTitle: liveIssue.parentTitle } : {}),
|
|
107
110
|
...(issue.identifier ? {} : liveIssue.identifier ? { identifier: liveIssue.identifier } : {}),
|
|
108
111
|
...(issue.title ? {} : liveIssue.title ? { title: liveIssue.title } : {}),
|
|
109
112
|
...(issue.url ? {} : liveIssue.url ? { url: liveIssue.url } : {}),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { classifyIssue } from "../issue-class.js";
|
|
2
|
+
import { computeOrchestrationSettleUntil, wakeOrchestrationParentsForChildEvent, } from "../orchestration-parent-wake.js";
|
|
1
3
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
2
4
|
import { resolveAwaitingInputReason } from "../awaiting-input-reason.js";
|
|
3
5
|
import { appendDelegationObservedEvent } from "../delegation-audit.js";
|
|
4
|
-
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
6
|
+
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isResolvedLinearState, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
5
7
|
import { buildOperatorRetryEvent } from "../operator-retry-event.js";
|
|
6
8
|
import { resolveLinkedPullRequest } from "../linear-linked-pr-reconciliation.js";
|
|
7
9
|
import { readRemotePrState } from "../remote-pr-state.js";
|
|
@@ -70,6 +72,24 @@ export class DesiredStageRecorder {
|
|
|
70
72
|
terminal,
|
|
71
73
|
currentState: existingIssue?.factoryState,
|
|
72
74
|
});
|
|
75
|
+
const childIssueCount = this.db.issues.listChildIssues(params.project.id, normalizedIssue.id).length;
|
|
76
|
+
const classification = classifyIssue({
|
|
77
|
+
issue: {
|
|
78
|
+
issueClass: existingIssue?.issueClass,
|
|
79
|
+
issueClassSource: existingIssue?.issueClassSource,
|
|
80
|
+
title: hydratedIssue.title ?? existingIssue?.title,
|
|
81
|
+
description: hydratedIssue.description ?? existingIssue?.description,
|
|
82
|
+
parentLinearIssueId: hydratedIssue.parentId ?? existingIssue?.parentLinearIssueId,
|
|
83
|
+
},
|
|
84
|
+
childIssueCount,
|
|
85
|
+
});
|
|
86
|
+
const shouldEnterOrchestrationSettle = Boolean(delegated
|
|
87
|
+
&& desiredStage === "implementation"
|
|
88
|
+
&& classification.issueClass === "orchestration"
|
|
89
|
+
&& childIssueCount === 0
|
|
90
|
+
&& !existingIssue?.threadId
|
|
91
|
+
&& !activeRun
|
|
92
|
+
&& !terminal);
|
|
73
93
|
const runRelease = decideActiveRunRelease({
|
|
74
94
|
hasActiveRun: Boolean(activeRun),
|
|
75
95
|
terminal,
|
|
@@ -126,6 +146,10 @@ export class DesiredStageRecorder {
|
|
|
126
146
|
projectId: params.project.id,
|
|
127
147
|
linearIssueId: normalizedIssue.id,
|
|
128
148
|
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
149
|
+
...(hydratedIssue.parentId !== undefined ? { parentLinearIssueId: hydratedIssue.parentId ?? null } : {}),
|
|
150
|
+
...(hydratedIssue.parentIdentifier !== undefined ? { parentIssueKey: hydratedIssue.parentIdentifier ?? null } : {}),
|
|
151
|
+
issueClass: classification.issueClass,
|
|
152
|
+
issueClassSource: classification.issueClassSource,
|
|
129
153
|
...(hydratedIssue.title ? { title: hydratedIssue.title } : {}),
|
|
130
154
|
...(hydratedIssue.description ? { description: hydratedIssue.description } : {}),
|
|
131
155
|
...(hydratedIssue.url ? { url: hydratedIssue.url } : {}),
|
|
@@ -152,6 +176,7 @@ export class DesiredStageRecorder {
|
|
|
152
176
|
...(terminalRunRelease ? { factoryState: "done", pendingRunType: null, pendingRunContextJson: null } : {}),
|
|
153
177
|
...(blockerPausedImplementation ? { factoryState: "delegated" } : {}),
|
|
154
178
|
...(undelegation.factoryState ? { factoryState: undelegation.factoryState } : {}),
|
|
179
|
+
...(shouldEnterOrchestrationSettle ? { orchestrationSettleUntil: computeOrchestrationSettleUntil() } : {}),
|
|
155
180
|
});
|
|
156
181
|
if (effectiveRunRelease.release && activeRun && effectiveRunRelease.reason) {
|
|
157
182
|
this.db.runs.finishRun(activeRun.id, { status: "released", failureReason: effectiveRunRelease.reason });
|
|
@@ -166,6 +191,10 @@ export class DesiredStageRecorder {
|
|
|
166
191
|
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
167
192
|
}))
|
|
168
193
|
: this.db.transaction(commitIssueUpdate);
|
|
194
|
+
const previousParentIssueId = existingIssue?.parentLinearIssueId;
|
|
195
|
+
const currentParentIssueId = issue.parentLinearIssueId;
|
|
196
|
+
const wasResolved = isResolvedLinearState(existingIssue?.currentLinearStateType, existingIssue?.currentLinearState);
|
|
197
|
+
const isResolved = isResolvedLinearState(issue.currentLinearStateType, issue.currentLinearState);
|
|
169
198
|
if (undelegation.factoryState) {
|
|
170
199
|
if (activeRun?.threadId && activeRun.turnId) {
|
|
171
200
|
await params.stopActiveRun(activeRun, "STOP: The issue was un-delegated from PatchRelay. Stop working immediately and exit.");
|
|
@@ -213,6 +242,17 @@ export class DesiredStageRecorder {
|
|
|
213
242
|
...buildOperatorRetryEvent(issue, startupResume.pendingRunType, startupResume.source),
|
|
214
243
|
});
|
|
215
244
|
}
|
|
245
|
+
else if (shouldEnterOrchestrationSettle) {
|
|
246
|
+
this.feed?.publish({
|
|
247
|
+
level: "info",
|
|
248
|
+
kind: "stage",
|
|
249
|
+
issueKey: issue.issueKey,
|
|
250
|
+
projectId: params.project.id,
|
|
251
|
+
stage: issue.factoryState,
|
|
252
|
+
status: "settling_children",
|
|
253
|
+
summary: "Waiting briefly for child issues to settle before orchestration starts",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
216
256
|
else if (!startupResume.factoryState
|
|
217
257
|
&& !startupResume.pendingRunType
|
|
218
258
|
&&
|
|
@@ -232,6 +272,46 @@ export class DesiredStageRecorder {
|
|
|
232
272
|
dedupeKey: `delegated:${normalizedIssue.id}`,
|
|
233
273
|
});
|
|
234
274
|
}
|
|
275
|
+
if (previousParentIssueId && previousParentIssueId !== currentParentIssueId) {
|
|
276
|
+
wakeOrchestrationParentsForChildEvent({
|
|
277
|
+
db: this.db,
|
|
278
|
+
child: {
|
|
279
|
+
projectId: issue.projectId,
|
|
280
|
+
linearIssueId: issue.linearIssueId,
|
|
281
|
+
parentLinearIssueId: previousParentIssueId,
|
|
282
|
+
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
283
|
+
...(issue.title ? { title: issue.title } : {}),
|
|
284
|
+
factoryState: issue.factoryState,
|
|
285
|
+
...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
|
|
286
|
+
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
287
|
+
...(issue.prState ? { prState: issue.prState } : {}),
|
|
288
|
+
},
|
|
289
|
+
eventType: "child_changed",
|
|
290
|
+
changeKind: "detached",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (currentParentIssueId) {
|
|
294
|
+
const changeKind = previousParentIssueId !== currentParentIssueId
|
|
295
|
+
? "attached"
|
|
296
|
+
: issue.currentLinearState?.trim().toLowerCase() === "duplicate"
|
|
297
|
+
? "duplicate"
|
|
298
|
+
: issue.currentLinearStateType === "canceled"
|
|
299
|
+
? "canceled"
|
|
300
|
+
: "updated";
|
|
301
|
+
const eventType = previousParentIssueId !== currentParentIssueId
|
|
302
|
+
? "child_changed"
|
|
303
|
+
: !wasResolved && isResolved
|
|
304
|
+
? "child_delivered"
|
|
305
|
+
: wasResolved && !isResolved
|
|
306
|
+
? "child_regressed"
|
|
307
|
+
: "child_changed";
|
|
308
|
+
wakeOrchestrationParentsForChildEvent({
|
|
309
|
+
db: this.db,
|
|
310
|
+
child: issue,
|
|
311
|
+
eventType,
|
|
312
|
+
changeKind,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
235
315
|
return {
|
|
236
316
|
issue: this.db.issueToTrackedIssue(issue),
|
|
237
317
|
wakeRunType: params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id),
|
|
@@ -310,6 +390,11 @@ export class DesiredStageRecorder {
|
|
|
310
390
|
})),
|
|
311
391
|
});
|
|
312
392
|
}
|
|
393
|
+
this.db.issues.replaceIssueParentLink({
|
|
394
|
+
projectId,
|
|
395
|
+
childLinearIssueId: source.id,
|
|
396
|
+
parentLinearIssueId: source.parentId ?? null,
|
|
397
|
+
});
|
|
313
398
|
return { issue: source, hydration };
|
|
314
399
|
}
|
|
315
400
|
async resolveLinkedPrAdoption(params) {
|
package/dist/webhooks.js
CHANGED
|
@@ -195,6 +195,7 @@ function extractIssueMetadata(payload) {
|
|
|
195
195
|
return undefined;
|
|
196
196
|
}
|
|
197
197
|
const teamRecord = asRecord(issueRecord.team);
|
|
198
|
+
const parentRecord = asRecord(issueRecord.parent);
|
|
198
199
|
const identifier = getString(issueRecord, "identifier");
|
|
199
200
|
const title = getString(issueRecord, "title");
|
|
200
201
|
const url = getString(issueRecord, "url") ?? payload.url;
|
|
@@ -208,12 +209,18 @@ function extractIssueMetadata(payload) {
|
|
|
208
209
|
const delegateId = getString(issueRecord, "delegateId") ?? getString(delegateRecord ?? {}, "id");
|
|
209
210
|
const delegateName = getString(delegateRecord ?? {}, "name");
|
|
210
211
|
const description = getString(issueRecord, "description");
|
|
212
|
+
const parentId = getString(issueRecord, "parentId") ?? getString(parentRecord ?? {}, "id");
|
|
213
|
+
const parentIdentifier = getString(parentRecord ?? {}, "identifier");
|
|
214
|
+
const parentTitle = getString(parentRecord ?? {}, "title");
|
|
211
215
|
const rawPriority = issueRecord.priority;
|
|
212
216
|
const priority = typeof rawPriority === "number" ? rawPriority : undefined;
|
|
213
217
|
const rawEstimate = issueRecord.estimate;
|
|
214
218
|
const estimate = typeof rawEstimate === "number" ? rawEstimate : undefined;
|
|
215
219
|
return {
|
|
216
220
|
id,
|
|
221
|
+
...(parentId ? { parentId } : {}),
|
|
222
|
+
...(parentIdentifier ? { parentIdentifier } : {}),
|
|
223
|
+
...(parentTitle ? { parentTitle } : {}),
|
|
217
224
|
...(identifier ? { identifier } : {}),
|
|
218
225
|
...(title ? { title } : {}),
|
|
219
226
|
...(description ? { description } : {}),
|