patchrelay 0.48.0 → 0.49.1
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/cli/data.js +40 -15
- package/dist/db/issue-store.js +87 -4
- package/dist/db/migrations.js +34 -0
- package/dist/db.js +12 -0
- package/dist/effective-active-run.js +15 -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/run-reconciler.js +14 -0
- package/dist/tracked-issue-list-query.js +40 -12
- package/dist/tracked-issue-projector.js +18 -8
- package/dist/tracked-issue-query.js +5 -1
- 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/cli/data.js
CHANGED
|
@@ -9,6 +9,8 @@ import { buildManualRetryAttemptReset, resolveRetryTarget } from "../manual-issu
|
|
|
9
9
|
import { WorktreeManager } from "../worktree-manager.js";
|
|
10
10
|
import { parseDelegationObservedPayload, parseRunReleasedAuthorityPayload } from "../delegation-audit.js";
|
|
11
11
|
import { CliOperatorApiClient } from "./operator-client.js";
|
|
12
|
+
import { resolveEffectiveActiveRun } from "../effective-active-run.js";
|
|
13
|
+
import { derivePatchRelayWaitingReason } from "../waiting-reason.js";
|
|
12
14
|
function safeJsonParse(value) {
|
|
13
15
|
if (!value)
|
|
14
16
|
return undefined;
|
|
@@ -105,8 +107,11 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
105
107
|
if (!issue)
|
|
106
108
|
return undefined;
|
|
107
109
|
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
108
|
-
const activeRun = dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined;
|
|
109
110
|
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
111
|
+
const activeRun = resolveEffectiveActiveRun({
|
|
112
|
+
activeRun: dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined,
|
|
113
|
+
latestRun,
|
|
114
|
+
});
|
|
110
115
|
const latestReport = normalizeStageReport(latestRun?.reportJson, latestRun?.status);
|
|
111
116
|
const latestSummary = safeJsonParse(latestRun?.summaryJson);
|
|
112
117
|
const completionCheck = latestRun ? extractCompletionCheck(latestRun) : undefined;
|
|
@@ -139,7 +144,10 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
139
144
|
if (!issue)
|
|
140
145
|
return undefined;
|
|
141
146
|
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
142
|
-
const run =
|
|
147
|
+
const run = resolveEffectiveActiveRun({
|
|
148
|
+
activeRun: dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined,
|
|
149
|
+
latestRun: this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
|
|
150
|
+
});
|
|
143
151
|
if (!run)
|
|
144
152
|
return undefined;
|
|
145
153
|
const live = run.threadId &&
|
|
@@ -469,19 +477,36 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
469
477
|
ORDER BY i.updated_at DESC, i.issue_key ASC
|
|
470
478
|
`)
|
|
471
479
|
.all(...values);
|
|
472
|
-
const items = rows.map((row) =>
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
480
|
+
const items = rows.map((row) => {
|
|
481
|
+
const detachedActiveRun = row.active_run_type === null
|
|
482
|
+
&& (row.latest_run_status === "queued" || row.latest_run_status === "running");
|
|
483
|
+
const activeRunType = row.active_run_type !== null
|
|
484
|
+
? String(row.active_run_type)
|
|
485
|
+
: detachedActiveRun && row.latest_run_type !== null
|
|
486
|
+
? String(row.latest_run_type)
|
|
487
|
+
: undefined;
|
|
488
|
+
const waitingReason = detachedActiveRun
|
|
489
|
+
? derivePatchRelayWaitingReason({
|
|
490
|
+
activeRunId: 1,
|
|
491
|
+
factoryState: String(row.factory_state ?? "delegated"),
|
|
492
|
+
})
|
|
493
|
+
: row.waiting_reason !== null
|
|
494
|
+
? String(row.waiting_reason)
|
|
495
|
+
: undefined;
|
|
496
|
+
return {
|
|
497
|
+
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
498
|
+
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
499
|
+
projectId: String(row.project_id),
|
|
500
|
+
...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
|
|
501
|
+
...(row.session_state !== null ? { sessionState: detachedActiveRun ? "running" : String(row.session_state) } : {}),
|
|
502
|
+
factoryState: String(row.factory_state ?? "delegated"),
|
|
503
|
+
...(waitingReason ? { waitingReason } : {}),
|
|
504
|
+
...(activeRunType ? { activeRunType } : {}),
|
|
505
|
+
...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
|
|
506
|
+
...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
|
|
507
|
+
updatedAt: String(row.updated_at),
|
|
508
|
+
};
|
|
509
|
+
});
|
|
485
510
|
return items.filter((item) => {
|
|
486
511
|
if (options?.active && !item.activeRunType)
|
|
487
512
|
return false;
|
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
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function isActiveRunStatus(status) {
|
|
2
|
+
return status === "queued" || status === "running";
|
|
3
|
+
}
|
|
4
|
+
export function hasDetachedActiveLatestRun(params) {
|
|
5
|
+
return params.activeRunId === undefined
|
|
6
|
+
&& params.latestRun !== undefined
|
|
7
|
+
&& isActiveRunStatus(params.latestRun.status);
|
|
8
|
+
}
|
|
9
|
+
export function resolveEffectiveActiveRun(params) {
|
|
10
|
+
if (params.activeRun)
|
|
11
|
+
return params.activeRun;
|
|
12
|
+
if (params.latestRun && isActiveRunStatus(params.latestRun.status))
|
|
13
|
+
return params.latestRun;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
@@ -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;
|